# Session changelog 2026-07-26 -- D-137 credential matrix, tier 1 (STATIC)

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).

---

## Item 1 -- D-137 matrix schema amended 10 -> 12 columns (operator-ruled)

**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:
- **off-manifest carve-outs** -- four deliberate, reasoned credential copies (both DCs'
  Juju client-store keys, the dc1 MAAS-snap power key, and the three ruled
  not-consolidated region files). Ruling 3 puts their locations INSIDE `--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.
- **multi-copy credentials** -- one credential, several locations, DIFFERENT filenames
  (`admin.pass` vs `maas-admin-password`), one designated authoritative copy. Tier 3's
  digest comparison cannot know what to compare without it.
- **verification-relevant detail** -- format specs, mint dates, the not-reproducible debt.

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).

## Item 2 -- `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.

## Item 3 -- `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:**
- The manifest **mode column is DERIVED** (`.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:**
1. **S2 enumerated only the sites the MATRIX mentions**, so an empty or site-truncated
   matrix examined no manifest, found nothing undeclared, and reported "both bounds clean".
   That is precisely the defect `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.
2. The uniqueness key was (site, host-role, filename), which false-failed two DISTINCT
   identities sharing one file -- the Vault init artifact holds both the unseal shares and
   the root token. Key now includes `id`. Regression-locked by T6.

**Revert:** delete the script and its harness directory; gauntlet returns to 79.

## Item 4 -- `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/`.

## Item 5 -- corrected a wrong `sec-ref` before it could be cited as authority

`dc1-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.

---

## EXPECTED RED -- the 5 findings are the deliverable, not a defect in it

`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.

---

# TIER 2 (EXISTENCE) -- tooling built, live sweep NOT run

## Item 6 -- `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).

## Item 7 -- tier-2 EXISTENCE checks in `creds-matrix.py`

E1 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:**
- **An unreachable host SKIPS with an explicit `[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.

## Item 8 -- `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.

---

## NOT done in this session (stated so the boundary is unambiguous)

- **The LIVE tier-2 sweep has NOT run.** The tooling exists and is harnessed, but no
  remote host has been probed, so SEC-021's on-disk half, SEC-022's shadow stores and
  SEC-023's sprawl are NOT yet reproduced as live findings. The plan's acceptance test is
  therefore still only partly met. Next gated step.
- Tier 3 (VALIDITY) -- not built. `--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.
- The preflight `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.
- SEC-009's demotion to a pointer (ruling 4) -- not done; it belongs with tier 2.
- No manifest was rewritten; no cloud state was touched.

---

## Item 9 -- LIVE tier-2 sweep run (read-only) + two probe false-greens it exposed

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:**

1. **"Absent" vs "could not look."** `/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.
2. **Role-level aggregation manufactured false absences.** Skipping ONE location for a
   role still let that role's other locations "cover" it, so every credential living in
   the unprobed location reported EXPECTED-BUT-ABSENT. Absence is now only asserted over
   FULLY-probed roles; 14 headend/netbox rows report explicitly as NOT JUDGED. A third
   refinement followed: `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.

## Item 10 -- `--privileged` escalation wired; the run itself is BLOCKED

**What.** `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.

## Item 11 -- permission-rule repair; privileged sweep COMPLETED

**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`):
- Region secrets dir holds exactly `admin.apikey`, `admin.pass`, `db.pass`,
  `lxd-trust.pass`, all 0600 -- precisely the SEC-020(i) carve-out list, nothing undeclared.
- Netbox dir: `admin.pass`, `api.token`, `secret_key`, all 0600.
- SEC-022 shadow stores confirmed and now audited (dc0: `maas-virsh_ed25519` +
  `vr1-dc0_svc_ed25519{,.pub}`; dc1: `vr1-dc1_svc_ed25519` only).
- **ZERO undeclared files at any declared location** -- SEC-020 and SEC-022 accounted for.

**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.

## Item 12 -- `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`.

## Item 13 -- COMMITTEE AUDIT (6 lenses) + phase 1: record corrections

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.
- CURRENT-STATE's `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).
- The **"ZERO undeclared files ... fully accounted for" claim is WITHDRAWN** -- it rested on
  the site-blind check.
- **"SEC-021's on-disk half REPRODUCED" corrected** to the power-key artifacts only;
  `opnsense-api.txt`'s on-disk absence was masked.
- Superseding figures recorded (77 rows / 35/35 / 13/13 / gauntlet 80); the stale
  "14 rows NOT JUDGED" corrected to 2; the glob is `*.pass`, not `admin.pass`.
- **12-column amendment recorded on D-137** in `design-decisions.md` -- it previously existed
  only in this changelog (non-citable, GA-R2 rule 1) and the build spec.
- Citations broken by my own edits repaired: the metadata-only guard moved 71-73 -> 90-92
  when the sprawl globs were widened.
- Plan's "12 operator-terminal rows" reconciled to the delivered 27.
- Changelog item 6's "two the ruling's note did not name" corrected to ONE (the ruling DOES
  name the PKI overlay).
- `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.

## Item 14 -- phase 2: the eight confirmed false-greens, fixed and regression-locked

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.

1. **Tier 2 is now scoped by (host-role, SITE-KEY), not host-role alone.**
   `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).
2. **`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.
3. **Literal-file locations get absent/unreadable detection.** `_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**.
4. **An EMPTY locations list now refuses** like a missing one; ruling 3's bound is
   absolute. Regression: **T37**.
5. **Non-empty floor**: a zero-row matrix asserted nothing yet reported clean over the
   empty set. Regression: **T38**.
6. **Crash path closed**: a mint-ref pointing at a directory raised `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.
7. **Mode enforcement no longer custody-gated.** Exact-match still applies to
   `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**.
8. **S5's second direction is tested.** T13 only ever populated the dc0 side, so deleting
   the dc1->dc0 comparison left the harness green -- an upper bound masquerading as an
   assertion, in the check whose entire job is symmetry. Regression: **T40**.

**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.

## Item 15 -- phase 3: re-measure the register; SEC-024 opened

**~21 inferred filenames corrected against their own mint-refs** (the session had owned
six; those were only the rows the first sweep physically touched):
- **octavia, 8 rows** -> `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.
- **vault, 2 rows** -> `init.txt` (the previous value was the DIRECTORY name).
- **tenant, 5 rows** -> `<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).

## 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.
