BuildNot yet confirmed elsewhere1 publisher3 min readPublished
The five-minute grep: what `synchronize: true` tells a reviewer before your tests do
A pass over one security suite turned up three patterns that never fail at runtime and sink a procurement read anyway. The arithmetic on the compose file is the interesting part.
The Engineer · Build desk
What happened
- A developer re-read his own multi-service security suite as an AppSec engineer would on clone, and found three problems that nothing at runtime complained about.
- auth-api shipped with TypeORM's synchronize enabled whenever DB_SYNC was unset, so the schema was re-derived from entity classes on every boot.
- The database module was printing its own config on boot, password included, until the console.log line was deleted.
- The tracked compose file held eighteen occurrences of six distinct secrets, all of them change-me placeholders.
Why it matters
- exposure By the author's own account of where stdout travels, every CI run and log sink that touched that boot output was holding the database password.
- constraint Replay protection is capped at one process: a restart empties the nonce memory, so horizontal scaling waits on a shared store rather than a config change.
- decision With one service referencing all six shared secrets, requiring them there makes docker compose config the go/no-go on an insecure boot, before any container runs.
- precedent If a five-minute grep decides hiring and procurement reads, defaults are a shipping item and not backlog cleanup, which is a standard applied to your repo whether or not you agree to it.
Grepping for `synchronize` is efficient because one line answers a question that otherwise takes an afternoon: does anyone own this schema? The setting rebuilds tables from entity classes on every boot, which leaves no migration history, no review step when the shape of the data changes, and a renamed column sitting one deploy away from data loss [2].
Flipping the default to false and switching on `migrationsRun` is the easy half [4]. Generating the baseline is where this could have gone quietly wrong. The data source that feeds the TypeORM CLI had been carrying a hand-listed subset of four entity classes, while the baseline it was meant to describe covers 16 tables across auth and billing [6][16]. Run as it stood, `migration:generate` would have emitted something that looked like a full schema and left the rest out [6]. Nothing errors in that scenario; the baseline is simply short, and every migration after it inherits the hole. The author's precaution was to generate against an empty throwaway Postgres and then apply the output to confirm it stands up [5], which is the only way to learn whether the entity glob is honest.
The compose file arithmetic is worth doing. Six logical secrets written eighteen times collapse into six interpolation variables, which removes twelve places where a value can be edited out of step with its twin [15]. The twins mattered: the same JWT secret was pasted into three service blocks, with auth signing and both zerotrust and billing verifying, so one careless edit produces cross-service auth failures that look like anything except a typo [9]. The same change fixes the duller complaint, which is that a secret baked into a tracked file cannot be overridden without editing the repository [9]. Real values now come from a gitignored root `.env`, with a fallback named `dev-insecure-*` so `docker compose up` and CI still run with no setup [10]. The production overlay drops the fallbacks and uses the `:?` form, so a missing variable stops compose instead of booting the stack on a placeholder [11].
Two of the three findings are reading problems: nothing misbehaves, the repo just reads badly. The third is different. Internal service-to-service calls and vault's Zero Trust requests are HMAC-signed with a nonce, and replay rejection depends on remembering those nonces for a short window in a per-process `Map` [13]. That is a functional limit rather than a matter of taste, and it is the one item on the list a reviewer and a load test would both find.
For a product whose buyers are engineers, the repository is part of the surface being evaluated; the person deciding whether to trust it opens it rather than clicking through the demo [17].
What to watch
- Whether the nonce cache moves to a shared store; the writeup cuts off before showing a multi-instance replacement.
- Whether a second migration:generate against the migrated database returns an empty diff, which is the real check on the entity glob.
- Whether billing-api and vault-api get the same DB_SYNC default and production-overlay treatment as auth-api.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence52
- Adoption
- Insufficient
- Hype gap+8
- Incentives62
- Confidence45
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
The author reviewed his own sytadel-suite repository (auth-api, billing-api, vault-api and the root compose file) asking what an AppSec engineer cloning it would wince at; three findings resulted, none of which broke anything at runtime.
- [2]
TypeORM's synchronize auto-derives the database schema from entities on every boot: no migration history, no review of schema changes, and one renamed column away from data loss.
- [3]
In auth-api the default was on: synchronize: String(process.env.DB_SYNC ?? 'true') === 'true'.
- [4]
The fix makes migrations the source of truth: DB_SYNC now defaults to false, a migrations glob is configured, and migrationsRun is true so migrations apply on boot.
- [5]
The baseline was generated with migration:generate against an empty throwaway Postgres so the diff covered the entire current schema (12 tables for auth, 4 for billing), then applied to confirm it stands up.
- [6]
The data-source.ts used for the CLI now globs every *.entity.ts; the old one listed a stale subset of four entities, which would have generated an incomplete baseline and silently missed tables.
- [7]
The DB module logged its own config on boot with console.log('DB CONFIG =>', db), and db includes the password; the line was deleted.
- [8]
The committed compose file contained eighteen occurrences of six distinct secrets written as placeholders, for example AUTH_JWT_ACCESS_SECRET: change-me-access-secret.
- [9]
Two problems: secrets in a committed file cannot be overridden without editing the repo, and the same JWT secret was copy-pasted across three service blocks (auth signs, zerotrust and billing verify), so it can drift out of sync during an edit and break cross-service auth in a way that is miserable to debug.
- [10]
The fix externalises each logical secret to a single interpolation variable reused everywhere it is shared, with real values from a gitignored root .env and a dev-insecure-* fallback that keeps docker compose up and CI working with zero setup.
- [11]
docker-compose.prod.yml removes the fallbacks and uses ${JWT_ACCESS_SECRET:?set JWT_ACCESS_SECRET in .env for production}, so a missing variable makes compose refuse to start with a required-variable error.
- [12]
Because auth-api references all six shared secrets, requiring them in that one service block gates the entire stack at docker compose config time, so nothing starts on placeholder secrets.
- [13]
Internal service-to-service calls and vault's Zero Trust requests are HMAC-signed with a nonce, and replays are rejected by remembering nonces for a short window in a per-process Map replayCache.
- [14]
That per-process nonce cache is correct for exactly one instance: a restart loses the memory, and scaling to two instances breaks the replay guarantee.
- [15]
Collapsing eighteen hardcoded secret occurrences into six interpolation variables removes twelve duplicate definitions, each of which was a place a value could be edited out of step with its twin.
- [16]
The generated baseline puts 16 tables across auth and billing under migration control.
- [17]
For a security product the reader is the user: the person deciding whether to trust you is going to open the repo, not just click the buttons.
- [18]
All three findings are the kind that turn "interesting" into "no thanks" in a hiring or procurement read, and are patterns a reviewer is trained to grep for in the first five minutes.
- [19]
A credential in stdout is a credential in your log aggregator, your CI output, and anywhere those get shipped.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toHarden for the reader: three red flags an AppSec engineer hits on `git clone`
1 article · August 23, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.
Topics
Entities
- sytadel-suiteFollow
- TypeORMFollow
- PostgreSQLFollow
- Docker ComposeFollow
- RedisFollow
- Elwin ErnstFollow
- JSON Web TokenFollow