From 719fb4380be78899b9a5113197521aa2716fa236 Mon Sep 17 00:00:00 2001 From: implementer Date: Sat, 29 Aug 2026 01:53:57 +0000 Subject: [PATCH] test: plant nested marker files in the layer-scan probe (E00-S02-T08) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Docker-gated probe only planted a root-level .env.t08-* marker, which no Dockerfile COPY instruction ever copies — so it could not observe a nested build-context leak in the image layers. Plant additional marker files at nested paths the Dockerfile's COPY apps/server apps/server would sweep into the build-stage image (apps/server/.env.t08-*, apps/server/secrets/t08-*.pem) so the end-to-end scan actually verifies the 'any depth' exclusion guarantee, not just the root form. --- tests/secrets-not-embedded.test.mjs | 69 ++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/tests/secrets-not-embedded.test.mjs b/tests/secrets-not-embedded.test.mjs index bc816c5..990e9a4 100644 --- a/tests/secrets-not-embedded.test.mjs +++ b/tests/secrets-not-embedded.test.mjs @@ -21,12 +21,15 @@ * root-anchored bare form, or adding a redundant equivalent pattern all * break the criterion). * - "image layers contain no secret values" → on machines with Docker, the - * real-image probe builds the committed image from the repo root with a - * marker-bearing probe env file (`.env.t08-*`, excluded by `.dockerignore`) - * present in the build context, then `docker save`s the image, extracts + * real-image probe builds the committed image from the repo root with + * marker-bearing probe files planted in the build context at the root + * (`.env.t08-*`) AND at nested paths the Dockerfile's + * `COPY apps/server apps/server` would sweep into the build-stage image + * (`apps/server/.env.t08-*`, `apps/server/secrets/t08-*.pem`) unless + * `.dockerignore` excludes them, then `docker save`s the image, extracts * every layer (raw + decompressed) and the image config, and confirms - * neither the probe marker nor the compose default credential values - * appear anywhere in the layers. CI runs this file on every PR + * neither the markers nor the compose default credential values appear + * anywhere in the layers. CI runs this file on every PR * (.gitea/workflows/ci.yml), so the static assertions gate merges. * * The dockerignore matcher below is a faithful port of Docker's real matcher, @@ -601,18 +604,37 @@ test('the committed files copied into the image contain no default credential va }); test('the built image layers contain no secret values (docker build + layer scan)', { skip: !DOCKER_DAEMON }, () => { - // Build the committed image from the repo root with a marker-bearing probe - // env file in the build context (`.env.t08-*` matches the `.env.*` - // exclusion), then scan every layer blob (raw + decompressed) and the image - // config for the marker and the compose default credential values. - const marker = `T08_PROBE_${randomUUID().replace(/-/g, '')}`; - const probeName = `.env.t08-${randomUUID().slice(0, 8)}`; - const probePath = path.join(REPO_ROOT, probeName); + // Build the committed image from the repo root with marker-bearing probe + // files planted in the build context at the root AND at nested paths the + // Dockerfile's `COPY apps/server apps/server` would sweep into the + // build-stage image if `.dockerignore` did not exclude them: + // - .env.t08- (root; `**/.env.*`) + // - apps/server/.env.t08- (nested; `**/.env.*`) + // - apps/server/secrets/t08-.pem (nested; `**/secrets`, `**/*.pem`) + // then scan every layer blob (raw + decompressed) and the image config for + // the markers and the compose default credential values. The nested markers + // are the observable end-to-end check of the "any depth" guarantee: a leak + // at apps/server/… would land in the build-stage layers via the COPY. + const rootMarker = `T08_ROOT_SECRET_${randomUUID().replace(/-/g, '')}`; + const nestedEnvMarker = `T08_NESTED_ENV_SECRET_${randomUUID().replace(/-/g, '')}`; + const nestedPemMarker = `T08_NESTED_PEM_SECRET_${randomUUID().replace(/-/g, '')}`; + const rootProbeName = `.env.t08-${randomUUID().slice(0, 8)}`; + const nestedEnvProbeName = `.env.t08-${randomUUID().slice(0, 8)}`; + const nestedPemName = `t08-${randomUUID().slice(0, 8)}.pem`; const tag = `personal-blog:t08-probe-${randomUUID().slice(0, 8)}`; const tmp = mkdtempSync(path.join(os.tmpdir(), 't08-layer-scan-')); - writeFileSync(probePath, `T08_PROBE_SECRET=${marker}\nPOSTGRES_PASSWORD=${marker}\n`); + const rootProbePath = path.join(REPO_ROOT, rootProbeName); + const nestedEnvProbePath = path.join(REPO_ROOT, 'apps/server', nestedEnvProbeName); + const secretsDir = path.join(REPO_ROOT, 'apps/server/secrets'); + const nestedPemPath = path.join(secretsDir, nestedPemName); + const hadSecretsDir = existsSync(secretsDir); try { + writeFileSync(rootProbePath, `T08_PROBE_ROOT_SECRET=${rootMarker}\n`); + writeFileSync(nestedEnvProbePath, `T08_PROBE_NESTED_ENV_SECRET=${nestedEnvMarker}\n`); + mkdirSync(secretsDir, { recursive: true }); + writeFileSync(nestedPemPath, `T08_PROBE_NESTED_PEM_SECRET=${nestedPemMarker}\n`); + const build = run('docker', ['build', '--file', 'apps/server/Dockerfile', '--tag', tag, '.'], { cwd: REPO_ROOT, }); @@ -638,19 +660,32 @@ test('the built image layers contain no secret values (docker build + layer scan `"tar -xf" must exit 0:\n${(untar.stdout || '')}\n${(untar.stderr || '')}`.trim(), ); - const found = anyFileContains(extractDir, [marker, ...COMPOSE_CREDENTIAL_VALUES]); + const found = anyFileContains(extractDir, [rootMarker, nestedEnvMarker, nestedPemMarker, ...COMPOSE_CREDENTIAL_VALUES]); assert.deepEqual( found, [], `the image layers must contain no secret values; found in the image: ${found.join(', ')} ` + - `(scan of every layer + image config of "${tag}"; see \`docker history ${tag}\` for the layer list)`, + `(scan of every layer + image config of "${tag}"; probe files planted at the root (${rootProbeName}) ` + + `and at nested paths (apps/server/${nestedEnvProbeName}, apps/server/secrets/${nestedPemName}) — ` + + `see \`docker history ${tag}\` for the layer list)`, ); } finally { try { - unlinkSync(probePath); + unlinkSync(rootProbePath); } catch { - // probe env file already gone + // probe file already gone } + try { + unlinkSync(nestedEnvProbePath); + } catch { + // probe file already gone + } + try { + unlinkSync(nestedPemPath); + } catch { + // probe file already gone + } + if (!hadSecretsDir) rmSync(secretsDir, { recursive: true, force: true }); rmSync(tmp, { recursive: true, force: true }); run('docker', ['image', 'rm', '-f', tag]); }