diff --git a/.gitignore b/.gitignore index 60d04f5..1150e19 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,12 @@ # SEC-004 records as PUBLIC. The trailing form also still covers the pre-rename name. # phase-01 step 1.0-GEN.d asserts `git check-ignore` on its own output before writing. overlays/*octavia-pki.yaml -octavia-pki/ +# ROOT-ANCHORED (fixed 2026-07-29). This was `octavia-pki/`, which git matches at ANY depth +# -- so it silently swallowed `tests/octavia-pki/`, and a commit adding that harness omitted +# it without error while `tests/HARNESS-MANIFEST` still named it. A fresh clone would then +# fail the gauntlet on a harness that was never pushed. The intent was only ever to stop a +# copy of the $HOME PKI workspace being committed at the repo root, which the anchor keeps. +/octavia-pki/ passphrase.txt # Bundle backups (timestamped) bundle.yaml.bak-* diff --git a/tests/octavia-pki/run-tests.sh b/tests/octavia-pki/run-tests.sh new file mode 100755 index 0000000..899395a --- /dev/null +++ b/tests/octavia-pki/run-tests.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# tests/octavia-pki/run-tests.sh -- harness for scripts/octavia-pki.sh verify +# +# WHAT THIS HARNESS IS FOR. The script asserts things the runbook only hoped for, so the +# assertions themselves must be proven capable of FAILING -- "a checker that cannot fail is +# not a gate". Every case below constructs a specific defect and requires the script to +# catch it. T1 is the non-zero floor: a CORRECT fixture must PASS, because a script that +# fails on everything is as useless as one that passes on everything. +# +# THE CASE THAT MATTERS MOST IS T2. F8 records that the runbook's heredoc paste hazard can +# yield a controller certificate with NO SANs while every command still prints OK, and that +# nothing in this repo asserted the SAN set. T2 is that assertion. +# +# T12 guards the opposite error: openssl prints IPv6 SANs EXPANDED AND UPPERCASE +# (`2602:F3E2:F02:11:0:0:0:57`) while the overlay carries the compressed form. A naive +# string compare would FAIL a CORRECT certificate. T12 proves we do not. +# +# Fixtures are thrown away. Nothing here touches the real ~/octavia-pki or the real repo: +# the script's OCTAVIA_PKI_HOME / OCTAVIA_PKI_REPO / OCTAVIA_PKI_THIS_HOST overrides exist +# for exactly this reason. + +set -uo pipefail +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO="$(cd -- "$SCRIPT_DIR/../.." && pwd)" +CHECK="$REPO/scripts/octavia-pki.sh" + +P=0; F=0 +ok() { P=$((P+1)); echo " [ok] $1"; } +no() { F=$((F+1)); echo "FAIL $1"; } + +TMPROOT="$(mktemp -d)" +trap 'rm -rf "$TMPROOT"' EXIT + +V4="10.12.4.57"; V6="2602:f3e2:f02:11::57" +KV4="10.12.4.50" + +# Build a throwaway fixture: a fake repo (git-initialised so check-ignore works) plus a +# per-DC PKI workspace. $1=env name $2=site $3=CA label $4=SAN mode +# SAN mode: full | none | v4only | wrongv4 +mkfix() { + local name="$1" site="$2" label="$3" sanmode="$4" + local E="$TMPROOT/$name"; local R="$E/repo"; local W="$E/home/octavia-pki/$site" + mkdir -p "$R/overlays" "$R/creds-manifests" "$W/issuing-ca" "$W/controller-ca" "$W/controller" + + printf 'headend fixturehost\n' > "$R/creds-manifests/host-identity" + printf 'overlays/*octavia-pki.yaml\n' > "$R/.gitignore" + git -C "$R" init -q 2>/dev/null; git -C "$R" add -A >/dev/null 2>&1 || true + + cat > "$R/overlays/${site}-vips.yaml" </dev/null 2>&1 + printf '0123456789012345678901234567890123456789012\n' > passphrase.txt + chmod 600 passphrase.txt issuing-ca.key.enc; chmod 644 issuing-ca.cert.pem ) + ( cd "$W/controller-ca" + openssl req -new -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-384 -sha384 -nodes \ + -days 3650 -subj "/CN=${label} Omega Cloud Octavia Controller CA/O=Neumatrix" \ + -keyout controller-ca.key.enc -out controller-ca.cert.pem >/dev/null 2>&1 + printf '0123456789012345678901234567890123456789012\n' > passphrase.txt + chmod 600 passphrase.txt controller-ca.key.enc; chmod 644 controller-ca.cert.pem ) + + ( cd "$W/controller" + openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out controller.key >/dev/null 2>&1 + chmod 600 controller.key + { printf '[req]\ndistinguished_name=dn\nreq_extensions=v3_req\nprompt=no\n\n[dn]\nCN=octavia-controller.example\nO=Neumatrix\n\n[v3_req]\nkeyUsage=critical,digitalSignature,keyEncipherment\nextendedKeyUsage=clientAuth,serverAuth\n' + case "$sanmode" in + none) : ;; # F8's failure mode: NO subjectAltName at all + *) printf 'subjectAltName=@alt_names\n\n[alt_names]\nDNS.1=octavia-controller.example\nDNS.2=octavia.example\n' ;; + esac + case "$sanmode" in + full) printf 'IP.1 = %s\nIP.2 = %s\n' "$V4" "$V6" ;; + v4only) printf 'IP.1 = %s\n' "$V4" ;; + wrongv4) printf 'IP.1 = %s\nIP.2 = %s\n' "10.99.99.99" "$V6" ;; + esac + } > controller.cnf + openssl req -new -sha256 -key controller.key -config controller.cnf -out controller.csr >/dev/null 2>&1 + openssl x509 -req -sha256 -in controller.csr -CA ../controller-ca/controller-ca.cert.pem \ + -CAkey ../controller-ca/controller-ca.key.enc -CAcreateserial -days 730 \ + -extfile controller.cnf -extensions v3_req -out controller.cert.pem >/dev/null 2>&1 + chmod 644 controller.cert.pem + cat controller.cert.pem controller.key > controller.bundle.pem; chmod 600 controller.bundle.pem ) + + # the deploy overlay: 5 keys, ASCII, 0600, and gitignored by the fixture .gitignore + { echo "applications:"; echo " octavia:"; echo " options:" + for k in issuing-cacert issuing-ca-private-key issuing-ca-key-passphrase controller-cacert controller-cert; do + echo " lb-mgmt-${k}: \"QUJD\"" + done; } > "$R/overlays/${site}-octavia-pki.yaml" + chmod 600 "$R/overlays/${site}-octavia-pki.yaml" + echo "$E" +} + +run() { # run [host] -> sets RC, OUT + local E="$1" site="$2" host="${3:-fixturehost}" + OUT="$(OCTAVIA_PKI_REPO="$E/repo" OCTAVIA_PKI_HOME="$E/home" OCTAVIA_PKI_THIS_HOST="$host" \ + bash "$CHECK" verify "$site" 2>&1)"; RC=$? +} + +echo "=== octavia-pki harness ===" + +# T1 -- NON-ZERO FLOOR. A correct fixture must PASS, or nothing below proves anything. +E=$(mkfix good vr1-dc0 "VR1 DC0" full); run "$E" vr1-dc0 +[ "$RC" = 0 ] && ok "T1 a correct PKI PASSES (non-zero floor)" \ + || { no "T1 baseline must pass (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T2 -- THE F8 CASE. No subjectAltName at all: the runbook's paste hazard output. +E=$(mkfix nosan vr1-dc0 "VR1 DC0" none); run "$E" vr1-dc0 +[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "NO subjectAltName" \ + && ok "T2 a cert with NO SANs FAILS -- the defect nothing previously asserted (F8)" \ + || { no "T2 must catch a SAN-less certificate (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T3 -- v6 SAN missing while the overlay declares a v6 provider leg (D-109 note 2026-07-29). +E=$(mkfix v4only vr1-dc0 "VR1 DC0" v4only); run "$E" vr1-dc0 +[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "MISSING this DC's provider v6" \ + && ok "T3 a v4-only SAN set FAILS when the overlay declares a v6 leg" \ + || { no "T3 must catch a missing v6 SAN (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T4 -- the SAN carries an address that is not this DC's VIP. +E=$(mkfix wrongip vr1-dc0 "VR1 DC0" wrongv4); run "$E" vr1-dc0 +[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "MISSING this DC's provider v4" \ + && ok "T4 a SAN naming the wrong v4 address FAILS" \ + || { no "T4 must catch a wrong v4 SAN (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T5 -- WRONG DC in the CA subject. This is the sibling of F8: the runbook guards ${DC:?} +# but NOT ${DC_LABEL}, so an unset label yields a DC-less CA with no error anywhere. +E=$(mkfix wronglabel vr1-dc0 "VR1 DC1" full); run "$E" vr1-dc0 +[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "A4 issuing CA subject does NOT contain" \ + && ok "T5 a CA subject naming the wrong DC FAILS" \ + || { no "T5 must catch a wrong-DC CA subject (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T6 -- E2: a group-writable CA certificate is a SUBSTITUTABLE TRUST ANCHOR. This is the +# finding the generating session dismissed as harmless, so it is pinned here. +E=$(mkfix gwcert vr1-dc0 "VR1 DC0" full) +chmod 664 "$E/home/octavia-pki/vr1-dc0/issuing-ca/issuing-ca.cert.pem"; run "$E" vr1-dc0 +[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "WRITABLE by group/other" \ + && ok "T6 a group-writable CA cert FAILS (E2)" \ + || { no "T6 must catch a group-writable CA cert (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T7 -- F4: the overlay carries CA key blobs plus a plaintext passphrase. If it is not +# gitignored, a CA private key is committable in a repo SEC-004 records as PUBLIC. +E=$(mkfix notignored vr1-dc0 "VR1 DC0" full) +: > "$E/repo/.gitignore"; run "$E" vr1-dc0 +[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "NOT gitignored" \ + && ok "T7 an un-gitignored overlay FAILS (F4)" \ + || { no "T7 must catch an un-gitignored overlay (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T8 -- a truncated overlay that still parses. 5 keys is the contract. +E=$(mkfix shortovl vr1-dc0 "VR1 DC0" full) +printf 'applications:\n octavia:\n options:\n lb-mgmt-issuing-cacert: "QUJD"\n' \ + > "$E/repo/overlays/vr1-dc0-octavia-pki.yaml" +chmod 600 "$E/repo/overlays/vr1-dc0-octavia-pki.yaml"; run "$E" vr1-dc0 +[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "lb-mgmt-\* key(s), expected 5" \ + && ok "T8 an overlay with the wrong key count FAILS" \ + || { no "T8 must catch a short overlay (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T9 -- WRONG HOST must REFUSE, never report absence. The F6 lesson: measuring the wrong +# machine's filesystem and calling the result a verdict is worse than declining to look. +E=$(mkfix wronghost vr1-dc0 "VR1 DC0" full); run "$E" vr1-dc0 someotherhost +[ "$RC" = 3 ] && printf '%s' "$OUT" | grep -q "PKI lives on" \ + && ok "T9 a run on the wrong host REFUSES (exit 3), not FAIL and not PASS" \ + || { no "T9 wrong host must refuse (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T10 -- an ABSENT host binding must REFUSE rather than assume this host is the right one. +E=$(mkfix nobinding vr1-dc0 "VR1 DC0" full) +: > "$E/repo/creds-manifests/host-identity"; run "$E" vr1-dc0 +[ "$RC" = 3 ] && printf '%s' "$OUT" | grep -q "no 'headend' binding" \ + && ok "T10 a missing headend binding REFUSES rather than guessing" \ + || { no "T10 missing binding must refuse (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T11 -- an absent workspace is a CONCLUSIVE observation, so FAIL (not REFUSE): we looked. +E=$(mkfix nowork vr1-dc0 "VR1 DC0" full) +rm -rf "$E/home/octavia-pki/vr1-dc0"; run "$E" vr1-dc0 +[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "has not been generated" \ + && ok "T11 an un-generated DC FAILS (looked, nothing there) rather than refusing" \ + || { no "T11 absent workspace must fail (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T12 -- R7's whole point: no cross-DC amphora root-of-trust. Before F1 the generator's +# paths were shared, so generating dc1 OVERWROTE dc0 and these were byte-identical. +E=$(mkfix indep vr1-dc0 "VR1 DC0" full) +mkdir -p "$E/home/octavia-pki/vr1-dc1" +cp -r "$E/home/octavia-pki/vr1-dc0/." "$E/home/octavia-pki/vr1-dc1/" +run "$E" vr1-dc0 +[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "IDENTICAL to vr1-dc1" \ + && ok "T12 shared material between DCs FAILS -- the state F1 existed to prevent" \ + || { no "T12 must catch cross-DC shared material (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T13 -- THE FALSE-FAIL GUARD. openssl prints v6 SANs expanded+uppercase while the overlay +# carries the compressed form. T1 already covers a correct cert, so this asserts the +# normalisation explicitly: the v6 assertion must be REACHED and PASS, not skipped. +E=$(mkfix v6norm vr1-dc0 "VR1 DC0" full); run "$E" vr1-dc0 +printf '%s' "$OUT" | grep -q "SAN carries this DC's provider v6 VIP" \ + && ok "T13 an expanded/uppercase v6 SAN is normalised and matches (no false FAIL)" \ + || { no "T13 v6 normalisation must match a correct cert"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T14 -- args. A bare `dcN` has been a live defect class; it must be REJECTED, not guessed. +RC=0; bash "$CHECK" verify dc0 >/dev/null 2>&1 || RC=$? +[ "$RC" = 2 ] && ok "T14 a bare 'dc0' is REJECTED (exit 2), never coerced" || no "T14 bare site must exit 2 (rc=$RC)" +RC=0; bash "$CHECK" generate vr1-dc0 >/dev/null 2>&1 || RC=$? +[ "$RC" = 2 ] && ok "T15 'generate' exits 2 -- unimplemented pending the D-137 fork-1 ruling" || no "T15 generate must exit 2 (rc=$RC)" + +echo +if [ "$F" = 0 ]; then echo "octavia-pki: $P/$P PASS"; exit 0; fi +echo "octavia-pki: FAILURES: $F (passed $P)"; exit 1