notes-ref targets for creds-matrix.tsv (D-137)The notes-ref column of creds-matrix.tsv points here. Free text lives HERE and never in the TSV, so the TSV stays logical-keys-only (SEC-004) and greppable. Added by the 12-column amendment 2026-07-26 (docs/D-137-implementation-plan.md).
Parse contract. One ## <key> heading per note; the body is every line until the next heading. scripts/creds-matrix.py --render emits a note as the manifest comment above the rows that reference it -- this is how D-137 ruling 2's "prose must not be dropped" obligation is met for per-row governance text.
No secret values, no paths, no hostnames -- the same discipline as the TSV. Where a note must refer to a location it names the role token and the register row that holds the detail (docs/security-ledger.md remains the custody/exposure register of record).
The MAAS region admin superuser. Password 28 characters, alphanumeric, NO trailing newline -- that format spec is the verify-by-format basis; never verify by printing. The REGION copy is source-of-record and the jumphost copy is a byte-identical working copy (sha256-verified 2026-07-25). SEC-020 ruling: admin's password was deliberately NOT rotated, so any future rotation MUST update both copies in one operation or the region copy becomes a stale trap. This identity is also the automation identity, which is why it carries both a human and a service row -- see n-maas-cli-profile. That is the ruling-5 violation D-137 exists to make machine-detectable, not a data-entry error.
Deliberately NOT consolidated (SEC-020(i)): trivially regenerable, and a second copy is pure added surface. not-consolidated-ruled records that this is a settled decision, so --remote must not report it as an undeclared find.
The installer logs the CLI in as profile admin; 19 maas admin ... call sites across 4 scripts hardcode that profile (SEC-020(b)). ROOT CAUSE of the SEC-020 miss: automation only ever needed the API key, so nobody ever needed the password, so nothing forced it into the folder. Remediation is the operator/admin split begun 2026-07-25 -- migrate those call sites to a service identity, which is what clears the ruling-5 FAIL.
Minted 2026-07-25 for HUMAN GUI login so a person no longer authenticates as the service identity. 32-char base64, generated on the jumphost, set via stdin -- never argv (createadmin --password would expose it in argv, the shape the installer uses). The first human-only MAAS identity.
Region infrastructure, ruled OUT of the consolidation task's scope (SEC-020(i)). Present at a declared location by decision, not by oversight.
Side effect of maas init region+rack; consumed by each DC rack at enrollment via a 0600 copy. Mint and consume are different stages -- minted at region standup, consumed at each DC standup.
Research FINDING 1 -- reproducibility debt. ssh-keygen returns ZERO hits repo-wide; there is likewise no mkpasswd, htpasswd, pwgen, uuidgen, nor any Terraform random_password/tls_private_key. This credential's only provenance record is a manifest comment, so it is not reproducible: if the jumphost is rebuilt, no repo artifact states how to recreate it. That is a Roosevelt-transfer defect, not merely hygiene. mint-ref=operator-terminal rows are the debt to convert, and must be admitted by the parser rather than treated as errors.
OPNsense REST API key+secret, minted ON the edge via the vendor model (D-113(a2)) into a 0600 temp file that is then wiped, and retrieved to the jumphost creds folder by the bootstrap driver.
Operator-supplied from a third-party control server. Correctly has NO repo mint command -- this is the one FINDING 1 row that is not a defect.
Minted on the upstream NetBox apex. SEC-006 records this credential as BURNED; the rotation obligation stands at v1 close.
The founding SEC-009 miss. Minted on the NetBox VM and never pulled into the creds folder; it went unnoticed until the fidelity re-check needed it. That case is why creds-audit exists at all, and why the source-of-record vs consolidated distinction is modelled: a VM-minted secret has no forcing function to reach the folder.
Held on the NetBox VM, outside the jumphost creds folder. netbox-secret-key was rotated off netbox-docker's PUBLIC default. The admin password is a human GUI login and is a separate identity from the service SECRET_KEY -- do not merge these ids.
netbox-admin CONSOLIDATED 2026-07-27 (SEC-025): the VM copy is now source-of-record and ~/vr1-office1-creds/netbox-admin-password is a verbatim-copy, so tier-3 V1 asserts the two stay byte-identical. It had never left the VM it was minted on -- the THIRD instance of the miss class that SEC-009 (api.token) founded and SEC-020 (admin.pass) repeated: a VM-minted HUMAN login with no forcing function to reach the operator's folder. It was invisible to every control because off-manifest-known asserts nothing about the jumphost, so no check could report it missing. Found only by ENUMERATING the accounts that actually exist (maas admin users read for MAAS, this row's account by inspection) rather than auditing the ones the register already knew about -- the register cannot report an absence it has no row for. netbox-secret-key is deliberately NOT consolidated: it is a service secret consumed only by the container, with no human who needs to read it.
Office1 MAAS API key consumed by OpenTofu as TF_VAR_maas_api_key.
DOCFIX-175: the same API key reaches terraform.tfstate in PLAINTEXT. A secret-at-rest location with no mint of its own -- it exists only as a second copy, which is exactly what the (credential, location) row shape is for. Any rotation of n-tofu-apikey must account for this copy.
Environment file carrying the edge root-password hash and an SSH key path. One of the three files whose loose, 0664 placement in the home directory WAS SEC-009.
NOT a credential -- the human index of the creds folder, declared in the manifest so the folder's undeclared-file check does not fire on it. principal is - deliberately; the ruling-5 invariant must skip rows with no principal rather than fault them.
Edge service SSH keypair for the D-112(c) console bootstrap. The runbook describes the bootstrap step but does NOT contain the key generation itself -- see n-no-mint-command.
SEC-021, tier-1 detectable. dc1 declares an edge API credential and dc0 does not, so this expected row has no manifest row to satisfy it. This is an EXPECTED-BUT-ABSENT failure, not a matrix error: do not delete the row to make the checker green.
SEC-021, per-DC asymmetry. dc1's power key is consolidated on the jumphost under a DC-qualified name; dc0's equivalent exists under a DIFFERENT name and on a DIFFERENT host-role (the headend shadow store, SEC-022). Both the name and the location diverge from the per-DC pattern, so the expected jumphost rows fail EXPECTED-BUT-ABSENT while the divergent copy shows up at a declared remote location.
The private half is installed into the region MAAS snap so MAAS's own power operations can dial the rack. The snap path is per-revision and may not survive a snap refresh -- it must be re-asserted after any refresh (a standing DC-standup item). Per-DC isolation is mandatory here: NEVER a cross-DC power key (SEC-012 dc0, SEC-016 dc1).
Dedicated dc1 MAAS->libvirt power key, minted under the SEC-016 ruling "Mint a dedicated dc1 power key" (per-DC isolation). Roosevelt analog: per-DC IPMI/BMC credentials.
Per-DC MAAS admin API key backing the Juju cloud credential. Generated FROM the jumphost so it never persisted on the region -- which is why its provenance is local rather than a VM path. Verified by FORMAT ONLY (parts=3, 70 chars); never printed, echoed, or placed in argv. GRANT is admin-scoped MAAS API. The owning MAAS superuser's password is random and unstored by ruling -- see n-juju-user-nopassword.
A runtime copy lives in the Juju client credential store on the HEADEND, a consequence of running the Juju client there per D-128. The jumphost file stays canonical. Recorded off-manifest in the manifests today (SEC-018/-019); this row is what makes it visible to the checker instead.
createadmin is invoked with NO --password, so the value is stored NOWHERE. Kept random and unstored by operator decision recorded in SEC-018/SEC-019 (2026-07-24): the API key is what is load-bearing, and the operator identity removes any need to log in as this user. filename is - because there is no host artifact -- existence checks must skip such rows rather than fail them.
SEC-022 -- a scope gap in the control itself, not a one-off miss. An entire parallel *-creds/ store exists on the headend holding real key material. creds-audit.sh:29-31 resolves the folder against $HOME on whatever host it runs on, and the script contains no ssh/scp at all, so these stores are structurally invisible to every check that exists today. The D-128 operating model puts Plane-2 execution on the headend, so headend-resident credentials are EXPECTED to keep appearing -- this is why ruling 3's declared-locations list must include the headend, and why adding a location row is DoD for any new mint site.
Research FINDING 2. A credential directory outside the SEC-009 *-creds/ convention and outside creds-audit entirely -- the audit checks the per-site folders plus a loose FILE glob in the home directory, and never inspects these DIRECTORIES. Named in CLAUDE.md as a secret location, so it is KNOWN; it is simply outside the control. Materially extends SEC-023. The Vault directory holds the cloud's entire root of trust and its mint is an irreversible one-shot.
Contains base64 CA cert/key blobs PLUS the issuing-CA passphrase in PLAINTEXT, written inside the repo clone and gitignored. Given SEC-004 (this repo is currently PUBLIC), that gitignore is the only thing standing between a CA private key and publication.
Per-tenant credential directory outside the *-creds/ convention (research FINDING 2). Hard tenant isolation applies: D-069 keeps custody off-repo, and D-137 ruling 3 bounds --remote to declared locations precisely so no tenant surface is ever walked. The runbook tenant-onboarding-v2-DRAFT.md documents the SAME credential set as the onboard script -- two creation locations, ONE set; do not double-count them into extra rows.
Charm-minted at bundle apply with no repo mint site, then MATERIALIZED to a file by the operator. In matrix scope per the operator-ruled materialization test (in-matrix if a credential is ever written to a file, presented by a human, or consumed out-of-band, regardless of who generated it) -- a generation-based test would have missed it entirely.
PREDICTED, not present (SEC-023). The script that writes it runs at a stage this deployment has not reached, so the checker must report this row as not-yet-expected via an explicit skip with an [ok], never as a silent pass. Both readings of the acceptance test apply: this row exercises the not-yet-expected path, and the harness separately proves the widened sprawl glob CATCHES the file once it appears.
Lives in the cloud or on a unit rather than in a host file, but is consumed out-of-band (captured by an operator command at a documented step), so the materialization test puts it in scope. filename may be -; existence checks skip such rows.
Written to a 0600 temp file ON the unit during a stage step. In scope because it is written to a file, but it is short-lived by design -- absence outside its step is normal, so this row must never drive an existence failure.
The libvirt SSH password for MAAS virsh power control. Read INTERACTIVELY (never a CLI arg, never echoed, never logged, never in the repo) and injected into MAAS power_pass. reenroll-hosts.sh:22-25 records that it was exposed in plaintext on 2026-06-26 -- maas machine power-parameters echoes power_pass -- with a standing instruction to ROTATE it. It is in scope by the materialization test (presented by a human, consumed out-of-band) and had NO row until the 2026-07-26 committee found it: a credential with a live rotation obligation that the credential-lifecycle authority could not see.
E2 reports FILE MODE, not reachability -- read an E2 finding before calling it an exposure. SEC-024 was opened on a 0664 state file and first written up as "group- and world-readable"; measuring the path showed opentofu/ is 0700 and ~ is 0750, so no other account could actually reach it. It was a defence-in-depth defect, not a live exposure, and the row had to be corrected the same day. Reporting mode alone is the RIGHT default for a credential gate -- directory modes change, a cp -p or a tarball escapes them, and a gate that reasons about traversal would go quiet the moment someone tightened a parent directory for an unrelated reason. But it means the human writing up an E2 finding owes a reachability check before using the word "exposed".
S5 compares mint-stage and mint-ref as part of the per-DC shape, deliberately. Today both DCs agree on both, so it does not fire on them. A future DC that legitimately mints the same credential at a DIFFERENT stage -- one standing up after Stage 5, say -- would trip S5 on a non-defect. That strictness is INTENDED: a per-DC credential appearing at a different stage or from a different mint site is exactly the drift the register exists to surface, and it should be argued about rather than absorbed. If a future DC makes it genuinely wrong, narrow it with a recorded reason -- do not silently drop the two fields from the comparison.
S3 is a partial gate on ruling 2, not a full one. It compares parsed row fields for rows present on both sides; it does not compare manifest header prose, and --render emits only the consolidated jumphost rows. Byte-equality becomes enforceable once the manifests are actually replaced by rendered output, which is operator-gated because it changes what creds-audit demands at three sites.
NOT a credential -- a config or known-hosts artifact that happens to sit in a declared credential location. Declared so the undeclared-file check stays meaningful instead of crying wolf on every config file next to a key. principal is -, so ruling 5 skips it.
These credentials appear in the research capture but are NOT matrix rows, because they fail the materialization test -- never written to a file, never presented to a human, never consumed out-of-band:
If any of these later becomes materialized to a file, it acquires a row at that moment -- adding one is DoD for the change that materializes it.