(1) docker-compose.yml:11 hardcodes POSTGRES_PASSWORD: capakraken_dev (repo-committed). (2) AI/SMTP secrets passed via environment: in all compose files — visible in docker inspect and /proc/<pid>/environ. (3) Dockerfile.prod:47-62 sets placeholder secrets as ENV (not just ARG) → secrets persist in image layers, and missing-runtime-env silently falls back to placeholders. (4) Dockerfile.dev runs as root, no HEALTHCHECK.
Dockerfile.dev — no USER directive, no HEALTHCHECK
Impact
(1) Dev password in repo history. (2) Any host-process reading /proc sees AI keys. (3) Placeholder secret image-layer persistence — missing runtime env silently degrades security. (4) Dev-container compromise yields root.
Proposed Fix
(1) Replace with ${POSTGRES_PASSWORD:?required} in dev compose. (2) Migrate all secrets to Docker secrets: with file mounts (/run/secrets/*). (3) In Dockerfile.prod, drop ENV lines for secrets — keep only ARG; fail-fast if runtime env not set. (4) Add USER node to Dockerfile.dev + HEALTHCHECK hitting /api/health. (5) Verify .dockerignore covers **/.env*, **/*.pem, **/secrets/**.
Acceptance Criteria
No hardcoded secrets in compose files
Compose files use secrets: block for API keys
Dockerfile.prod: missing NEXTAUTH_SECRET at runtime → container exits
Fixed on branch security/audit-2026-04-17 (commit 805bb04).
What changed
1. Hardcoded dev password removed docker-compose.yml now requires ${POSTGRES_PASSWORD:?...} for both the postgres service init and the app container s DATABASE_URL. Compose refuses to start without a value — no silent fallback to a known-committed password. Mirrors the existing PGADMIN_PASSWORD pattern.
2. Placeholder secrets no longer bake into published images Dockerfile.prod used to ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET etc. at the builder stage. Since the published migrator image is FROM builder, those ENV lines leaked ci-build-placeholder-secret-minimum-32-chars into the registry-pushed artifact. Fixed by moving the env vars to an inline env prefix on the pnpm build RUN step — still available to next build, no longer persisted in any image layer.
3. runtime-env.ts guard extended
Added the two CI placeholder strings to DISALLOWED_PRODUCTION_SECRETS, so a prod container started with a leaked placeholder fails at startup instead of silently running with a known-bad secret. New unit test covers the case.
4. Dockerfile.dev HEALTHCHECK
Added HEALTHCHECK hitting /api/health on port 3100. Installed curl for it. (Did not add USER node — the dev image bind-mounts the host repo at .:/app; changing owner would break write access from host-triggered file edits.)
5. .dockerignore hardening
Added **/.env*, **/*.pem, **/*.key, **/secrets/** globs so nested package-level env files or accidental PEM files can t sneak into the build context.
6. CI adjustments
Fresh-Linux Docker Deploy job s minimal .env now writes POSTGRES_PASSWORD=capakraken_dev (matches the hardcoded password in docker-compose.ci.yml s DATABASE_URL override).
e2e-tests job sets POSTGRES_PASSWORD: ci-unused in its env block (same reason as the existing PGADMIN_PASSWORD: ci-unused — compose validates interpolation across all services before applying profile filters).
7. .env.example documented
Added the new POSTGRES_PASSWORD variable with explanation of the dev-vs-prod guidance.
Verification
pnpm test:unit — 396 test files / 1922 tests passed
docker compose config without POSTGRES_PASSWORD — fails with the required-variable error (as expected)
docker compose config with POSTGRES_PASSWORD=test — parses cleanly
Out of scope (left for follow-up)
Migrating AI/SMTP secrets from environment: to docker-compose secrets: mounts — that is a deployment-ops change (needs swarm or secrets manager), not a code change. Listed in the ticket as (2) but gated on infra decision; tracked separately if it becomes required.
USER node in Dockerfile.dev — rejected because of bind-mount ownership (see above).
Fixed on branch `security/audit-2026-04-17` (commit 805bb04).
### What changed
**1. Hardcoded dev password removed**
`docker-compose.yml` now requires `${POSTGRES_PASSWORD:?...}` for both the postgres service init and the app container s `DATABASE_URL`. Compose refuses to start without a value — no silent fallback to a known-committed password. Mirrors the existing `PGADMIN_PASSWORD` pattern.
**2. Placeholder secrets no longer bake into published images**
`Dockerfile.prod` used to `ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET` etc. at the builder stage. Since the published `migrator` image is `FROM builder`, those ENV lines leaked `ci-build-placeholder-secret-minimum-32-chars` into the registry-pushed artifact. Fixed by moving the env vars to an inline env prefix on the `pnpm build` RUN step — still available to `next build`, no longer persisted in any image layer.
**3. `runtime-env.ts` guard extended**
Added the two CI placeholder strings to `DISALLOWED_PRODUCTION_SECRETS`, so a prod container started with a leaked placeholder fails at startup instead of silently running with a known-bad secret. New unit test covers the case.
**4. Dockerfile.dev HEALTHCHECK**
Added `HEALTHCHECK` hitting `/api/health` on port 3100. Installed `curl` for it. (Did not add `USER node` — the dev image bind-mounts the host repo at `.:/app`; changing owner would break write access from host-triggered file edits.)
**5. .dockerignore hardening**
Added `**/.env*`, `**/*.pem`, `**/*.key`, `**/secrets/**` globs so nested package-level env files or accidental PEM files can t sneak into the build context.
**6. CI adjustments**
- `Fresh-Linux Docker Deploy` job s minimal `.env` now writes `POSTGRES_PASSWORD=capakraken_dev` (matches the hardcoded password in `docker-compose.ci.yml` s `DATABASE_URL` override).
- `e2e-tests` job sets `POSTGRES_PASSWORD: ci-unused` in its env block (same reason as the existing `PGADMIN_PASSWORD: ci-unused` — compose validates interpolation across all services before applying profile filters).
**7. .env.example documented**
Added the new `POSTGRES_PASSWORD` variable with explanation of the dev-vs-prod guidance.
### Verification
- `pnpm test:unit` — 396 test files / 1922 tests passed
- `pnpm lint` — 0 errors
- `pnpm --filter @capakraken/web exec tsc --noEmit` — clean
- `node scripts/check-architecture-guardrails.mjs` — passed
- `docker compose config` without `POSTGRES_PASSWORD` — fails with the required-variable error (as expected)
- `docker compose config` with `POSTGRES_PASSWORD=test` — parses cleanly
### Out of scope (left for follow-up)
- Migrating AI/SMTP secrets from `environment:` to docker-compose `secrets:` mounts — that is a deployment-ops change (needs swarm or secrets manager), not a code change. Listed in the ticket as (2) but gated on infra decision; tracked separately if it becomes required.
- `USER node` in `Dockerfile.dev` — rejected because of bind-mount ownership (see above).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
(1)
docker-compose.yml:11hardcodesPOSTGRES_PASSWORD: capakraken_dev(repo-committed). (2) AI/SMTP secrets passed viaenvironment:in all compose files — visible indocker inspectand/proc/<pid>/environ. (3)Dockerfile.prod:47-62sets placeholder secrets asENV(not justARG) → secrets persist in image layers, and missing-runtime-env silently falls back to placeholders. (4)Dockerfile.devruns as root, no HEALTHCHECK.Evidence
docker-compose.yml:11 — POSTGRES_PASSWORD hardcodeddocker-compose.yml:73-85 — AZURE_OPENAI_API_KEY via env: not secrets:Dockerfile.prod:56-61 — ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRETDockerfile.dev — no USER directive, no HEALTHCHECKImpact
(1) Dev password in repo history. (2) Any host-process reading /proc sees AI keys. (3) Placeholder secret image-layer persistence — missing runtime env silently degrades security. (4) Dev-container compromise yields root.
Proposed Fix
(1) Replace with
${POSTGRES_PASSWORD:?required}in dev compose. (2) Migrate all secrets to Dockersecrets:with file mounts (/run/secrets/*). (3) In Dockerfile.prod, dropENVlines for secrets — keep onlyARG; fail-fast if runtime env not set. (4) AddUSER nodeto Dockerfile.dev + HEALTHCHECK hitting /api/health. (5) Verify.dockerignorecovers**/.env*,**/*.pem,**/secrets/**.Acceptance Criteria
secrets:block for API keysParent Epic: #1
Source: Full-Codebase Security Audit 2026-04-16 (C-9, C-10, C-13)
Fixed on branch
security/audit-2026-04-17(commit805bb04).What changed
1. Hardcoded dev password removed
docker-compose.ymlnow requires${POSTGRES_PASSWORD:?...}for both the postgres service init and the app container sDATABASE_URL. Compose refuses to start without a value — no silent fallback to a known-committed password. Mirrors the existingPGADMIN_PASSWORDpattern.2. Placeholder secrets no longer bake into published images
Dockerfile.prodused toENV NEXTAUTH_SECRET=$NEXTAUTH_SECRETetc. at the builder stage. Since the publishedmigratorimage isFROM builder, those ENV lines leakedci-build-placeholder-secret-minimum-32-charsinto the registry-pushed artifact. Fixed by moving the env vars to an inline env prefix on thepnpm buildRUN step — still available tonext build, no longer persisted in any image layer.3.
runtime-env.tsguard extendedAdded the two CI placeholder strings to
DISALLOWED_PRODUCTION_SECRETS, so a prod container started with a leaked placeholder fails at startup instead of silently running with a known-bad secret. New unit test covers the case.4. Dockerfile.dev HEALTHCHECK
Added
HEALTHCHECKhitting/api/healthon port 3100. Installedcurlfor it. (Did not addUSER node— the dev image bind-mounts the host repo at.:/app; changing owner would break write access from host-triggered file edits.)5. .dockerignore hardening
Added
**/.env*,**/*.pem,**/*.key,**/secrets/**globs so nested package-level env files or accidental PEM files can t sneak into the build context.6. CI adjustments
Fresh-Linux Docker Deployjob s minimal.envnow writesPOSTGRES_PASSWORD=capakraken_dev(matches the hardcoded password indocker-compose.ci.ymlsDATABASE_URLoverride).e2e-testsjob setsPOSTGRES_PASSWORD: ci-unusedin its env block (same reason as the existingPGADMIN_PASSWORD: ci-unused— compose validates interpolation across all services before applying profile filters).7. .env.example documented
Added the new
POSTGRES_PASSWORDvariable with explanation of the dev-vs-prod guidance.Verification
pnpm test:unit— 396 test files / 1922 tests passedpnpm lint— 0 errorspnpm --filter @capakraken/web exec tsc --noEmit— cleannode scripts/check-architecture-guardrails.mjs— passeddocker compose configwithoutPOSTGRES_PASSWORD— fails with the required-variable error (as expected)docker compose configwithPOSTGRES_PASSWORD=test— parses cleanlyOut of scope (left for follow-up)
environment:to docker-composesecrets:mounts — that is a deployment-ops change (needs swarm or secrets manager), not a code change. Listed in the ticket as (2) but gated on infra decision; tracked separately if it becomes required.USER nodeinDockerfile.dev— rejected because of bind-mount ownership (see above).