# creds-matrix-notes.md -- `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).

---

## n-maas-region-admin
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.

## n-maas-admin-apikey
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.

## n-maas-cli-profile
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.

## n-maas-region-operator
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.

## n-region-infra-carveout
Region infrastructure, ruled OUT of the consolidation task's scope (SEC-020(i)). Present
at a declared location by decision, not by oversight.

## n-rack-enroll
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.

## n-no-mint-command
**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.

## n-edge-api
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.

## n-third-party
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.

## n-netbox-upstream
Minted on the upstream NetBox apex. SEC-006 records this credential as BURNED; the
rotation obligation stands at v1 close.

## n-netbox-sandbox
**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.

## n-netbox-vm-secrets
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.

## n-tofu-apikey
Office1 MAAS API key consumed by OpenTofu as `TF_VAR_maas_api_key`.

## n-tfstate-plaintext
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.

## n-office1-env
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.

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

## n-edge-key
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`.

## n-dc0-edge-api-absent
**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.

## n-dc0-power-key-divergence
**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.

## n-snap-key-fragility
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).

## n-dc1-power-key
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.

## n-dc-maas-apikey
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`.

## n-juju-cred-store
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.

## n-juju-user-nopassword
`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.

## n-headend-shadow-store
**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.

## n-outside-convention
**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.

## n-overlay-in-clone
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.

## n-tenant-dir
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.

## n-charm-minted
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.

## n-predicted-openrc
**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.

## n-cloud-only
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.

## n-ephemeral-unit
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.

## n-libvirt-power-pw
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.

---

## Checker behaviour worth knowing before you "fix" it

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

## n-colocated-nonsecret
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.

---

## Deliberate EXCLUSIONS (recorded so the decision is not silent)

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:

- Per-service DB / RabbitMQ / Ceph keyring credentials -- charm relation data (the capture
  confirms 56 apps, zero hardcoded secret options in the bundle).
- Vault-issued per-service TLS certificates -- they cascade from the charm-pki-local root;
  the verify script checks them, nothing mints them.
- The charm-pki-local root CA itself -- created inside Vault and never materialized.
- The short-lived (10-minute TTL) child Vault token and the charm authorization token --
  printed once, deliberately so the root token never reaches juju logs.
- CAPI/CAPO per-cluster CAs and bootstrap tokens, and per-cluster trustee users/trusts --
  in-cluster Kubernetes Secrets and cloud-side objects, minted at every cluster create.
- Throwaway acceptance-run credentials -- created and deleted within the same run.
- Tenant end-user passwords and client-minted CI application credentials -- these live on
  CLIENT infrastructure, outside this cloud's custody entirely.

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.
