mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:01:30 +00:00
fix(deploy/test) + ci(guard): unblock deploy-vendor-e2e — encryption-key length
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.
This commit is contained in:
@@ -305,8 +305,28 @@ services:
|
|||||||
CERTCTL_SCEP_PROFILE_E2EINTUNE_INTUNE_CLOCK_SKEW_TOLERANCE: 60s
|
CERTCTL_SCEP_PROFILE_E2EINTUNE_INTUNE_CLOCK_SKEW_TOLERANCE: 60s
|
||||||
CERTCTL_SCEP_PROFILE_E2EINTUNE_INTUNE_PER_DEVICE_RATE_LIMIT_24H: 3
|
CERTCTL_SCEP_PROFILE_E2EINTUNE_INTUNE_PER_DEVICE_RATE_LIMIT_24H: 3
|
||||||
|
|
||||||
# Dynamic issuer/target config encryption (M34/M35)
|
# Dynamic issuer/target config encryption (M34/M35).
|
||||||
CERTCTL_CONFIG_ENCRYPTION_KEY: test-encryption-key-32chars!!
|
#
|
||||||
|
# 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
|
# Network scanning
|
||||||
CERTCTL_NETWORK_SCAN_ENABLED: "true"
|
CERTCTL_NETWORK_SCAN_ENABLED: "true"
|
||||||
|
|||||||
+97
@@ -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: <literal>`. 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: <value>
|
||||||
|
# CERTCTL_CONFIG_ENCRYPTION_KEY: ${VAR:-<default>}
|
||||||
|
# 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."
|
||||||
@@ -53,7 +53,7 @@ Current helpers:
|
|||||||
4. CI auto-picks up new scripts via the `for g in scripts/ci-guards/*.sh`
|
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.
|
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 |
|
| 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-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-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 |
|
| `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
|
## Guards explicitly NOT here
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user