Security [HIGH]: Docker + Compose — hardcoded dev password, env-var secrets, placeholder secrets baked in prod image #50

Closed
opened 2026-04-16 22:05:11 +02:00 by Hartmut · 1 comment
Owner

Problem

(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.

Evidence

  • docker-compose.yml:11 — POSTGRES_PASSWORD hardcoded
  • docker-compose.yml:73-85 — AZURE_OPENAI_API_KEY via env: not secrets:
  • Dockerfile.prod:56-61 — ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
  • 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
  • Dockerfile.dev: non-root USER + HEALTHCHECK
  • .dockerignore verified complete

Parent Epic: #1
Source: Full-Codebase Security Audit 2026-04-16 (C-9, C-10, C-13)

## Problem (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. ## Evidence - `docker-compose.yml:11 — POSTGRES_PASSWORD hardcoded` - `docker-compose.yml:73-85 — AZURE_OPENAI_API_KEY via env: not secrets:` - `Dockerfile.prod:56-61 — ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET` - `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 - [ ] Dockerfile.dev: non-root USER + HEALTHCHECK - [ ] .dockerignore verified complete --- Parent Epic: #1 Source: Full-Codebase Security Audit 2026-04-16 (C-9, C-10, C-13)
Hartmut added the security label 2026-04-16 22:05:11 +02:00
Author
Owner

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).
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).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Hartmut/CapaKraken#50