Newer
Older
openstack-caracal-dc-dc / docs / security-ledger.md

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. RETENTION RULED 2026-07-27 (GA-R5) -- question as presented: delete both pre-* state-surgery snapshots, keep both, or keep pre-G6 and delete pre-G16, noting both gates are CLOSED and deletion is irreversible with no other copy of that pre-surgery state. Operator answer, exact utterance: "Keep both". Both snapshots are RETAINED at 0600, neither git-tracked, as the only record of state prior to the two direct tfstate edits (G6 autostart, G16 channels). This row therefore stays OPEN as a standing WATCH, not an open remediation: the mode defect is fixed, but the umask CAUSE was out of ruled scope so a future apply may rewrite 0664 -- preflight P5 is the detection. 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.