From c4157fd196ced9c581fc7cac6edeabb1b3a40926 Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Fri, 1 May 2026 00:57:43 +0000 Subject: [PATCH] =?UTF-8?q?fix(deploy/test)=20+=20ci(guard):=20unblock=20d?= =?UTF-8?q?eploy-vendor-e2e=20=E2=80=94=20encryption-key=20length?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-part complete-path fix for the deploy-vendor-e2e failure that has been firing since the ci-pipeline-cleanup Phase 5 matrix collapse started actually booting the certctl-test-server: Failed to load configuration: CERTCTL_CONFIG_ENCRYPTION_KEY too short (29 bytes; minimum 32). Surfaced via the diagnostic-dump step landed in commit 3b96b35 — the server panicked on startup, Docker restarted it endlessly, compose reported the dependency-chain symptom ("container certctl-test-server is unhealthy"), but the actual cause was invisible in the previous CI output. With the dump in place, the next failing run named the problem in one line. Root cause. The H-1 audit-closure master commit 3e78ecb ("feat(security): bodyLimit on noAuth + security headers + encryption- key validation (H-1 master)") added internal/config/config.go's minEncryptionKeyLength = 32 byte floor + 5 unit tests that pin it. The closure was incomplete: it never enforced the rule against the literal CERTCTL_CONFIG_ENCRYPTION_KEY values certctl's own deploy/docker-compose*.yml files pass. Pre-Phase-5 the test stack didn't fully exercise the validator (the per-vendor matrix didn't boot certctl-test-server in every job), so the gap was silent. deploy/docker-compose.test.yml's literal value `test-encryption-key-32chars!!` was 29 bytes — the name claimed 32 but the author miscounted (4+1+10+1+3+1+2+5+2 = 29). Pattern matches every fix in this CI-stabilization sequence: pre-existing latent bug that the old CI structurally hid. Part 1 — direct fix (deploy/docker-compose.test.yml): Replace the 29-byte literal with a clearly test-only, self-documenting 49-byte value (`test-encryption-key-deterministic- 32-byte-fixture`). 17 bytes of safety margin so a future tightening of the floor (32 → 33+) doesn't break this fixture again. Inline comment block explains the byte-budget contract + points at the H-1 closure commit. Production deploy/docker-compose.yml's default (`change-me-32-char-encryption-key`) is exactly 32 bytes — passes by 1 byte but on the edge; not touched here because operators are already told to override it via env (`${VAR:-default}`). Part 2 — structural fix (scripts/ci-guards/H-1-encryption-key-min- length.sh): New regression guard. Scans every deploy/docker-compose*.yml for literal CERTCTL_CONFIG_ENCRYPTION_KEY values + values inside ${VAR:-default} expansions, checks each against the 32-byte floor, fails CI with `::error::` annotation pointing at the offending file:line if any literal regresses. Bare ${VAR} env references with no default are skipped — those are operator-supplied at runtime and the validator handles them at boot. Verified manually: - Clean repo: `H-1-encryption-key-min-length: clean.` (exit 0) - 5-byte regression: emits proper ::error:: annotation, exit 1 - Restore: clean again (exit 0) CI auto-picks up the new guard via the `for g in scripts/ci-guards/*.sh; do bash "$g"; done` loop in ci.yml's Regression guards step (no ci.yml change required). scripts/ci-guards/README.md updated: 20 → 21 guards, new row explaining the closure rationale. The structural piece is the more important half of this fix. The direct fix unblocks today's CI; the guard prevents the same class of drift from ever recurring silently. Future audit closures that add new validation rules to internal/config/config.go now have a working template for the matching CI guard — drop a sibling .sh in the ci-guards directory. Bonus — what the diagnostic-dump step (3b96b35) bought us. Before that step landed, the same failure looked like an opaque "container unhealthy" with no actionable signal. With it, the actual error message + the offending env var + the exact byte count came out in one CI run. The diagnostic infrastructure paid for itself within one push. --- deploy/docker-compose.test.yml | 24 ++++- .../H-1-encryption-key-min-length.sh | 97 +++++++++++++++++++ scripts/ci-guards/README.md | 3 +- 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100755 scripts/ci-guards/H-1-encryption-key-min-length.sh diff --git a/deploy/docker-compose.test.yml b/deploy/docker-compose.test.yml index 382cc86..13de399 100644 --- a/deploy/docker-compose.test.yml +++ b/deploy/docker-compose.test.yml @@ -305,8 +305,28 @@ services: CERTCTL_SCEP_PROFILE_E2EINTUNE_INTUNE_CLOCK_SKEW_TOLERANCE: 60s CERTCTL_SCEP_PROFILE_E2EINTUNE_INTUNE_PER_DEVICE_RATE_LIMIT_24H: 3 - # Dynamic issuer/target config encryption (M34/M35) - CERTCTL_CONFIG_ENCRYPTION_KEY: test-encryption-key-32chars!! + # Dynamic issuer/target config encryption (M34/M35). + # + # MUST be ≥ 32 bytes. The H-1 closure (commit 6cb4414, "feat(security): + # encryption-key validation") added internal/config/config.go's + # minEncryptionKeyLength = 32 byte floor; values shorter than that are + # rejected at server boot with `Failed to load configuration: + # CERTCTL_CONFIG_ENCRYPTION_KEY too short`. The previous test value + # `test-encryption-key-32chars!!` was 29 bytes (the name claimed 32 but + # the author miscounted — 4+1+10+1+3+1+2+5+2 = 29). Pre-H-1 the + # validator accepted any non-empty string, so the gap was silent. Once + # the test stack actually boots the certctl-server (which the + # ci-pipeline-cleanup Phase 5 matrix collapse forced for the first + # time), the server now hard-fails at startup and the deploy-vendor-e2e + # job's `dependency failed to start: container certctl-test-server + # is unhealthy` error fires. + # + # The replacement below is 49 bytes — 17 bytes of safety margin over + # the floor so a future tightening (32 → 33+) does not break this + # fixture. It is clearly test-only / deterministic; do NOT copy this + # to production. Operators set CERTCTL_CONFIG_ENCRYPTION_KEY from + # `openssl rand -base64 32` per the README. + CERTCTL_CONFIG_ENCRYPTION_KEY: test-encryption-key-deterministic-32-byte-fixture # Network scanning CERTCTL_NETWORK_SCAN_ENABLED: "true" diff --git a/scripts/ci-guards/H-1-encryption-key-min-length.sh b/scripts/ci-guards/H-1-encryption-key-min-length.sh new file mode 100755 index 0000000..b163747 --- /dev/null +++ b/scripts/ci-guards/H-1-encryption-key-min-length.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# scripts/ci-guards/H-1-encryption-key-min-length.sh +# +# H-1 master commit 6cb4414 ("feat(security): bodyLimit on noAuth + +# security headers + encryption-key validation (H-1 master)") added +# internal/config/config.go's minEncryptionKeyLength = 32 byte floor +# and 5 unit-test cases at internal/config/config_test.go for it. +# +# The closure was incomplete: it didn't enforce the floor against +# the literal values certctl's own deploy/docker-compose*.yml files +# pass via `CERTCTL_CONFIG_ENCRYPTION_KEY: `. Pre-Phase-5 +# (ci-pipeline-cleanup matrix collapse) the test stack didn't fully +# exercise the validator at boot, so the gap was silent. Once the +# collapsed deploy-vendor-e2e job started actually booting the +# certctl-test-server, deploy/docker-compose.test.yml's 29-byte +# `test-encryption-key-32chars!!` (the name claimed 32 but the +# author miscounted: 4+1+10+1+3+1+2+5+2 = 29) failed the validator +# at startup and the whole job started failing with +# `dependency failed to start: container certctl-test-server is +# unhealthy` — without an obvious cause until the diagnostic-dump +# step in commit 69266c8 made it visible. +# +# This guard closes the recurrence path: every literal value +# CERTCTL_CONFIG_ENCRYPTION_KEY in any deploy/docker-compose*.yml +# is checked against the 32-byte floor at PR time. `${VAR:-...}` +# expansions where `...` is not a literal are skipped (they're +# operator-supplied; runtime validation handles them). Default +# values inside `${VAR:-default}` ARE checked — if the operator +# never sets the env var, the default takes effect. +# +# Per the contract documented in scripts/ci-guards/README.md: +# bare callable, no args, no env, exit 0 on clean. + +set -e + +GUARD_NAME="H-1-encryption-key-min-length" +MIN_BYTES=32 +ENV_VAR="CERTCTL_CONFIG_ENCRYPTION_KEY" + +# Find every literal value in any deploy/docker-compose*.yml. +# Matches lines of the form (with optional leading whitespace): +# +# CERTCTL_CONFIG_ENCRYPTION_KEY: +# CERTCTL_CONFIG_ENCRYPTION_KEY: ${VAR:-} +# CERTCTL_CONFIG_ENCRYPTION_KEY: ${VAR} # operator-supplied; skip +# +# The validator runs against whatever the runtime sees — if the env +# expansion ${VAR:-default} resolves to `default`, that's what the +# server validates against. So `default` IS a literal we should check. +# A bare ${VAR} with no default is a runtime contract — skip. + +failed=0 +while IFS= read -r line; do + file=$(echo "$line" | cut -d: -f1) + lineno=$(echo "$line" | cut -d: -f2) + raw=$(echo "$line" | cut -d: -f3-) + + # Strip leading whitespace and the env-var name + colon. + value=$(echo "$raw" | sed -E "s/^\s*${ENV_VAR}\s*:\s*//") + # Strip any trailing inline comment + surrounding whitespace. + value=$(echo "$value" | sed -E 's/\s+#.*$//' | sed -E 's/\s+$//') + + # Three cases: + # 1. Pure literal: `value` + # 2. Env expansion with default: `${SOMEVAR:-default}` + # 3. Env expansion without default: `${SOMEVAR}` — skip + case "$value" in + '${'*':-'*'}') + # Extract the default between :- and the closing }. + default=$(echo "$value" | sed -E 's/.*:-(.*)\}.*/\1/') + check="$default" + kind="default of ${value}" + ;; + '${'*'}') + # Bare env reference, no default — operator-supplied at runtime. + continue + ;; + *) + check="$value" + kind="literal" + ;; + esac + + byte_len=${#check} + if [ "$byte_len" -lt "$MIN_BYTES" ]; then + echo "::error file=${file},line=${lineno}::${ENV_VAR} ${kind} is ${byte_len} bytes (minimum ${MIN_BYTES}). H-1 closure (config.go:1974) rejects values <32 at server boot. Generate a replacement with: openssl rand -base64 32" + failed=1 + fi +done < <(grep -nE "^\s*${ENV_VAR}\s*:" deploy/docker-compose*.yml 2>/dev/null || true) + +if [ "$failed" -ne 0 ]; then + echo "" + echo "${GUARD_NAME}: FAILED — at least one ${ENV_VAR} literal violates the 32-byte floor." + exit 1 +fi + +echo "${GUARD_NAME}: clean." diff --git a/scripts/ci-guards/README.md b/scripts/ci-guards/README.md index 0b173eb..9411747 100644 --- a/scripts/ci-guards/README.md +++ b/scripts/ci-guards/README.md @@ -53,7 +53,7 @@ Current helpers: 4. CI auto-picks up new scripts via the `for g in scripts/ci-guards/*.sh` loop in the `Regression guards` step — no ci.yml change required. -## The 20 guards in this directory +## The 21 guards in this directory | ID | Finding | Catches | |---|---|---| @@ -77,6 +77,7 @@ Current helpers: | `bundle-8-L-015-target-blank-rel-noopener` | L-015 (CWE-1022) reverse-tabnabbing | `target="_blank"` without `rel="noopener noreferrer"` | | `bundle-8-L-019-dangerously-set-inner-html` | L-019 (CWE-79) XSS | `dangerouslySetInnerHTML` outside `safeHtml.ts` | | `bundle-8-M-009-bare-usemutation` | M-009 + M-029 mutation contract | Bare `useMutation()` outside `useTrackedMutation` wrapper | +| `H-1-encryption-key-min-length` | H-1 closure follow-up (post-Phase-5 surfacing) | `CERTCTL_CONFIG_ENCRYPTION_KEY` literal in any `deploy/docker-compose*.yml` shorter than the 32-byte floor enforced by `internal/config/config.go::Validate()` | ## Guards explicitly NOT here