# Security exposure / obligation ledger (DOCFIX-078)

Commercial posture: every credential exposure, rotation obligation, and
security TODO gets a ROW here with an owner and status -- never only a comment
in a script header (where the libvirt item below lived for a week). Review this
ledger at every phase-00 (teardown) and before any handoff. Locations of key
material are deliberately NOT recorded here -- custody detail lives off-repo
per D-069.

| id | date | item | source / evidence | owner | status |
|---|---|---|---|---|---|
| SEC-001 | 2026-06-26 | libvirt SSH credential printed in plaintext by `maas machine power-parameters` during reenroll work | scripts/reenroll-hosts.sh header note | operator | OPEN -- rotate at v1 close (operator ruling 2026-07-06; was "after rebuild") |
| SEC-002 | 2026-06-17 | juju action params persist in the operation log -- charm authorization must use short-lived child tokens | DOCFIX-011 | operator | STANDING RULE (verify each vault authorize) |
| SEC-003 | 2026-07-03 | Vault unseal-key custody is single-operator (bus factor) | D-069 | operator | OPEN -- assign custodians + rehearse second-person unseal (DEFERRED 2026-07-06 by operator; note: keeps d011-06 MANUAL, gates D-011 full close) |
| SEC-004 | 2026-05-27 | repo temporarily PUBLIC for v1 web_fetch workflow | project completion list | operator | OPEN -- flip to private at v1 close (deferral reaffirmed 2026-07-06) |
| SEC-005 | 2026-07-13 | long-lived GitBucket PAT stored in PLAINTEXT on the jumphost (`credential.helper store`, mode 600) so the agent can push unattended; GitBucket PATs are account-wide -- no per-repo scoping exists | docs/archive/changelogs/changelog-20260713-git-credential-store.md | operator | OPEN -- dedicated token (not a reused personal one); revoke + rotate at v1 close, or immediately if the jumphost is shared/rebuilt. Revoke: GitBucket -> Account Settings -> Applications |
| SEC-006 | 2026-07-12 | NetBox API token PASTED INTO AGENT CHAT CONTEXT during D-111 IPv6 reconciliation -- the token is BURNED (it exists in a transcript that is not under our control) and must be revoked + reissued, not merely rotated on schedule | docs/archive/changelogs/changelog-20260712-office1-opnsense-edge-build.md; flagged again at 2026-07-12 session close | operator | OPEN -- **DEFERRED by operator ruling 2026-07-13: revoke at completion of this deployment.** Until then the token is live and exposed. Revoke: NetBox -> Admin -> API Tokens |
| SEC-007 | 2026-07-12 | `~/vr1-office1-creds/` on the jumphost holds the Office1 edge root password + its bcrypt hash + the `office1_svc` SSH PRIVATE key + (added 2026-07-13) the OPNsense **API key/secret** (`opnsense-api.txt`, 0600), all in plaintext (dir 0700, files 0600) | 2026-07-12 edge build; custody detail deliberately not recorded here per D-069 | operator | OPEN -- required for edge management (D-112(c) makes SSH the only management path), so this is a ROTATION obligation, not a delete-me. Rotate at v1 close / if the jumphost is rebuilt or shared |
| SEC-008 | 2026-07-13 | `~/vr1-office1-creds/tailscale-authkey.txt` (vcloud, 0600) -- the operator's 48-char Tailscale auth key for the SELF-HOSTED control plane `tailscale.baldurkeep.com`. Used to enrol `office1-tailscale`. Consumed BY PATH and never printed into a session; the copy shipped to the node was SHREDDED after `tailscale up` (tailscaled holds its own node key now). | 2026-07-13 Office1 Tailscale enrolment; docs/vr1-office1-as-built.md | operator | OPEN -- ROTATION obligation, not a delete-me: the key is still on vcloud for re-enrolment. Revoke/reissue on the control server at v1 close, or immediately if vcloud is rebuilt or shared. |
| SEC-009 | 2026-07-15 | Credential/env SPRAWL + world-readable exposure on vcloud: three env files sat loose in `~` outside the consolidated `~/vr1-office1-creds/` -- `.vr1-netbox.env` (upstream NetBox token), `vr1-office1.env` (edge root-password hash + SSH key path), and `vr1-stage1.env` which held `TF_VAR_maas_api_key` (a MAAS API secret) at mode **0664 (group/world-readable)**; `tailscale-authkey.txt` in the creds dir was also 0664. | docs/archive/changelogs/changelog-20260715-creds-consolidation.md | operator | **REMEDIATED 2026-07-15** -- all three moved into `~/vr1-office1-creds/`, every sensitive file `chmod 600`. STANDING CONVENTION established (below). Note: this is a CLOSED in-house test; the rotation obligations of SEC-005/006/007 still apply at v1 close, this row only closes the SPRAWL + PERMS exposure. |
| SEC-010 | 2026-07-16 | **metal-admin DC-LOCAL invariant (D-052/D-100) is PRESERVED but NOT ENFORCED in the committed Stage-3 config.** The `vvr1_dc0` rack straddles metal-admin (10.12.8.0/22, DC-local) + the office1<->dc0 transit (crosses fiber). Its committed cloud-init pins static IPs only -- **no `net.ipv4.ip_forward=0` sysctl, no host firewall on the transit leg.** Cross-plane routing (metal-admin <-> the whole Office1 /22, bidirectional) is blocked ONLY by Ubuntu's distro default; the deferred MAAS-rack install could silently flip it. A MAAS rack proxies at the application layer and needs no kernel forwarding, so pinning is free. **MODEL B UPDATE (2026-07-16):** after the D-123 Model B reshape, the OUTER `vvr1-dc0` is single-leg (transit only); metal-admin + the other 5 planes are now INNER bridges on `vvr1-dc0`'s own libvirtd. The forwarding hardening is MORE critical (vvr1-dc0 now bridges ALL 6 inner planes + the transit) and belongs on the inner libvirt host, not just a 2-leg rack -- wire it in the C3 bootstrap (`site-headend-install.sh` node-host mode). | 2026-07-16 plane-segregation review + Model B reshape cross-check; `opentofu/main.tf` `module "vvr1_dc0"` | operator | **CLOSED 2026-07-20** (was: artifact COMMITTED Phase D 2026-07-16, gated on apply+verify -- both now done, see close note at end of this cell). Enforced via a FORWARD-drop across the transit leg, NOT a global `ip_forward=0`. `scripts/site-headend-install.sh --host-nodes` writes `/etc/nftables-sec010.nft` (drop FORWARD in+out the transit interface, default `mgmt`) + a boot-persistent `sec010-fw.service`; `--host-nodes --check` is the MECHANICAL pre-apply gate (fails if the rule is absent, AND -- hardened 2026-07-16 -- if the keyed transit interface does not EXIST, since an nftables oifname on an absent iface loads clean but matches nothing = fail-open). **D-125 UPDATE (2026-07-16):** the earlier rationale (a global `ip_forward=0` being unusable because the inner `vr1-dc0-wan` NAT forced `ip_forward=1`) NO LONGER applies -- under bridge-in the inner `vr1-dc0-wan` is a BRIDGE (no inner NAT). The scoped FORWARD-drop is RETAINED as-is (correct either way). **br_netfilter CONSTRAINT:** it MUST stay interface-scoped and NEVER be globalized -- with `br_netfilter` loaded, bridged WAN frames traverse the L3 FORWARD chain, so a global forward-drop would silently kill the `br-vr1-dc0-wan` WAN path. `--check` now also verifies the WAN bridge exists with its uplink enslaved (same fail-open class). Nothing routes across the fiber THROUGH vvr1-dc0; the rack proxies MAAS at the app layer (originated/terminated, not forwarded). Same pin follows onto `voffice1` when its transit leg is wired. Region route must target only the rack transit /30, never 10.12.8.0/22. **CLOSED 2026-07-20 (operator ruling, exact utterance: "Close SEC-010 (Recommended)"): applied + verified BOTH ends in the logged dc0-deploy step-B window.** vvr1-dc0: written by `site-headend-install.sh --role rack --host-nodes`, verified by the named `--check` EXIT 0 (`docs/audit/stepB-check-20260720-final.txt` -- rule present + `enp1s0` exists + `br-vr1-dc0-wan`/`enp2s0` enslaved; note the transit interface is `enp1s0`, not the script-default `mgmt` -- netplan set-name dropped, session changelog). voffice1 (the "same pin follows" clause -- its transit leg `enp2s0` was wired 2026-07-20): identical scoped artifact + `sec010-fw.service` enabled, `nft list table inet sec010` verified, proxy/region paths re-verified working post-pin. |
| SEC-012 | 2026-07-20 | **New SERVICE credential: a dedicated MAAS -> libvirt SSH keypair.** Minted during the DC0 deploy so MAAS can power-control the inner node VMs (`power_type=virsh`). It is a SERVICE credential held by a daemon, not an operator key: the private half is installed inside the MAAS snap on BOTH controllers (region + DC rack) because MAAS dials the power address from whichever controller it chooses -- MEASURED to be the REGION, not the rack that owns the hardware. The public half authorizes a user that is in the rack's `libvirt` group, so anyone holding this key can drive libvirt on `vvr1-dc0` -- i.e. power/define/destroy every inner node VM and the DC edge. Blast radius is deliberately narrowed by being per-purpose (operator-ruled 2026-07-20: "Dedicated MAAS->libvirt key" over reusing the dc0 service key). | 2026-07-20 step-D power wiring; `scripts/maas-node-power.sh` header; session changelog items 15-17 | operator | **OPEN -- rotation obligation + a scope question.** (1) ROTATE at v1 close, or immediately if either controller is rebuilt or shared. (2) SCOPE: the key authorizes a `libvirt`-group user, which is broader than the power verbs MAAS actually needs; a tighter grant (dedicated unix user + polkit rule limiting to domain start/stop/status) is the hardening candidate and is Roosevelt-relevant, since bare metal replaces this with IPMI credentials that have the same "power-only vs full-control" question. Custody detail off-repo per D-069. |
| SEC-013 | 2026-07-20 | **MAAS API key materialized to disk on the region.** An admin-scoped MAAS API key (`consumer:token:secret`) was placed by the operator in a 0600 file on `voffice1` so the `opentofu/vr1-dc0-maas` root could consume it via `TF_VAR_maas_api_key`. It grants FULL MAAS admin API access (machines, power, deploy, users). Two exposure surfaces beyond the file itself: (a) the OpenTofu **state file** of any root that uses the maas provider records it -- unavoidable with this provider, so that state inherits credential handling (0600, never committed); (b) a MAAS CLI profile was also created for the operator user from the same key. It was verified by FORMAT ONLY (71 bytes, 3 colon-separated parts) and never printed, echoed, or passed in argv. | 2026-07-20 step-D part 2; `opentofu/vr1-dc0-maas/main.tf` header; session changelog item 15 | operator | **OPEN -- rotation obligation (surface NARROWED 2026-07-21).** The retire-fully ruling was executed 2026-07-21: `opentofu/vr1-dc0-maas` removed from the repo, the on-disk key file (`~/vr1-dc0-creds/maas-api-key.txt` on voffice1) and the root's `terraform.tfstate` DELETED, absence verified by listing (session changelog 2026-07-21 items 13-15; NOTE the pod-object deletion cascaded to the 9 machine records -- incident capture `docs/audit/incident-20260721-pod-delete-cascade.txt`, recovered same-day). REMAINING surface: the operator's MAAS CLI profile only. Rotate the underlying API key at v1 close, or immediately if `voffice1` is rebuilt/shared. Custody detail off-repo per D-069. |
| SEC-014 | 2026-07-21 | **Rack cluster secret exposed to session context.** During the commissioning diagnosis, a read of `/var/snap/maas/<rev>/agent.yaml` on the DC0 rack (hunting the agent resolver's config surface) returned the rack's MAAS cluster `secret` into the Claude session context, the operator terminal scrollback, and the as-executed log (`~/as-executed/2026-07-21-ops-commissioning-diag.log`, 0600, jumphost-only). The secret authenticates rack<->region enrollment. The read was not anticipated to contain a credential (config file, not a key file); disclosed same-session. | 2026-07-21 session changelog item 6; docs/audit/commissioning-diag-20260721.txt | operator | **OPEN -- rotation obligation.** Rotate the MAAS shared secret (= rack re-enrollment for `7chphy`) at a convenient maintenance point, or immediately if session artifacts leave the jumphost. Process fix queued: add `agent.yaml` to the guard hook's never-read list alongside key/cred globs. |
| SEC-011 | 2026-07-16 | **Node least-connectivity gap (not an L2 breach).** Under D-121 Option C role separation, all nodes get a uniform 6-plane NIC set, so a ceph-osd STORAGE node has a leg on provider-public (external/FIP) + data-tenant (tenant geneve) -- planes it never binds per D-052. Planes stay isolated L2 (no crosstalk). | 2026-07-16 plane-segregation review; `opentofu/main.tf` `local.vr1_dc0_node_nics` | operator | **CLOSED 2026-07-16 (operator ruling -- keep uniform 6-NIC).** Review R3-F10: A2's cross-examination refuted the attack-surface concern -- in the isolated-L2 sim the unbound vNICs have no reachability out, and pruning would INCREASE Roosevelt-delta (baremetal trunks all VLANs to every node on bonded NICs, so all planes are present regardless of L3 binding). Uniform 6-NIC is the more Roosevelt-faithful model. Accepted non-issue; no code change. |
| SEC-015 | 2026-07-23 | **dc1 edge SSH private key staged transiently on the DC1 rack.** The `vr1-dc1-edge_ed25519` private key (root-granting on `vr1-dc1-opnsense`) was scp'd to `vvr1-dc1:~/vr1-dc1-edge_ed25519` (0600) so `opnsense-bootstrap-apikey.sh` -- which SSHes the edge directly, no ProxyJump -- could mint the API key there (dc0 method). It existed on the rack only for the mint + v4-addressing phase and was `shred -u`'d immediately after (absence verified by `ls`). The permanent copy stays on vcloud `~/vr1-dc1-creds/` (SEC-007 pattern: edge SSH is the only management path -- a rotation obligation, not a delete-me). | 2026-07-23 session changelog item 6; docs/changelog-20260723-g12-dc1-edge.md | operator | **OPEN -- rotation obligation (transient exposure already remediated).** The on-disk rack copy is gone; the standing surface is the vcloud key + the minted API key/secret (`~/vr1-dc1-creds/opnsense-api.txt`). Rotate both at v1 close, or immediately if vcloud is rebuilt/shared. Roosevelt note: metal edges reached differently may avoid the rack-staging step entirely (a ProxyCommand-from-vcloud mint keeps the key off the rack -- deferred, not adopted this build to stay on the dc0-proven path). |

| SEC-016 | 2026-07-23 | **Dedicated dc1 MAAS->libvirt power key (per-DC isolation -- operator-ruled).** dc1 commissioning needs `power_type=virsh`, which has the REGION's MAAS snap SSH to the dc1 rack's libvirt to power-cycle nodes. RULING (GA-R5) -- question presented: "How should the region drive the dc1 rack's libvirt for node power control (power_type=virsh)?" options (A) reuse the SEC-012 key on dc1, (B) mint a dedicated dc1 power key. Operator answer, exact utterance (option selected): **"Mint a dedicated dc1 power key"** -- a SEPARATE per-DC MAAS->libvirt keypair, NOT cross-DC reuse of SEC-012's key. Mechanism: private half in the region MAAS snap (`/var/snap/maas/current/root/.ssh/` + per-host `ssh config` Host 172.31.0.6 -> that IdentityFile, so 172.31.0.2/dc0 keeps SEC-012 and 172.31.0.6/dc1 uses this one); public half authorizes jessea123 (libvirt group) on the dc1 rack. The maas-node-power SCRIPT's own mapping virsh (jessea123 on voffice1) uses the dc1 SERVICE key via voffice1 `ssh config` -- the dc0 split exactly (script=svc key, MAAS=dedicated key). | 2026-07-23 session changelog; docs/changelog-20260723-g12-dc1-edge.md; dc0 precedent SEC-012 | operator | **OPEN -- rotation obligation.** (1) ROTATE at v1 close or if the region/dc1 rack is rebuilt/shared. (2) Same libvirt-group SCOPE hardening candidate as SEC-012 (power-only polkit grant). (3) FRAGILITY: the snap-side key + ssh config live under per-revision `/var/snap/maas/current/` and may not survive a snap refresh -- re-assert after any refresh (queued to DC standup DoD). Roosevelt-relevance: establishes PER-DC power-credential isolation (bare metal: per-DC IPMI/BMC creds, never a shared cross-DC power credential) -- whether that becomes a standing D-principle is a follow-up, not blocking. |

| SEC-017 | 2026-07-24 | **Third-party Claude Code plugin `caveman@caveman` (JuliusBrussee/caveman) installed at USER scope on vcloud -- a standing per-session/per-prompt third-party code-execution surface on a credential-bearing jumphost.** SessionStart + UserPromptSubmit hooks run `node` on every session start and every prompt while `enabledPlugins` is true, INDEPENDENT of the `CAVEMAN_DEFAULT_MODE` guard (the env var gates terse-output ARMING, NOT hook EXECUTION). Inspected benign at SHA `0d95a81` by a 4-seat council audit + operator self-remeasure 2026-07-24: no network egress, no shell, one gated subprocess (`/caveman-stats` only), symlink-hardened flag I/O, zero PATH contribution/shadowing. This verdict is POINT-IN-TIME: `plugin.json` carries no `version` field (every git commit is a new "version"), the marketplace source tracks the default branch with no tag/SHA ref -> the code is NOT declaratively pinnable; third-party-marketplace auto-update is OFF by default (Claude Code 2.1.218) but flippable via the `/plugin` UI or managed settings. Baseline: git clone HEAD `0d95a81d35a9f2d123a5e9430d1cfc43d55f1bb0` (tree clean), executing cache copy == clone (only a `.in_use` runtime marker differs); 6 audited files' full sha256 recorded in the 2026-07-24 changelog. | docs/changelog-20260723-caveman-optin-guard.md (commit 338c919); docs/changelog-20260724-caveman-disable.md; this session's council audit | operator | **OPEN -- supply-chain re-verify obligation (NOT a credential rotation).** DISABLED on the live host 2026-07-24: `enabledPlugins."caveman@caveman"=false` at BOTH user (`~/.claude/settings.json`) and committed repo (`.claude/settings.json`, higher precedence) scope, `CAVEMAN_DEFAULT_MODE=off` retained -- this STOPS the per-turn hook execution from the next session start. Marketplace entry RETAINED so a deliberate re-enable for hardened-workflow deployment TESTING stays possible. STANDING DUTY: on ANY re-enable OR plugin update, RE-VERIFY the hooks against the baseline (confirm the clone is at `0d95a81`; re-hash + `diff` the cache copy vs the marketplace clone) BEFORE trusting -- the benign verdict does not survive a version bump. Keep-available-vs-uninstall decision DEFERRED (NOT yet ruled). Provenance for the disable: operator accepted the council-review recommendations this session (exact utterance: "I'll accept your recommendations"). Register note: OPS adoption, no D-number (GA-R3 -- no Roosevelt-delta; precedent D-129); this row is the durable home the ledger mandate requires ("row at discovery, never only a note"). Count note: this row takes open SEC rows 12 -> 13; CURRENT-STATE G14 reconciles the count at Stage 4 close per its existing deferral pattern. |

| SEC-018 | 2026-07-24 | **Dedicated dc0 MAAS admin API key (Juju -> MAAS cloud credential).** Created so the vr1-dc0 Juju controller can authenticate to the Office1 MAAS region (bootstrap + provision dc0 nodes). Per-DC credential isolation (SEC-012/-016 pattern; NEVER a cross-DC key): dedicated MAAS superuser `juju-vr1-dc0`, its API key staged at `~/vr1-dc0-creds/maas-api-key.txt` on vcloud (SEC-009, 0600), generated FROM the jumphost so it never persisted on the region, verified by FORMAT ONLY (parts=3, 70 chars), never printed/echoed/in argv. The user's password is random + UNSTORED (Juju auths via the key). GRANT: admin-scoped MAAS API (full machine/power/deploy). | 2026-07-24 driven creation (as-executed `2026-07-24-dc-maas-creds`); phase-4 Step 2.0; docs/changelog-20260724-caveman-disable.md item 7 | operator | **OPEN -- rotation obligation.** Rotate at v1 close, or immediately if it leaks or the region/jumphost is rebuilt/shared. Note it also lands in whatever Juju add-credential store + the maas-provider state of any root that consumes it (same 0600 handling). **2026-07-24: a runtime copy now lives in Juju's client credential store on voffice1 (`~/.local/share/juju/credentials.yaml`, cloud `vr1-maas`, credential `vr1-dc0-cred`) -- the canonical file stays on vcloud; this is the operator-noted consequence of running the Juju client on the headend (voffice1) per D-128.** Roosevelt: per-DC cloud credential. Custody off-repo per D-069. |

| SEC-019 | 2026-07-24 | **Dedicated dc1 MAAS admin API key (Juju -> MAAS cloud credential).** Same as SEC-018 for the vr1-dc1 Juju controller: dedicated MAAS superuser `juju-vr1-dc1`, key staged at `~/vr1-dc1-creds/maas-api-key.txt` on vcloud (0600), generated from the jumphost, format-verified (parts=3, 70 chars), never printed; random unstored password. GRANT: admin-scoped MAAS API. | 2026-07-24 driven creation (as-executed `2026-07-24-dc-maas-creds`); phase-4 Step 2.0; docs/changelog-20260724-caveman-disable.md item 7 | operator | **OPEN -- rotation obligation** (same disposition as SEC-018). **2026-07-24: runtime copy now in Juju's client credential store on voffice1 (`vr1-maas` / `vr1-dc1-cred`); canonical file stays on vcloud.** Roosevelt: per-DC cloud credential. Custody off-repo per D-069. |

| SEC-020 | 2026-07-25 | **MAAS region superuser passwords: an un-consolidated VM-minted secret, plus a new human GUI identity -- and the human/service identity conflation underneath both.** (a) The `admin` superuser's password was minted ON the region VM by `scripts/site-headend-install.sh:452` (`newpass` -> 28 chars alphanumeric, no trailing newline) and sat root-only at `voffice1:/root/maas-secrets/admin.pass` since 2026-07-13, never consolidated to `~/vr1-office1-creds/` -- the SAME SEC-009 miss class the NetBox token established (a site-VM-minted secret with no forcing function to reach the folder). It was NOT lost and NOT unset: MEASURED working this session (login POST 204 with the stored value vs 400 with a wrong-password control; Django `has_usable_password=True`). The prior claim that MAAS web-GUI login "does NOT exist / was never minted" (session-ledger 2026-07-25 close) is FALSIFIED and corrected in the same commit. (b) ROOT CAUSE of the miss: `admin` is simultaneously the human GUI login AND the automation identity -- the installer mints its API key (`:453`) and logs the CLI in as profile `admin` (`:455`), and 19 `maas admin ...` call sites across 4 scripts (`site-headend-install.sh` 10, `phase-04-network-verify.sh` 5, `phase-04-network-create.sh` 3, `phase-00-teardown-release.sh` 1) hardcode that profile. Automation only ever needed the KEY, so nobody ever needed the PASSWORD, so nothing forced it into the folder. (c) NEW standing credential: superuser `operator` (email `jesse.austin@neumatrix.com`) minted 2026-07-25 for HUMAN GUI login, 32-char base64 generated on vcloud, set via **stdin -- never argv** (SEC-018 discipline; note `maas changepassword` has no `--password` and `createadmin --password` would expose it in argv, the shape `site-headend-install.sh:452` uses). Both passwords now at `~/vr1-office1-creds/{maas-admin-password,maas-operator-password}` 0600, declared in `creds-manifests/vr1-office1.manifest`, `creds-audit vr1-office1` CLEAN. | this session (operator-ruled scope: "New account + consolidate only" -- no existing password rotated); measured captures in `docs/changelog-20260725-maas-admin-recovery.md`; `scripts/site-headend-install.sh:452-455` | operator | **OPEN -- rotation obligation on TWO credentials** (`admin` + `operator` region superusers; rotate at v1 close, or immediately if vcloud/the region is rebuilt or shared). `admin`'s password was deliberately NOT rotated this session (operator-ruled) -- so `voffice1:/root/maas-secrets/admin.pass` REMAINS source-of-record and the vcloud file is a byte-identical (sha256-verified) working copy; **any future `admin` rotation MUST update both copies in one operation or the VM copy becomes a stale trap.** ALSO OPEN, un-actioned by ruling (hard rule 1, findings are logged not executed): (i) `voffice1:/root/maas-secrets/{admin.apikey,db.pass,lxd-trust.pass}` are still un-consolidated -- `admin.apikey` is deliberately NOT copied (trivially regenerable via `maas apikey --username=admin`, and a second copy is pure added surface), `db.pass`/`lxd-trust.pass` are region infrastructure and out of this task's scope; (ii) the DURABLE fix for the miss class is a `site-headend-install.sh` amendment to consolidate at mint time (queued, unruled); (iii) the human/service conflation in (b) is proposable as the next-free D-number, tagged [ARCH], a Roosevelt-delta (per-identity MAAS separation / audit attribution on a multi-tenant cloud) -- NOT assigned, GA-R3 doubt-resolves-DOWN, operator's call. Count note: this row takes open SEC rows 15 -> 16; CURRENT-STATE G14 (which still reads 12) reconciles at Stage 4 close per its existing deferral. Custody off-repo per D-069. |

| SEC-021 | 2026-07-25 | **dc0 credential custody defects -- a consolidated credential is ABSENT from its recorded location, and the per-DC power key diverges in name, host and custody.** (a) `docs/archive/changelogs/changelog-20260719-dc0-deploy-stepB.md:263` records the dc0 edge REST API credential written to `~/vr1-dc0-creds/opnsense-api.txt` (0600, secret never printed). A `find` over vcloud `$HOME` returns `opnsense-api.txt` for `vr1-office1` (2026-07-13) and `vr1-dc1` (2026-07-23) ONLY -- **there is no dc0 copy**. It was never added to dc0's manifest, so `creds-audit vr1-dc0` reports CLEAN. A changelog asserting consolidation is not evidence the consolidation survived. SEVERITY: this is a custody/record defect plus an API-access gap, NOT a management lockout -- `vr1-dc0-edge_ed25519{,.pub}` IS present, and D-112(c) makes key-only SSH the primary edge management path. (b) The dc0 MAAS->libvirt power key (SEC-012) is `maas-virsh_ed25519` on **voffice1**, while dc1's equivalent (SEC-016) is `vr1-dc1-maas-power_ed25519{,.pub}` on **vcloud** and manifest-declared: same purpose, different filename, different host, and dc0 carries no `.pub`. Per-DC rows that should be symmetric are not, and nothing compares them. | measured read-only this session (folder listings both hosts, `find` over vcloud `$HOME`, both manifests); D-137 research; plan `whimsical-wandering-spindle` findings F1+F3 | operator | **OPEN.** (a) needs a decision between RE-MINT (D-113(a2) vendor model, the dc1-proven path) and locating an off-jumphost copy; until then dc0 has no API management path from vcloud. (b) needs a naming/custody reconciliation to the dc1 shape, and dc0's key should gain a jumphost copy + manifest row -- note SEC-016's standing fragility applies harder to dc0: the snap-side key lives under per-revision `/var/snap/maas/current/` and may not survive a snap refresh, and dc0 currently has NO jumphost copy to re-assert from. NOT actioned this session (hard rule 1; findings are logged, never executed mid-task). Custody off-repo per D-069. |

| SEC-022 | 2026-07-25 | **Two UNAUDITED shadow `*-creds/` credential stores exist on voffice1 -- an entire parallel store outside every control.** `ssh voffice1 'ls -d ~/*-creds/'` returns `~/vr1-dc0-creds/` AND `~/vr1-dc1-creds/`, holding real key material (dc0: `maas-virsh_ed25519` 464 bytes 2026-07-20, `vr1-dc0_svc_ed25519{,.pub}`). SEC-009's convention says ALL site secrets live in `~/<site>-creds/` on the JUMPHOST; nothing anticipated a second store on the headend. `scripts/creds-audit.sh:29-31` derives `CREDS="$CREDS_ROOT/${SITE}-creds"` with `CREDS_ROOT` defaulting to `$HOME` **on whatever host it runs on** -- i.e. vcloud only -- and the script contains no `ssh`/`scp` at all, so these stores are structurally invisible to every check that exists. This is a SCOPE gap in the control itself, not a one-off miss: the D-128 operating model puts Plane-2 execution ON voffice1, so headend-resident credentials are expected to keep appearing. | measured read-only this session; `scripts/creds-audit.sh:29-31` (no remote capability); D-137 research finding F2 | operator | **OPEN -- scope gap + rotation obligation.** Every file in those stores is a live credential with no manifest row, no mode/presence enforcement, and no rotation tracking. Remediation is coupled to the D-137 `--remote` fork (declared-directories vs broader sweep) which is UNRULED -- so no tooling is built yet. Interim: treat `voffice1:~/*-creds/` as in-scope for any rotation performed at v1 close. Custody off-repo per D-069. |

| SEC-023 | 2026-07-25 | **Sprawl detection has systematic blind spots, and one exposure is already PREDICTED for Stage 5.** `scripts/creds-audit.sh:72-73` sprawl-globs exactly `*.env`, `.*.env`, `*appcred*`, `*-cred*.txt`, `*authkey*`, `*.token`. That set misses whole file classes this repo actually uses: `admin.pass`, `*.apikey`, `*.key`, `*.pem`, `*_ed25519`, and `maas-api-key.txt` (which is manifest-declared in TWO sites yet would not be caught loose). PREDICTED EXPOSURE: `scripts/phase-03-admin-openrc.sh:32` writes `RC="${RC:-$HOME/admin-openrc}"` and `:72` writes `export OS_PASSWORD='$ADMIN_PASS'` into it -- a credential-bearing file in the HOME ROOT, outside any `*-creds/` folder, whose filename matches NONE of the sprawl globs. It does not exist yet (Stage 5 has not run), so no discovery sweep can flag it today; it is recorded here in advance so the mint is not missed when Stage 5 executes. NOTE the keystone admin password is CHARM-generated but OPERATOR-materialized (`:52` extracts it via `juju run keystone/leader get-admin-password`) -- the operator-ruled "materialization test" puts it firmly in scope. | measured read-only this session; `scripts/creds-audit.sh:72-73`; `scripts/phase-03-admin-openrc.sh:32,52,72`; D-137 research finding F4 | operator | **OPEN -- latent, becomes live at Stage 5.** Two remediations, both UNRULED and NOT built: widen the sprawl globs (cheap, independent of D-137), and add an expected-state row so the openrc credential is asserted rather than discovered. Until then, whoever runs Stage 5 phase-03 must consolidate `~/admin-openrc` into the site creds folder BY HAND at mint time. Count note: SEC-021/-022/-023 take open SEC rows 16 -> 19. Custody off-repo per D-069. |

| SEC-024 | 2026-07-26 | **`opentofu/terraform.tfstate.backup` carries the Office1 MAAS API key in PLAINTEXT at file mode 0664.** SEVERITY CORRECTED 2026-07-26 (measured, same day the row was opened): the original wording called it "group- and world-readable", which overstated it. The FILE BITS are 0664, but `opentofu/` is **0700** and `~` is 0750, so no other account can traverse to it and it is NOT effectively readable today. This is a DEFENCE-IN-DEPTH defect -- one `chmod` on the directory, a `cp -p` elsewhere, or an archive/extract would expose it -- not a live exposure. DOCFIX-175 records that `maas_api_key` reaches the OpenTofu state file in clear; the live `terraform.tfstate` is correctly 0600, but its automatically-written `.backup` sibling is 0664. MEASURED `stat -c '%n %a'` this session on the jumphost clone. FOUND BY the D-137 credential matrix on its first run after the committee fixes: the world-readable check had been gated to `custody=consolidated` rows (43 of 77 rows unchecked), and the tfstate siblings were not declared locations at all -- so neither the mode nor the file was visible to any control. Of the two `terraform.tfstate.pre-*` snapshots, `pre-G16-20260721` is also 0664 and `pre-G6-20260719` is 0600 -- an inconsistency consistent with an accidental umask at write time rather than a deliberate mode. Neither snapshot nor the backup is git-TRACKED (both match `.gitignore:72-73`), so SEC-004's public-repo exposure is NOT implicated. Other accounts exist on the host (`logxen` uid 1000, `libvirt-qemu`), which is why the directory mode is the only thing standing between them and the key. | measured 2026-07-26; `creds-matrix.py --tier2` E2 WORLD-READABLE; DOCFIX-175; capture `docs/audit/d137-tier2-postcommittee-20260726.txt` | operator | **OPEN -- logged, NOT actioned (hard rule 1).** Remediation is a `chmod` on the jumphost plus a decision about whether the historical snapshots should be retained at all; both are mutations and neither is in this step's scope. Rotation of the underlying key remains the SEC-018/-019 obligation. Custody off-repo per D-069. |

| SEC-025 | 2026-07-27 | **NetBox web-GUI `admin` password consolidated to the jumphost -- a HUMAN GUI login that had never left the VM it was minted on.** Found by the 2026-07-27 consolidation audit, which enumerated the accounts that ACTUALLY EXIST instead of the ones the register already knew about. The password was minted on `office1-netbox` at stage 2 (`operator-terminal`, no repo mint command -- research FINDING 1 class) and sat ONLY at `/root/netbox-secrets/admin.pass` (0600, root-owned, 28 bytes). This is the THIRD instance of one miss class: the founding SEC-009 defect (`api.token`), SEC-020 (`admin.pass` for MAAS), and now this -- a VM-minted human credential with no forcing function to reach the operator's folder, so the human who needs it cannot find it and no audit reports it missing. It was structurally invisible: the matrix carried the row as `off-manifest-known`, which asserts nothing about the jumphost. REMEDIATED THE SAME DAY (this row is opened for the exposure the remediation CREATES, not for the gap it closed): a byte-identical copy now sits at `~/vr1-office1-creds/netbox-admin-password` (0600 in a 0700 dir), sha256-verified at both ends, value never printed or echoed. The VM copy REMAINS source-of-record. | 2026-07-27 consolidation audit, capture `docs/audit/creds-consolidation-audit-20260727.txt`; matrix rows `netbox-admin` (netbox=source-of-record, jumphost=verbatim-copy); `creds-manifests/vr1-office1.manifest` | operator | **OPEN -- rotation obligation + a stale-copy hazard.** (1) ROTATE at v1 close, or immediately if the jumphost is rebuilt or shared -- the same obligation SEC-007 and SEC-020 carry for the other consolidated human passwords. (2) STALE-COPY HAZARD: two copies now exist and nothing keeps them in step, so any future rotation MUST update BOTH or the jumphost copy becomes a trap that reads healthy -- identical to the caveat SEC-020 records for `maas-admin-password`. D-137 tier 3 (V1) now asserts byte-identity between them, so a divergence is at least DETECTABLE, which it was not before. (3) Distinct identity from `netbox-secret-key` (a service secret) and from the two NetBox API tokens -- do not merge them; note SEC-006's upstream token is separately declared burned with revocation ruled-deferred. Custody off-repo per D-069. |

**CREDENTIAL-LIFECYCLE POLICY LIVES IN D-137 -- this register does not carry the rule.**
DEMOTED 2026-07-26 per D-137 sub-ruling 4 ("D-137 is the authority"): the standing
consolidation convention that used to be stated here has MOVED to
`docs/design-decisions.md` D-137, which is now the credential-lifecycle policy authority.
This ledger returns to being purely an EXPOSURE/ROTATION register: one row per exposure,
with its disposition. Do not re-state policy here -- a gate may now cite a D-number
instead of an exposure row, which was the point of the ruling.

SEC-009's FOUNDING HISTORY stays below, deliberately: the NetBox-token miss is what
established the convention, and history is exactly what this register is for.

**ADDENDUM (2026-07-15): the sandbox NetBox token was MISSED by the original consolidation.** It lived
only at `/root/netbox-secrets/api.token` on `office1-netbox`, never in `~/vr1-office1-creds/` -- the
prior consolidation moved the three loose `~` env files but did not pull this VM-resident secret into
the folder, contrary to the standing convention ("ALL sensitive files for a site live in
`~/<site>-creds/`"). Found while starting C2 sub-task (i) (the fidelity re-check needed it). Now
consolidated to `~/vr1-office1-creds/vr1-netbox-sandbox.env` (mode 0600; `NETBOX_URL=http://10.10.1.10:8000`
+ the assembled `nbt_<key>.<plaintext>` token), copied off the VM without being printed into the
session. The `/root/netbox-secrets/` copy remains the source-of-record on the VM; this is a working
copy on the jumphost, mirroring how `vr1-netbox.env` holds the upstream token. See
`docs/archive/changelogs/changelog-20260715-fidelity-check-scope-correctness.md`.

**ENFORCEMENT -- HISTORY (2026-07-15).** The convention was never ENFORCED; a VM-minted
secret had no forcing function to get consolidated and no check to catch a miss. The
2026-07-15 build introduced `creds-manifests/<site>.manifest` and `scripts/creds-audit.sh`
(`docs/archive/changelogs/changelog-20260715-creds-audit.md`). That machinery is now
SUPERSEDED in its policy role by D-137: manifests are DERIVED from `creds-matrix.tsv`
rather than hand-declared, discovery is bounded by `creds-manifests/vm-secret-locations`,
and enforcement is a blocking preflight gate rather than a run-it-and-remember habit.
The current rule text is in D-137; this paragraph is kept as the record of what existed
before it.
