diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index b04e734..38a6410 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -840,7 +840,20 @@ operator:** ruling 1 lands the check as a BLOCKING preflight `Pn` and the first run is red, so wiring it hard-fails `preflight.sh` until the credential defects are remediated; wiring-as-ruled is the default and deferring until after per-row remediation is the - departure. Not decided; nothing wired either way. + departure. **RESOLVED + EXECUTED 2026-07-26.** Question as presented: wire the blocking + `Pn` as ruled, or defer it until after per-row remediation. Operator answer, exact + utterance: "wire tier 1 as the blocking preflight Pn". TIER 1 IS NOW WIRED as + `preflight.sh` **P5**, blocking, ahead of the stage-2 reminders block so its verdict + participates in the deploy decision; it FAILS CLOSED if the checker is absent (a missing + file made `python3` exit 2, which `note` maps to WARN -- so deleting the gate would have + downgraded it to a warning; harness T9 encodes this). Tier 2's `Pn` is NOT wired and + remains a separate decision: it needs `--remote`/`--privileged` and a caller-supplied + `--pending-stage`, none of which belong in an unattended gate. + **CONSEQUENCE, STATED PRECISELY: `preflight.sh` exits 1 and P5 is one of the reasons -- + but preflight was ALREADY exiting 1 before this change** (P4: `overlays/octavia-pki.yaml` + absent, and MAAS unreachable from the jumphost). P5 adds a fifth reason to an + already-red gate; it did NOT flip preflight from pass to fail, and no deploy path that + was open is closed by it. **TIER 2 TOOLING BUILT 2026-07-26, NOT YET RUN LIVE.** Shipped: `creds-manifests/vm-secret-locations` (ruling 3's absolute bound -- jumphost creds folders, the headend shadow stores of SEC-022, the region secrets dir of SEC-020, the @@ -955,8 +968,8 @@ correctly 0600. Invisible to every prior control because the world-readable check was custody-gated and the siblings were undeclared. **LOGGED, NOT ACTIONED (hard rule 1)** -- the `chmod` and the retain-or-delete question are mutations outside this step. - **STILL OUTSTANDING (not done, not silently dropped):** ruling 1's preflight `Pn` (the - sequencing question is still the operator's); ruling 4's SEC-009 demotion; tier 3 + **STILL OUTSTANDING (not done, not silently dropped):** ruling 1's TIER 2 `Pn` (tier 1 + is wired -- see above); ruling 4's SEC-009 demotion; tier 3 (VALIDITY); `--render`'s source-field derivation; and the `cardinality` field remains largely inert with a one-token S5 bypass (`per-DC` -> `singleton`). diff --git a/docs/changelog-20260726-d137-tier1.md b/docs/changelog-20260726-d137-tier1.md index d7f48d5..135a525 100644 --- a/docs/changelog-20260726-d137-tier1.md +++ b/docs/changelog-20260726-d137-tier1.md @@ -470,3 +470,39 @@ **Revert:** `git revert` this commit; the register returns to the inferred filenames and SEC-024 closes as unrecorded (the exposure remains, unseen). + +## Item 16 -- D-137 ruling 1 EXECUTED: tier 1 wired as the blocking preflight P5 + +Operator-directed, resolving the open sequencing question. Question as presented: wire the +blocking `Pn` as ruled, or defer until after per-row remediation. Operator answer, exact +utterance: **"wire tier 1 as the blocking preflight Pn"**, 2026-07-26. Recorded on D-137 in +`design-decisions.md`; status in CURRENT-STATE. + +**What landed.** `scripts/preflight.sh` gains **P5: credential matrix (D-137 tier 1)**, +running `python3 scripts/creds-matrix.py` with `note $?` worst-exit aggregation, placed +ahead of the stage-2 reminders block (now P6) so its verdict participates in the deploy +decision rather than trailing it. Tier 1 only: it is offline and its findings ARE the three +ruled failure classes. Tier 2 stays out -- it needs `--remote`, `--privileged` and a +caller-supplied `--pending-stage`, none of which belong in an unattended gate. + +**A fail-open in my own wiring, caught by the new test.** A missing `creds-matrix.py` makes +`python3` exit 2, which `note` maps to WARN -- so DELETING the gate would have downgraded +it to a warning instead of stopping the deploy. P4 already guarded this class; P5 did not +until T9 failed. Now guarded explicitly. + +**Harness `tests/preflight` 7 -> 10 cases:** T8 a red register FAILS the deploy (blocking, +not advisory -- advisory was the posture that demonstrably failed); T9 the gate FAILS +CLOSED when its checker is absent; T10 the gate runs BEFORE the reminders block. `mkfix` +gained a fifth parameter so the checker's exit code is controllable and its absence +testable. + +**CONSEQUENCE, stated precisely so it is not overclaimed:** `preflight.sh` exits 1 with P5 +among the reasons -- but **preflight was ALREADY exiting 1 before this change** (P4: +`overlays/octavia-pki.yaml` absent, MAAS unreachable from the jumphost). P5 adds a fifth +reason to an already-red gate. It did NOT flip preflight from pass to fail, and it closes +no deploy path that was open. Verified by stashing the change and re-running. + +Gauntlet ALL GREEN (80), repo-lint 0-fail. + +**Revert:** drop the P5 block from `scripts/preflight.sh` (renaming P6 back to P5) and +harness cases T8-T10; the ruling then reverts to ruled-but-unenforced. diff --git a/docs/design-decisions.md b/docs/design-decisions.md index 812a0ea..7b13317 100644 --- a/docs/design-decisions.md +++ b/docs/design-decisions.md @@ -5227,3 +5227,16 @@ the full defect list, and remediation progress live in `docs/CURRENT-STATE.md` (GA-R1); this note exists so a reader of D-137 is not left believing the adopted design shipped sound. The DESIGN is unchanged and was not faulted; the defects are implementation. + +**SUB-RULING 1 ENFORCEMENT EXECUTED 2026-07-26 (tier 1).** The adopted ruling is +"blocking in preflight"; the open question was whether to wire it while the register is +red by design, or defer until after per-row remediation. Question as presented: wire the +blocking `Pn` as ruled, or defer. Operator answer, exact utterance: "wire tier 1 as the +blocking preflight Pn". Tier 1 (STATIC, offline) now runs as `scripts/preflight.sh` P5, +blocking, ahead of the stage-2 reminders block, and FAILS CLOSED if the checker is absent. +Tier 1 was the half that needed no further exchange because its findings ARE the three +ruled failure classes (expected-but-absent / undeclared / per-DC-asymmetric). Tier 2's +`Pn` is deliberately NOT wired: it requires `--remote`, `--privileged` and a +caller-supplied `--pending-stage`, which do not belong in an unattended gate -- that +remains an open operator decision. Status and the live consequence live in +`docs/CURRENT-STATE.md` (GA-R1). diff --git a/scripts/preflight.sh b/scripts/preflight.sh index c6c2a9c..56b39e0 100644 --- a/scripts/preflight.sh +++ b/scripts/preflight.sh @@ -13,7 +13,8 @@ # P2 bundle invariants scripts/provider-bundle-check.py # P3 channel assert scripts/channel_assert.py (needs charmhub; WARN if offline) # P4 live pre-flight scripts/pre-flight-checks.sh (needs MAAS; overlay/VIP/planes/nodes) -# P5 reminders stage-2 gates: juju-spaces-check.sh (AFTER add-model), +# P5 credential matrix scripts/creds-matrix.py (D-137 ruling 1; tier 1 STATIC) +# P6 reminders stage-2 gates: juju-spaces-check.sh (AFTER add-model), # osd-blank-check.sh (sudo), 'juju deploy --dry-run' (phase-01 1.2) # # Exit: 0 all pass | 1 any FAIL (do NOT deploy) | 2 warnings only (review, then decide). @@ -40,7 +41,31 @@ echo " [FAIL] scripts/pre-flight-checks.sh missing"; note 1 fi -echo "================ P5: stage-2 reminders (NOT run here) ================" +echo "================ P5: credential matrix (D-137 tier 1) ================" +# D-137 ruling 1: enforcement is BLOCKING IN PREFLIGHT. Advisory was rejected as the +# posture that demonstrably failed -- `creds-audit` read CLEAN on 2026-07-15 while four +# region-VM secrets sat undeclared, and `admin.pass` surfaced 12 days later only because +# someone looked. Tier 1 is wired here because it is OFFLINE and its findings ARE the three +# ruled failure classes (expected-but-absent / undeclared / per-DC-asymmetric). Tier 2 +# needs --remote/--privileged and a caller-supplied --pending-stage, so it stays out of an +# unattended gate; wiring it is a separate decision. +# +# EXPECTED TO FAIL TODAY, BY DESIGN. The register is red on real defects (SEC-021's +# declaration gaps, the SEC-020 ruling-5 identity conflation, the per-DC power-key +# asymmetry). That is this gate working, not misconfigured: a deploy should not proceed +# over an unreconciled credential estate. Going green is remediation, gated per row -- +# do NOT silence it by deleting matrix rows or by demoting this to a warning. +# FAIL CLOSED if the checker is absent, mirroring P4. Without this guard a missing file +# makes python3 exit 2, which `note` maps to WARN -- so DELETING the gate would downgrade +# it to a warning instead of stopping the deploy. A gate that vanishes must never read as +# a pass (harness T9 encodes this). +if [ -f scripts/creds-matrix.py ]; then + python3 scripts/creds-matrix.py; note $? +else + echo " [FAIL] scripts/creds-matrix.py missing"; note 1 +fi + +echo "================ P6: stage-2 reminders (NOT run here) ================" echo " - after 'juju add-model': bash scripts/juju-spaces-check.sh" echo " - with sudo: bash scripts/osd-blank-check.sh" echo " - phase-01 Step 1.2: juju deploy --dry-run (plan: 50 apps / 97 relations)" diff --git a/tests/preflight/run-tests.sh b/tests/preflight/run-tests.sh index dd2e240..2bcec9d 100644 --- a/tests/preflight/run-tests.sh +++ b/tests/preflight/run-tests.sh @@ -10,7 +10,7 @@ TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT PASS=0; FAIL=0 -mkfix() { # mkfix +mkfix() { # mkfix [creds_rc|"omit"] local d="$TMP/$1"; mkdir -p "$d/scripts" "$d/fakebin" cp "$REPO/scripts/preflight.sh" "$REPO/scripts/channel_assert.py" "$d/scripts/" printf 'applications:\n keystone: {charm: keystone, channel: 2024.1/stable}\n' > "$d/bundle.yaml" @@ -19,6 +19,12 @@ if [ "$4" = "1" ]; then printf '#!/usr/bin/env bash\necho " [stub] live preflight"\nexit 0\n' > "$d/scripts/pre-flight-checks.sh" fi + # P5 credential matrix (D-137 ruling 1). "omit" leaves the gate script absent so the + # fail-closed path can be exercised. + if [ "${5:-0}" != "omit" ]; then + printf '#!/usr/bin/env python3\nimport sys;print(" [stub] creds-matrix");sys.exit(%s)\n' \ + "${5:-0}" > "$d/scripts/creds-matrix.py" + fi # fakebin juju: publishes 2024.1/stable in the channel map cat > "$d/fakebin/juju" <<'FB' #!/usr/bin/env bash @@ -60,5 +66,22 @@ chmod +x "$D/fakebin/juju" run 2 'verify .* manually' "T7 charmhub unreachable -> WARN" "$D" +# T8 -- D-137 ruling 1: the credential gate is BLOCKING. A red register must stop a +# deploy, not warn about it -- advisory was the posture that demonstrably failed. +D=$(mkfix creds_fail 0 0 1 1); run 1 'PREFLIGHT: FAIL' "T8 credential matrix FAIL -> overall FAIL" "$D" +# T9 -- the credential gate must FAIL CLOSED if its checker is missing, exactly as T5 +# requires of pre-flight-checks.sh. A gate that vanishes must never read as a pass. +D=$(mkfix creds_gone 0 0 1 omit); run 1 'PREFLIGHT: FAIL' "T9 credential gate missing -> FAILS CLOSED" "$D" +# T10 -- ordering: the credential gate runs with the REAL gates, before the stage-2 +# reminders block, so its verdict participates in the deploy decision. +D=$(mkfix creds_order 0 0 1 0) +out="$(PATH="$D/fakebin:$PATH" bash "$D/scripts/preflight.sh" "$D" 2>&1)" +if [[ "$(grep -n 'P5: credential matrix' <<<"$out" | cut -d: -f1)" -lt \ + "$(grep -n 'P6: stage-2 reminders' <<<"$out" | cut -d: -f1)" ]]; then + echo " PASS T10 credential gate runs BEFORE the reminders block"; PASS=$((PASS+1)) +else + echo " FAIL T10 credential gate ordering"; FAIL=$((FAIL+1)) +fi + echo; echo "RESULT: PASS=$PASS FAIL=$FAIL" [[ "$FAIL" -eq 0 ]] && { echo "ALL PASS"; exit 0; } || exit 1