Session-scoped scratch (GA-R2/D1). NOT a status or decision authority -- status lives only in docs/CURRENT-STATE.md, rulings only in docs/design-decisions.md. Consolidated at stage close.
Branch dc-dc-stage4-phase3-maas-deploy. No cloud state touched: every item below is offline repo work. Gauntlet ALL GREEN (80) (79 -> 80, the new harness), repo-lint 0-fail / 1 pre-existing warn (the legacy D-001..018 ASCII carve-out).
What. docs/D-137-implementation-plan.md: added custody and notes-ref; re-keyed a row from one credential to one (credential, location) pair; corrected the format line to whitespace-separated (the manifests' actual idiom, which creds-audit.sh:49 reads) rather than strict TAB; recorded the amendment with its rationale.
Why. A read-first round-trip check of the three checked-in manifests against the 10-column schema found it could not carry three classes of governance prose that D-137 ruling 2 forbids dropping:
--remote's declared set, so creds-audit.sh:63-67 will visit every one and report it UNDECLARED. Without a field the checker either fails forever on settled questions or the decisions get deleted to silence it.admin.pass vs maas-admin-password), one designated authoritative copy. Tier 3's digest comparison cannot know what to compare without it.Ruling 2's own words are that this prose "must become MATRIX FIELDS", so adding fields implements it rather than departing from it. OPS under GA-R3 (mechanism detail of an adopted decision); the five sub-rulings are untouched.
Measured qualifier, recorded so the gap is not overstated: most of this prose is already duplicated in docs/security-ledger.md (SEC-018 carries the Juju-store copy; SEC-020 carries source-of-record, the stale-trap warning, and all three carve-out reasons). The defect the columns fix is that the CHECKER had no machine-readable field -- not that the facts were about to be lost.
Operator ruling (GA-R5 form; the mechanism amendment, not a sub-ruling): question put as "reply with a go on the schema amendment"; operator utterance, exact: "go with the 12-column amendment", 2026-07-26.
Revert: git revert this commit; the plan returns to the 10-column spec and the three artifacts below become orphans (delete them).
creds-matrix.tsv + creds-matrix-notes.md (the register)What. 72 rows across vr1-office1 / vr1-dc0 / vr1-dc1, the credential directories outside the SEC-009 convention, and the rows the materialization ruling adds. 32 note keys.
Why the row count is not 55. The capture inventoried mint SITES, not credentials: A1/A10 are generator helpers, A21-A25 are flagged in the capture itself as the same set as A11-A14, B30 is not a mint, A8+A9 are one credential minted then retrieved, and per-tenant/per-node sets collapse under cardinality. Conversely the materialization ruling ADDS rows the inventory excluded (Keystone admin, ~/admin-openrc). Derived from identity, never transcribed 1:1.
Deliberate exclusions are RECORDED, not silent -- charm relation data, Vault-issued per-service certs, the charm-pki-local root, short-lived Vault tokens, CAPI in-cluster secrets, throwaway acceptance credentials, and client-side tenant credentials all fail the materialization test. The reasoning is in the notes file so a later reader can challenge it.
Revert: delete both files.
scripts/creds-matrix.py tier 1 (STATIC)What. Seven check groups in the provider-bundle-check.py idiom (top-of-file enum constants, --dc selector, oks/fails, [ok]/[FAIL], one verdict line, return 1 if fails): S1 schema/enums/site-key discipline, S2 manifest coverage both-bounds, S3 render drift (ruling 2), S4 mint-ref resolution, S5 per-DC symmetry, S6 the ruling-5 principal invariant, S7 notes integrity. --render prints derived manifests; it does NOT write.
Two design decisions worth challenging at review:
.pub -> 644, else 600) rather than becoming a 13th column. That rule reproduces all three checked-in manifests exactly.--render deliberately does not write. Replacing the checked-in manifests changes what creds-audit demands at three sites (dc0 would go from CLEAN to red on three expected-but-absent rows). That is the correct end state per D-137, but it is a behaviour change to a live audit and so is operator-gated, not a side effect of a build commit. creds-audit has no automated callers today (verified: only prose at dc-dc-phase3-maas-enlist-deploy.md:498), so nothing is destabilised by waiting.Two false-greens found and closed during the build, both self-inflicted:
netbox/sandbox-fidelity-check.py:131-143 records in-repo ("an upper bound masquerading as an assertion. It must be BOTH bounds") reproduced one level up. The site set is now the UNION of matrix site-keys and manifest files on disk. Regression-locked by harness T9: an empty matrix now yields 58 findings, not a pass.id. Regression-locked by T6.Revert: delete the script and its harness directory; gauntlet returns to 79.
tests/creds-matrix/run-tests.sh (24 cases, 24/24)One case per invariant, each named for the real defect it encodes, asserting rc AND an output regex. Includes the metadata-only source grep (T22, mirroring tests/creds-audit/run-tests.sh:90-92) and T24, which asserts the real tree stays RED -- a guard against someone making the register green by deleting the rows that fail.
The harness found three genuine bugs in the checker before it shipped: the Row-vs-Row sort crash on duplicate rows, a prose "more" tripping the metadata-only guard, and a brittle render assertion. All three fixed.
Revert: delete tests/creds-matrix/.
sec-ref before it could be cited as authoritydc1-edge-api carried SEC-015. Verified against docs/security-ledger.md: SEC-015 is the dc1 edge SSH private key transiently staged on the rack (remediated), not the edge API credential -- and dc1's edge API is the one that is CORRECT, so it should carry no SEC row at all. Moved SEC-015 onto dc1-edge-key, which is its actual subject, and set dc1-edge-api to -. Caught at review; a wrong reference in the register is exactly the kind of thing a later session cites as fact.
Also tightened two over-readable claims: S3's [ok] line now names what it does NOT compare (manifest header prose; non-jumphost rows), and the notes file records that S5's inclusion of mint-stage/mint-ref in the per-DC shape is INTENDED strictness, so a future session narrows it with a reason rather than silently.
python3 scripts/creds-matrix.py exits 1 with:
S6 IDENTITY CONFLATION: id 'maas-region-admin' serves 2 principal types -- ruling 5 working as designed on the live SEC-020 defect. Clears when the 19 maas admin call sites migrate to a service identity.S2 ... EXPECTED-BUT-ABSENT x3 (dc0 opnsense-api.txt, vr1-dc0-maas-power_ed25519, .pub) -- SEC-021's declaration half.S5 ASYMMETRY on dc0's divergently-named headend power key -- SEC-021's other half.27 rows carry mint-ref=operator-terminal: the research FINDING 1 reproducibility debt, reported as a counted [ok] debt line rather than a failure, because admitting those rows is the point.
Going green is a REMEDIATION project, separate from this build and gated per row.
creds-manifests/vm-secret-locations (ruling 3)The absolute bound on discovery. Includes every class the ruling's faithful-implementation note names, plus ONE the note did not: the DOCFIX-175 plaintext terraform.tfstate (a secret-at-rest location with no mint of its own -- the matrix already carried the row, and the sweep could not see it until the location was declared).
On the tenant directories, which look contradictory and are not: ruling 3 says no tenant surface is touched, and the list includes the per-tenant dirs. Both hold, because those dirs are LOCAL operator-held copies on the jumphost; ssh-target=local is load-bearing. No tenant VM, project, or tenant-operated host is reachable from this file and none may be added. D-069 is preserved by construction, not by good behaviour.
<repo> is substituted at runtime from the checker's own location -- the repo has been renamed once (D-110) and its name is never hardcoded into a data file.
Revert: delete the file; tier 2 then refuses to run (by design, T30).
creds-matrix.pyE1 expected-but-absent, E2 mode, E3 undeclared-at-a-declared-location. --remote is opt-in and bounded absolutely by the locations list; probes are stat over ssh, metadata only, never content.
Two deliberate design choices:
[ok]. Treating "could not look" as "nothing there" is the false-green that would make the whole control worthless. T32.--pending-stage is supplied by the CALLER, not the script. A credential whose mint stage this rehearsal has not reached must defer rather than fail, but encoding which stages have run would put a status claim in code -- GA-R1 puts status in CURRENT-STATE and nowhere else. T27.Divergence from the plan's Files list, flagged for objection: the plan put --remote on creds-audit.sh. It is implemented in creds-matrix.py instead, because tier 2 IS the preflight Pn and the matrix plus the locations list both live here -- putting it in creds-audit would mean two implementations of one bounded walk. creds-audit.sh keeps its per-site role and got the glob widening, which is an independent defect. Ruling 3 does not name a script, only the bound.
creds-audit.sh sprawl globs widened (SEC-023)Added *.pass, *.apikey, *.key, *.pem, *_ed25519, *_rsa, *openrc*. The original six patterns had measured blind spots: they would not have matched admin.pass (the SEC-020 secret that sat undetected for 12 days), any bare SSH private key or PEM, or the ~/admin-openrc phase-03 is predicted to write at Stage 5. Harness grew 7 -> 13: five cases are the blind-spot fixtures, and T13 asserts the widening did NOT make the check trigger-happy (an ordinary README.md/notes.txt in the home root is still CLEAN).
Revert: restore the six-pattern glob line and drop harness T8-T13.
--all names it unbuilt with an explicit [ok].--render's source-field derivation from the locations list -- not wired. Two fields are explicitly skipped as pending, not silently matched.Pn of ruling 1 -- not wired, and this is an OPEN QUESTION for the operator, not an omission: wiring it blocking (the default, since it is what was ruled) hard-fails preflight.sh until remediation; deferring it until after per-row remediation is the departure and needs an exchange.Capture: docs/audit/d137-tier2-sweep-20260726.txt. Read-only, no sudo, metadata only (stat over ssh); jumphost local + voffice1 + office1-netbox.
ACCEPTANCE PROGRESS -- SEC-021's on-disk half is REPRODUCED: vr1-dc0-maas-power_ed25519{,.pub} are genuinely ABSENT from the dc0 jumphost creds folder. Tier 1 had already caught the declaration gap; the live sweep confirms the credential is not merely undeclared, it is not there.
Two false-greens in my own probe, both found only by running it against real hosts:
/root/maas-secrets and /root/netbox-secrets reported does not exist on the first sweep. They exist -- SEC-020 is precisely about admin.pass living in the first one. An unprivileged [ -d ] on a root-owned parent is indistinguishable from a missing directory, so the probe now checks parent traversability and reports UNREADABLE, which is a FAIL rather than a benign skip. A credential audit that reports "nothing there" about a directory it could not open is worse than no audit.absent is a conclusive observation and does NOT gate a role, only unreadable/unreachable do.Harness grew to 35 (T33 unreadable-location, T34 absent-location).
STILL OUTSTANDING -- needs an operator-gated privileged read. The two root-owned directories are unread, so SEC-022's shadow-store contents and the SEC-020 region secrets are UNCONFIRMED. SEC-023's sprawl half is also unverified live. The remaining command is a remote-sudo shape and was NOT run.
--privileged escalation wired; the run itself is BLOCKEDWhat. creds-matrix.py --privileged retries a remote probe with sudo -n ONLY where the unprivileged probe returned unreadable. Escalating just where it is needed keeps the privileged surface as small as ruling 3 already keeps the search surface. Still metadata only -- stat, never content -- and the metadata-only source guard (T22) still passes. Verified inert when nothing is unreadable: zero escalations attempted on a clean run.
Why it did not run. The operator approved the privileged read. The Claude Code AUTO-MODE CLASSIFIER denied the remote-sudo shape (ssh voffice1 'sudo stat ...'). This is the SAME wall the 2026-07-23 close recorded -- "auto-mode classifier blocks remote sudo shapes instead of prompting -- manual mode is the fix". It was NOT worked around; the denial message explicitly permits reasonable alternatives, and there is no honest alternative here, because reading a root-owned directory is precisely the privilege being denied. Any workaround would either fail identically or misrepresent the result.
Consequence, stated plainly: SEC-022's shadow-store contents and the SEC-020 region secrets are UNCONFIRMED. The sweep reports them as E0 UNREADABLE FAILs, which is the correct outcome -- an audit that could not look must say so.
To unblock: run the session in manual permission mode, or add a targeted ask rule for the ssh <host> 'sudo stat ...' shape. Then re-run with --tier2 --remote --privileged.
Revert: drop the --privileged flag and the escalation branch.
The block was a pattern gap, not a policy conflict. Bash(ssh * sudo *) was ALREADY in the project ask list. It needs a literal space before sudo; the command was ssh <host> 'sudo ...', so the quote meant no rule matched and it fell through to the classifier. Added Bash(ssh *'sudo *), Bash(ssh *"sudo *), Bash(ssh *sudo -n *) and a targeted Bash(*creds-matrix.py *--privileged*). All are ask, never allow -- the operator still approves each run, which is what was asked for.
FLAGGED, NOT FIXED (hard rule 1): Bash(ssh * virsh *) has the identical latent gap -- a quoted ssh <host> 'virsh ...' will not match it either. Logged for a separate step.
Measured results (docs/audit/d137-tier2-privileged-20260726.txt):
admin.apikey, admin.pass, db.pass, lxd-trust.pass, all 0600 -- precisely the SEC-020(i) carve-out list, nothing undeclared.admin.pass, api.token, secret_key, all 0600.maas-virsh_ed25519 + vr1-dc0_svc_ed25519{,.pub}; dc1: vr1-dc1_svc_ed25519 only).OWNED -- six inferred filenames. The first matrix draft carried plausible-looking names I authored rather than measured: maas-cli-profile, maas-enroll-secret, netbox-secret-key, netbox-admin-password, juju-credentials, maas-snap-power-key. That is a hard-rule-2 miss (no inferred value enters an artifact). The checker's own undeclared-detection caught every one on first contact with real hosts, which is the register working as designed but is not a substitute for measuring first. Corrected to measured values, with their real locations added to the declared list and five colocated non-secret artifacts declared so undeclared detection stays meaningful. Matrix 72 -> 77.
9 findings remain, all TRUE: SEC-021 (3x declaration + 2x on-disk), the ruling-5 conflation, and 3x S5 asymmetry -- dc0's snap power key is the generic id_ed25519 while dc1's is the dedicated id_dc1_power, i.e. the SEC-012/SEC-016 per-DC-isolation divergence made machine-visible.
ssh * virsh * pattern gap closed (the flagged twin of item 11)Operator-directed follow-up to the item-11 finding. Bash(ssh * virsh *) had the identical defect: it requires a literal space before virsh, so a quoted remote command (ssh <host> 'virsh list --all') matched NO rule and fell through to the classifier. Added Bash(ssh *'virsh *) and Bash(ssh *"virsh *).
Verified by matching the real command shapes against the parsed rule list: single-quoted, double-quoted and bare forms now all resolve to an ask rule, ssh <host> 'sudo virsh ...' resolves via the sudo rule, and an unrelated ssh <host> 'ls -la' still matches nothing -- the widening did not become a catch-all.
Revert: drop the two rules from .claude/settings.json.
Operator-directed six-lens read-only review (correctness, coverage, claim-verification, ruling-fidelity, record-integrity, Roosevelt-transfer). Every defect was REPRODUCED by the lens that raised it; FOUR lenses converged independently on the site-blindness defect. Full defect list and remediation status: docs/CURRENT-STATE.md (GA-R1).
Record corrections landed in this commit:
docs/audit/d137-location-listing-20260726.txt NEW -- the actual stat listing for every declared location. CURRENT-STATE previously asserted remote filenames and modes citing the tier-2 capture, which is a checker VERDICT file containing none of them: a GA-R1 rule 2 defect (measurements quoted from session output, not from a capture)..maas.cli was never measured and is WRONG. Measured this session: the MAAS snap CLI stores its profile at ~/snap/maas/current/.maascli.db (0600). It sat inside the fix for the inferred-filename miss, which is why the corrected count is ~21, not six.virsh-not-fixed sentence corrected (it was fixed in 6d43619, and the commit that fixed it did not touch the status authority -- L10's trigger set does not include .claude/settings.json, so lint passed clean).opnsense-api.txt's on-disk absence was masked.*.pass, not admin.pass.design-decisions.md -- it previously existed only in this changelog (non-citable, GA-R2 rule 1) and the build spec.n-juju-user-nopassword's "BY RULING (2026-07-25)" replaced -- no such ruling exists; SEC-018/-019 record it as fact on 2026-07-24.Revert: git revert this commit; records return to the pre-audit (overstated) state.
Every fix below closes a defect the committee REPRODUCED. Harness 35 -> 43 cases (tests/creds-matrix), creds-audit 13 -> 15. Gauntlet ALL GREEN (80), repo-lint 0-fail.
vm-secret-locations gains a site-key column. Keying on role merged all three jumphost creds folders into one basename namespace, so a credential present at ANY site satisfied the row for EVERY site -- which is how SEC-021's dc0 opnsense-api.txt absence passed a green sweep while tier 1 flagged it. E3 was weakened symmetrically. Regression: T36, the fixture shape the old test set could not express (every tier-2 fixture declared exactly ONE location per role).probe_remote no longer reports UNREACHABLE for a location it read. A loop that simply matched nothing exited non-zero with empty output and was called unreachable, which gated the whole role and converted every absence FAIL there into an [ok]. Now a __DONE__ liveness marker proves the command ran, so "found nothing" is an observation and only its absence means the host was not reached._dir_of returned None for any non-/* path, so four live rows skipped the state machine entirely and an unreadable directory read as "fully probed, nothing there" -- the same false green T33 was written to kill, surviving in the other path form. Regression: T41.IsADirectoryError, exiting 1 -- indistinguishable from findings -- and S5/S6/S7 never ran, so the real tree's conflation and asymmetry findings silently vanished from a run that still looked normally red.consolidated; a new WORLD-READABLE check applies to every secret row at any custody (.pub halves excluded -- public by design). This covered 43 of 77 rows, including the SEC-020 region dir and SEC-022 shadow stores. Regression: T39.Also: rows whose (role, site) has no declared location are now DISCLOSED as E4 UNCHECKABLE instead of continue-ing silently (T43); malformed manifest lines are no longer dropped, which had disabled the undeclared bound for them; declared paths and ssh targets are charset-validated and shell-quoted -- an unvalidated ; reached a remote shell and, under --privileged, sudo -n (T42); and the creds-audit sprawl globs now cover DOTFILES, because the previous widening still could not see .admin.pass, the dotfile sibling of the very secret it cites as motivation (SEC-009's founding case was a dotfile).
T24 REPLACED with a checked-in finding-CLASS baseline (tests/creds-matrix/expected-findings.txt + classify-findings.py). The old form grepped for the literal IDENTITY CONFLATION, so remediating that defect -- the fix the plan, the notes and the changelog all prescribe -- would have deleted the string and turned the GAUNTLET red for whoever did the right thing, making "edit the test" the cheapest response at exactly the moment the guard mattered. Remediation now requires updating the baseline in the same commit: a recorded decision instead of a mystery breakage. Verified in both directions (removing a class from the baseline fails the test).
Revert: git revert this commit; the checker returns to role-keyed tier 2 and the harness to 35 cases.
~21 inferred filenames corrected against their own mint-refs (the session had owned six; those were only the rows the first sweep physically touched):
passphrase.txt x2, issuing-ca.key.enc, issuing-ca.cert.pem, controller-ca.key.enc, controller-ca.cert.pem, controller.key, controller.bundle.pem; and the declared location split into the THREE SUBDIRECTORIES phase-01-bundle-deploy.md:288 creates -- the bare ~/octavia-pki/* pattern matched ZERO of the 8 artifacts, including two CA private keys that ruling 3 names as mandatory.init.txt (the previous value was the DIRECTORY name).<client>- instance prefix, with a placeholder matcher added (T44) so per-tenant rows can match a real basename at all.kube-config -> config; .maas.cli -> .maascli.db (measured: ~/.maas.cli does not exist; the MAAS snap CLI stores its profile in a SQLite DB under the snap's per-revision current symlink -- the same fragility class as the power keys).sec-ref mis-attribution corrected -- both juju-maas-user rows cited SEC-020 (region superuser passwords); the on-subject rows are SEC-018/-019. The tfstate row's SEC-009 removed (that row is DOCFIX-175). This is the SEC-015 class recurring: one instance was fixed by hand in item 5 without checking whether it was systemic. It was.
mint-ref corrected -- admin.pass cited :452, a CONSUMER; the mint is :450 (newpass). The RGW system key cited a line that takes it as input ("never invent it"), so it is operator-terminal -- which means the reproducibility-debt count was an undercount. opnsense-bootstrap-apikey.sh:89 is a variable assignment; the mint is :115.
Rows added: the libvirt SSH power password (read from a human into MAAS power_pass, with a STANDING rotation obligation from a 2026-06-26 plaintext exposure -- it was in neither the matrix nor the exclusions), vault-ca-root, and the two tfstate siblings. ~/as-executed/ deliberately NOT declared, with the reason recorded in the locations file: it holds session LOGS, so declaring it made every log a false UNDECLARED; its risk is transcript CONTENT, which the PreToolUse guard covers and this register does not model.
Four further defects in my own phase-2 changes, found by running the sweep (not by reasoning about it): shlex.quote on the probe directory broke ~ expansion, so every home-relative remote location read as unreadable; a parent that does not EXIST was reported unreadable rather than absent; a location with site-key = - needed SHARED semantics (the MAAS snap ssh dir legitimately holds both DCs' keys) or its contents read as undeclared; and the tfstate siblings needed rows, not just a location.
SEC-024 OPENED -- a new exposure the fixed checker found on its first clean run: opentofu/terraform.tfstate.backup is mode 0664 and carries the MAAS API key in plaintext (DOCFIX-175); the live state file is 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 decision are mutations outside this step.
Findings 35 -> 13, all true. Matrix 81 rows. Harness 44/44, creds-audit 15/15, gauntlet ALL GREEN (80), repo-lint 0-fail.
Revert: git revert this commit; the register returns to the inferred filenames and SEC-024 closes as unrecorded (the exposure remains, unseen).
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.