diff --git a/scripts/octavia-pki.sh b/scripts/octavia-pki.sh index 4d08508..9c93767 100755 --- a/scripts/octavia-pki.sh +++ b/scripts/octavia-pki.sh @@ -283,6 +283,55 @@ fi fi +# ------------------------------------------------- A12 the DNS SANs, ARMED BY THE POSTURE +# F9. The controller cert's two DNS SANs are baked `dc0.vr0` literals on BOTH DCs. They are +# INERT today only because `os-public-hostname` is set NOWHERE -- bundle.yaml posture B5 is +# IP-ONLY ("the dual VIPs ARE the catalog endpoints"), and R5 refused setting it at Stage 5 +# because that repeats D-019. Amphorae reach the controller by ADDRESS on o-hm0's ULA and the +# controller verifies them by UUID, so nothing resolves these names. +# +# THE POINT OF THIS CHECK IS THAT NOBODY HAS TO REMEMBER F9. It ARMS ITSELF from the very +# change that makes the SANs load-bearing: the moment `os-public-hostname` appears in the +# merged deploy input (D-106's Stage-7 work), the assertion goes live. A scheduled fix depends +# on a future session reading a note; this depends on nothing. +# +# WHAT IT CAN AND CANNOT ASSERT, and the difference is a real gap in a RULED decision: +# D-008 fixes the shape `....cloud.neumatrix.local`, and D-106 +# instantiates VR1 as `...omega.dc1.vr1...` / `...dc2.vr1...` -- while the substrate's DCs are +# `vr1-dc0` and `vr1-dc1`. So `vr1-dc1` maps to `dc1.vr1` by TOKEN or `dc2.vr1` by POSITION, +# which is the DC1/DC2 ambiguity item 3.1 retired elsewhere, here deciding certificate identity. +# The REGION is unambiguous and is asserted. The DC label REFUSES pending a ruling rather than +# blessing a name that cannot be validated. +if [ -f "$CON_CERT" ]; then + REGION="${SITE%%-*}" # vr1-dc0 -> vr1 (derived, never typed) + # Is the posture armed? Look for a real option KEY in the merged input, not prose. + ARMED=0 + for f in "$REPO_ROOT/bundle.yaml" "$REPO_ROOT/overlays/${SITE}-vips.yaml" \ + "$REPO_ROOT/overlays/${SITE}-machines.yaml"; do + [ -f "$f" ] || continue + grep -qE '^[[:space:]]+os-public-hostname:[[:space:]]*[^[:space:]#]' "$f" && ARMED=1 + done + DNSN="$(printf '%s' "${SAN_RAW:-}" | grep -oE 'DNS:[^,[:space:]]+' | sed 's/^DNS://')" + if [ "$ARMED" -eq 0 ]; then + ok "A12 DNS SANs are INERT -- os-public-hostname is set in no deploy artifact (B5 IP-only), so nothing resolves them; this assertion ARMS ITSELF when D-106 sets it" + case "$DNSN" in + *".${REGION}."*) : ;; + *) ok "A12 (recorded, not failed) the DNS SANs do not carry this DC's region '$REGION' -- harmless while inert, and F9's reissue obligation" ;; + esac + else + # Armed: the names are now load-bearing. + BAD=0 + for n in $DNSN; do + case "$n" in + *".${REGION}."*) : ;; + *) nfail "A12 os-public-hostname IS SET, so DNS SANs are load-bearing -- '$n' does not carry this DC's region '$REGION' (F9); the certificate must be reissued"; BAD=1 ;; + esac + done + [ "$BAD" -eq 0 ] && ok "A12 DNS SANs carry this DC's region '$REGION'" + nrefuse "A12 the per-DC DC-LABEL cannot be validated: D-008's shape plus D-106's VR1 instantiation ('dc1.vr1'/'dc2.vr1') do not say whether substrate '$SITE' is 'dc0'/'dc1' by token or 'dc1'/'dc2' by position. That mapping needs a ruling before an FQDN cert can be blessed." + fi +fi + # ---------------------------------------------------------------- A10 the deploy overlay if [ ! -f "$OVERLAY" ]; then nfail "A10 deploy overlay absent at overlays/${SITE}-octavia-pki.yaml -- phase-01 hard-ABORTs without it" @@ -325,11 +374,18 @@ # ---------------------------------------------------------------- verdict: three outcomes echo +# PRECEDENCE: a CONFIRMED defect outranks an UNEVALUATED item. This ordering was originally +# the other way round, and T20 caught the consequence: once A12 arms, it always refuses on the +# unruled DC label, so a definite wrong-region SAN was being reported as "could not evaluate". +# That UNDERSTATES a known defect. Both counts are printed either way, so a refusal is never +# lost -- it just does not mask a failure. +if [ "$NFAIL" -ne 0 ]; then + printf 'octavia-pki verify (%s): FAIL -- %d assertion(s) failed, %d passed' "$SITE" "$NFAIL" "$NOK" + [ "$NREFUSE" -ne 0 ] && printf ' (and %s)' "1+ item(s) could NOT be evaluated -- see REFUSE above" + echo; exit 1 +fi if [ "$NREFUSE" -ne 0 ]; then echo "octavia-pki verify ($SITE): REFUSE -- could not evaluate; this is NOT a pass"; exit 3 fi -if [ "$NFAIL" -ne 0 ]; then - echo "octavia-pki verify ($SITE): FAIL -- $NFAIL assertion(s) failed, $NOK passed"; exit 1 -fi echo "octavia-pki verify ($SITE): PASS -- $NOK assertion(s), 0 failed" exit 0 diff --git a/tests/octavia-pki/run-tests.sh b/tests/octavia-pki/run-tests.sh index 4c95d6e..0cfe384 100755 --- a/tests/octavia-pki/run-tests.sh +++ b/tests/octavia-pki/run-tests.sh @@ -38,7 +38,7 @@ # 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 name="$1" site="$2" label="$3" sanmode="$4" dnsbase="${5:-example}" 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" @@ -76,7 +76,7 @@ { 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' ;; + *) printf 'subjectAltName=@alt_names\n\n[alt_names]\nDNS.1=octavia-controller.%s\nDNS.2=octavia.%s\n' "$dnsbase" "$dnsbase" ;; esac case "$sanmode" in full) printf 'IP.1 = %s\nIP.2 = %s\n' "$V4" "$V6" ;; @@ -235,6 +235,40 @@ && ok "T18 a group-writable CA serial FAILS -- duplicate-serial forcing (found by P5, not by this gate)" \ || { no "T18 must catch a group-writable serial (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } +# --- A12: the DNS SANs, and the arming that makes F9 unmissable ------------------------- +# F9's whole risk is that a future session forgets. So A12 arms itself from the change that +# makes the SANs matter (os-public-hostname appearing), rather than from a note. + +# T19 -- UNARMED is the live posture: os-public-hostname set nowhere, so the DNS names resolve +# to nothing and must NOT fail the run. Without this case, arming A12 would have broken every +# current verify -- the same trap T17 guards for the deleted intermediates. +E=$(mkfix dnsinert vr1-dc0 "VR1 DC0" full); run "$E" vr1-dc0 +[ "$RC" = 0 ] && printf '%s' "$OUT" | grep -q "A12 DNS SANs are INERT" \ + && ok "T19 with os-public-hostname unset, DNS SANs are INERT and the run still PASSES" \ + || { no "T19 unarmed A12 must not fail the run (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T20 -- ARMED with a wrong-region name is F9 exactly: the live certs say `dc0.vr0` on BOTH +# VR1 DCs, so once hostnames are live the name belongs to another deployment. +E=$(mkfix dnsarmed vr1-dc0 "VR1 DC0" full "omega.dc0.vr0.cloud.neumatrix.local") +printf ' os-public-hostname: keystone.omega.dc0.vr0.cloud.neumatrix.local\n' \ + >> "$E/repo/overlays/vr1-dc0-vips.yaml" +run "$E" vr1-dc0 +[ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q "does not carry this DC's region 'vr1'" \ + && ok "T20 ARMED + a wrong-region DNS SAN FAILS (F9, self-arming)" \ + || { no "T20 armed A12 must fail a wrong-region SAN (rc=$RC)"; printf '%s\n' "$OUT" | sed 's/^/ /'; } + +# T21 -- ARMED with the RIGHT region still REFUSES, because the per-DC label is genuinely +# unruled: D-008's shape plus D-106's `dc1.vr1`/`dc2.vr1` instantiation do not say whether +# substrate `vr1-dc1` is `dc1` by token or `dc2` by position. Refusing to bless a name it +# cannot validate is the correct outcome -- "could not look" is never "nothing there". +E=$(mkfix dnsregionok vr1-dc0 "VR1 DC0" full "omega.dc0.vr1.cloud.neumatrix.local") +printf ' os-public-hostname: keystone.omega.dc0.vr1.cloud.neumatrix.local\n' \ + >> "$E/repo/overlays/vr1-dc0-vips.yaml" +run "$E" vr1-dc0 +[ "$RC" = 3 ] && printf '%s' "$OUT" | grep -q "DC-LABEL cannot be validated" \ + && ok "T21 ARMED + right region REFUSES on the unruled DC label rather than blessing it" \ + || { no "T21 armed A12 must refuse the unruled label (rc=$RC)"; 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)"