test: plant nested marker files in the layer-scan probe (E00-S02-T08)
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.
This commit is contained in:
@@ -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-<uuid> (root; `**/.env.*`)
|
||||
// - apps/server/.env.t08-<uuid> (nested; `**/.env.*`)
|
||||
// - apps/server/secrets/t08-<uuid>.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]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user