[E00-S02-T08] Secrets are not embedded in image #389
No Reviewers
Labels
Clear labels
agent/analyst-drafted
agent/analyst-drafted
needs/human-decision
needs/human-decision
needs/security-review
needs/security-review
tier/t0
tier/t1
tier/t2
tier/t3
kind
bug
kind
bug
kind
epic
kind
epic
kind
initiative
EPPP programme initiative
kind
story
kind
story
kind
task
EPPP engineering card/task decomposed from a story
kind
toil
kind
toil
loop
1
loop
1
loop
2
loop
2
loop
3
loop
3
risk
agent-full
risk
agent-full
risk
human-gated
risk
human-gated
risk
human-only
risk
human-only
size
l
size
l
size
m
size
m
size
s
size
s
status
blocked
status
blocked
status
done
Workflow: Done
status
in-progress
status
in-progress
status
proposed
status
proposed
status
ready
status
ready
status
review
status
review
stream
checkout
stream
checkout
stream
onboarding
stream
onboarding
stream
platform
stream
platform
trivial — implementer only, auto-merge
standard — implementer + reviewer + tester
complex — security if triggered, human merge
critical — full chain + security, human merge
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Fabrika/PersonalBlog#389
Reference in New Issue
Block a user
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.
What changed
Implements [E00-S02-T08] Secrets are not embedded in image (#175) on top of the [E00-S02-T01..T07] Compose baseline (#59): the workspace server application image carries no secrets — statically guaranteed by the committed Dockerfile and
.dockerignore, and verified end to end by scanning a built image's layers.This revision resolves the security review of the previous head (issuecomment-2784, findings 1–4) against the reworked acceptance criteria:
.dockerignore— exclusions now apply at the root AND at any depth. Every env/credential pattern is**/-prefixed:**/.env,**/.env.*,**/node_modules,**/.npmrc,**/.netrc,**/.credentials,**/.aws,**/.ssh,**/secrets,**/*.pem,**/*.key,**/*.p12,**/*.pfx,**/*.jks,**/id_rsa,**/id_ed25519. Docker's real matcher (moby/patternmatcher) anchors slash-less patterns to the context root, so the previous bare.npmrc/*.key/secretsforms excluded nothing underapps/server/…(finding 1). The redundantsecrets/duplicate is gone (finding 3).tests/secrets-not-embedded.test.mjs— the matcher is now Docker-faithful. It is a port of moby/patternmatcher (filepath.Clean+ anchored full-path match + parent-directory propagation), not gitignore basename semantics; it asserts the nested example paths (apps/server/.npmrc,config/server.key,apps/server/secrets/…) are excluded, requires the**/-prefixed forms verbatim, and requires no redundant equivalent patterns (finding 2/3). The Docker-gated layer-scan probe now plants marker files at nested paths the Dockerfile'sCOPY apps/server apps/serverwould sweep in (apps/server/.env.t08-*,apps/server/secrets/t08-*.pem), so a nested-context leak is observable end to end.apps/server/Dockerfile+compose.yaml— the guarantee is stated precisely: credential files are excluded from the build context at the root and at any depth, so a local secret file cannot be embedded even by mistake (finding 3)..gitea/workflows/ci.yml— the guarantee is enforced in CI: a newsecrets-not-embeddedjob runsnode --test tests/secrets-not-embedded.test.mjson every PR; the Docker-gated layer-scan probe runs where a Docker daemon is reachable and skips cleanly otherwise (finding 4).tests/compose-config.test.mjs— the.dockerignorepresence list updated to the**/-prefixed forms.Runtime credentials (e.g.
DATABASE_URL) remain Compose-injected at run time (compose.yamlapp.environment), never baked into the image.Explicitly out of scope per the brief, not touched: multi-arch build targets (E00-S02-T07 — unchanged behavior), app health endpoint (E00-S02-T03).
Criterion → test table
secrets are not embedded in the imagetests/secrets-not-embedded.test.mjs— "the app image embeds no secrets (Dockerfile declares no secret ENV/ARG and copies no secret paths)" (static: no secret-bearingARG/ENVname/value — the only ENV isNODE_ENV=production— noCOPYof.env/credential paths, no blanketCOPY . .); "the build context excludes env and credential files (.dockerignore)" (static: every**/-prefixed secret pattern present verbatim, nested example paths + root forms excluded under Docker's own matcher semantics, no redundant equivalent patterns required, image inputs kept); "the committed files copied into the image contain no default credential values" (static scan of the files the Dockerfile copies). Mutation probes prove non-vacuous: addingENV POSTGRES_PASSWORD=…,ARG DATABASE_URL=…, a credential-URIENV,COPY .env,COPY . ., removing**/.env.*/**/*.key, replacing a**/pattern with its root-anchored bare form, or adding redundantsecrets+secrets/— all failimage layers contain no secret valuestests/secrets-not-embedded.test.mjs— "the built image layers contain no secret values (docker build + layer scan)" (Docker-gated):docker buildthe committed image with marker probe files planted at the root (**/.env.*) AND at nested paths swept byCOPY apps/server apps/server(apps/server/.env.t08-*,apps/server/secrets/t08-*.pem),docker save+ extract, scan every layer (raw + gunzipped) and the image config — the probe markers andpostgres://eppp:eppp@db:5432/epppmust appear nowherethe guarantee is enforced in CI.gitea/workflows/ci.yml—secrets-not-embeddedjob runsnode --test tests/secrets-not-embedded.test.mjson every PR (static assertions gate merges; the layer-scan probe runs where a Docker daemon exists and skips cleanly otherwise)Test plan executed
node --test tests/secrets-not-embedded.test.mjs→ 15/15 pass, 1 skipped (the Docker-gated layer-scan probe skips cleanly where no Docker daemon exists; this sandbox has none).node --test tests/*.test.mjs(full suite) → 101 pass / 12 fail / 8 skip; the 12 failures are identical to cleanmain(frozen-install / root-commands / strict-tsconfig / node-engine / typescript-pin suites needpnpm install+ Node 24, absent here: nonode_modules, Node 22.23.2) — zero new failures, +2 secrets tests net..npmrc/*.key/secretsdo NOT match nested paths;**/-prefixed forms match at the root and any depth; parent-directory propagation prunes matched dirs).secrets-not-embeddedjob → 16 tests, 15 pass / 1 skip (no Docker daemon in the runner container), job green;Frozen lockfile installgreen. The static assertions now gate every PR.Risks / notes
.dockerignorehardening + static/mutation tests + nested-marker layer-scan probe + CI enforcement)..env.t08-<uuid>,apps/server/.env.t08-<uuid>,apps/server/secrets/t08-<uuid>.pem, all matched by the**/-prefixed exclusions), so it cannot collide with a developer's real files and cannot pass on a stale cache; probe files are removed infinally(the transientapps/server/secrets/dir only if the probe created it).skipwithout a daemon, so the suite stays green everywhere while giving real layer-scan validation where Docker exists (same pattern as T01..T07).Refs #175