security(docker): remove hardcoded dev password, stop placeholder secrets leaking into migrator image (#50)
- docker-compose.yml: require ${POSTGRES_PASSWORD} for the postgres service
and the app container's DATABASE_URL. No default — compose refuses to start
without it, mirroring the existing PGADMIN_PASSWORD pattern.
- Dockerfile.prod: move auth/db ENV assignments from persistent ENV lines into
an inline env prefix on the `pnpm build` RUN step. Placeholders are still
available to `next build` but no longer persist in the builder layer or in
the published migrator image (which is FROM builder).
- Dockerfile.dev: add HEALTHCHECK against /api/health and install curl for it.
- .dockerignore: cover nested **/.env*, **/*.pem, **/*.key, **/secrets/**.
- runtime-env.ts: add the CI build placeholder strings to the disallowed-secret
set so a misconfigured prod deploy using the baked-in ARG defaults fails
startup instead of silently running with a known-bad secret.
- .env.example: document the new POSTGRES_PASSWORD requirement.
- CI: write POSTGRES_PASSWORD into the Fresh-Linux Docker Deploy job's .env
(must match docker-compose.ci.yml's hardcoded DATABASE_URL), and provide a
dummy value in the E2E job where compose validates all services' interp.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+11
-1
@@ -17,11 +17,21 @@ node_modules
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Environment files (injected at runtime)
|
||||
# Environment files (injected at runtime). Glob variants catch nested
|
||||
# .env, .env.local, etc. inside any package directory.
|
||||
.env
|
||||
.env.*
|
||||
**/.env
|
||||
**/.env.*
|
||||
!.env.example
|
||||
|
||||
# Private keys, certificates, and any secrets-like directory. Defence in
|
||||
# depth against accidentally bind-mounting or COPYing these in.
|
||||
**/*.pem
|
||||
**/*.key
|
||||
**/secrets
|
||||
**/secrets/**
|
||||
|
||||
# Test artifacts
|
||||
coverage
|
||||
**/coverage
|
||||
|
||||
+11
-4
@@ -21,10 +21,17 @@ NEXTAUTH_SECRET=
|
||||
|
||||
# ─── Database ────────────────────────────────────────────────────────────────
|
||||
|
||||
# REQUIRED — PostgreSQL connection string.
|
||||
# When running with Docker Compose the app container uses the Docker-internal
|
||||
# host (postgres:5432); the host-level connection (for pnpm dev on the host)
|
||||
# uses localhost:5433 (the published port).
|
||||
# REQUIRED when starting Docker Compose — postgres container initializes with
|
||||
# this password and the app container derives DATABASE_URL from it. No default
|
||||
# is shipped; set any non-empty value for local dev, use a generated secret in
|
||||
# any shared or production environment.
|
||||
# Generate one with: openssl rand -hex 32
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# REQUIRED — PostgreSQL connection string used by `pnpm dev` running on the
|
||||
# host (outside Docker). Must match POSTGRES_PASSWORD above. Inside the app
|
||||
# container this variable is overridden by docker-compose.yml (which routes
|
||||
# to the postgres service name on the internal network).
|
||||
DATABASE_URL=postgresql://capakraken:capakraken_dev@localhost:5433/capakraken
|
||||
|
||||
# ─── Redis ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -323,6 +323,11 @@ jobs:
|
||||
# ${PGADMIN_PASSWORD:?} check fires and aborts the compose call.
|
||||
# Provide a dummy value so parsing succeeds — pgadmin is never started.
|
||||
PGADMIN_PASSWORD: ci-unused
|
||||
# Same reason as PGADMIN_PASSWORD: docker compose validates env
|
||||
# interpolation across all services, including postgres (which has
|
||||
# ${POSTGRES_PASSWORD:?}). Dummy value — postgres service is not used
|
||||
# here (the `e2epg` GH Actions service container is).
|
||||
POSTGRES_PASSWORD: ci-unused
|
||||
# Tell test-server.mjs not to spin up its own postgres-test container
|
||||
# — the e2epg job service is already running and reachable. Without
|
||||
# this, test-server tries to publish 5432 on the QNAP host, which
|
||||
@@ -462,6 +467,9 @@ jobs:
|
||||
NEXTAUTH_URL=http://localhost:3100
|
||||
NEXTAUTH_SECRET=ci-test-secret-minimum-32-chars-xx
|
||||
PGADMIN_PASSWORD=ci-pgadmin
|
||||
# Must match the password baked into docker-compose.ci.yml's
|
||||
# DATABASE_URL override (capakraken_dev).
|
||||
POSTGRES_PASSWORD=capakraken_dev
|
||||
EOF
|
||||
|
||||
- name: Tear down any stale stack & volumes
|
||||
|
||||
+5
-2
@@ -1,7 +1,7 @@
|
||||
FROM node:20-bookworm-slim AS base
|
||||
|
||||
# Prisma needs OpenSSL available during install/generate/runtime.
|
||||
RUN apt-get update -y && apt-get install -y openssl postgresql-client && rm -rf /var/lib/apt/lists/*
|
||||
# Prisma needs OpenSSL; curl is used by HEALTHCHECK below.
|
||||
RUN apt-get update -y && apt-get install -y openssl postgresql-client curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install pnpm
|
||||
RUN npm install -g pnpm@9.14.2
|
||||
@@ -30,4 +30,7 @@ RUN pnpm --filter @capakraken/db db:generate
|
||||
|
||||
EXPOSE 3100
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
|
||||
CMD curl -fsS http://localhost:3100/api/health || exit 1
|
||||
|
||||
CMD ["sh", "./tooling/docker/app-dev-start.sh"]
|
||||
|
||||
+11
-7
@@ -47,19 +47,23 @@ ENV NODE_ENV=production
|
||||
# next build collects page data for /api/auth/[...nextauth] which crashes
|
||||
# without these envs even though they are placeholders at image-build time
|
||||
# (real values are injected at container start). Mirrors the CI build job.
|
||||
#
|
||||
# IMPORTANT: pass these only as inline env on the RUN step, not via `ENV`.
|
||||
# `ENV` persists the placeholder into the image layer — scanned as a leaked
|
||||
# secret and inherited by the `migrator` stage (which is published).
|
||||
ARG NEXTAUTH_URL=http://localhost:3100
|
||||
ARG AUTH_URL=http://localhost:3100
|
||||
ARG NEXTAUTH_SECRET=ci-build-placeholder-secret-minimum-32-chars
|
||||
ARG AUTH_SECRET=ci-build-placeholder-secret-minimum-32-chars
|
||||
ARG DATABASE_URL=postgresql://placeholder:placeholder@localhost:5432/placeholder
|
||||
ARG REDIS_URL=redis://placeholder:6379
|
||||
ENV NEXTAUTH_URL=$NEXTAUTH_URL
|
||||
ENV AUTH_URL=$AUTH_URL
|
||||
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
|
||||
ENV AUTH_SECRET=$AUTH_SECRET
|
||||
ENV DATABASE_URL=$DATABASE_URL
|
||||
ENV REDIS_URL=$REDIS_URL
|
||||
RUN pnpm --filter @capakraken/web build
|
||||
RUN NEXTAUTH_URL="$NEXTAUTH_URL" \
|
||||
AUTH_URL="$AUTH_URL" \
|
||||
NEXTAUTH_SECRET="$NEXTAUTH_SECRET" \
|
||||
AUTH_SECRET="$AUTH_SECRET" \
|
||||
DATABASE_URL="$DATABASE_URL" \
|
||||
REDIS_URL="$REDIS_URL" \
|
||||
pnpm --filter @capakraken/web build
|
||||
|
||||
# ============================================================
|
||||
# Stage 3: Migration runner
|
||||
|
||||
@@ -32,7 +32,21 @@ describe("runtime env validation", () => {
|
||||
NEXTAUTH_SECRET: "dev-secret-change-in-production",
|
||||
NEXTAUTH_URL: "https://capakraken.example.com",
|
||||
}),
|
||||
).toContain("AUTH_SECRET or NEXTAUTH_SECRET must not use a known development placeholder in production.");
|
||||
).toContain(
|
||||
"AUTH_SECRET or NEXTAUTH_SECRET must not use a known development placeholder in production.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects the CI build-time placeholder that leaks from Dockerfile ARG default", () => {
|
||||
expect(
|
||||
getRuntimeEnvViolations({
|
||||
NODE_ENV: "production",
|
||||
NEXTAUTH_SECRET: "ci-build-placeholder-secret-minimum-32-chars",
|
||||
NEXTAUTH_URL: "https://capakraken.example.com",
|
||||
}),
|
||||
).toContain(
|
||||
"AUTH_SECRET or NEXTAUTH_SECRET must not use a known development placeholder in production.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects non-https auth urls in production", () => {
|
||||
|
||||
@@ -6,6 +6,8 @@ const DISALLOWED_PRODUCTION_SECRETS = new Set([
|
||||
"change-me",
|
||||
"default",
|
||||
"secret",
|
||||
"ci-build-placeholder-secret-minimum-32-chars",
|
||||
"ci-test-secret-minimum-32-chars-xx",
|
||||
]);
|
||||
|
||||
type RuntimeEnv = Partial<Record<string, string | undefined>>;
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ services:
|
||||
environment:
|
||||
POSTGRES_DB: capakraken
|
||||
POSTGRES_USER: capakraken
|
||||
POSTGRES_PASSWORD: capakraken_dev
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env (any non-empty value for local dev)}
|
||||
command: >
|
||||
postgres
|
||||
-c log_connections=on
|
||||
@@ -61,7 +61,7 @@ services:
|
||||
# Always use the Docker-internal service name. The host-level DATABASE_URL
|
||||
# (localhost:5433) must not bleed into the container where "localhost" is
|
||||
# the container itself, not the host.
|
||||
DATABASE_URL: postgresql://capakraken:capakraken_dev@postgres:5432/capakraken
|
||||
DATABASE_URL: postgresql://capakraken:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/capakraken
|
||||
REDIS_URL: redis://redis:6379
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL:?NEXTAUTH_URL must be set (e.g. https://your-domain.com)}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?set NEXTAUTH_SECRET}
|
||||
|
||||
Reference in New Issue
Block a user