# 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/archive/changelogs/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/archive/changelogs/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/archive/changelogs/changelog-20260723-caveman-optin-guard.md (commit 338c919); docs/archive/changelogs/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/archive/changelogs/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/archive/changelogs/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/archive/changelogs/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.
| SEC-026 | 2026-07-30 | **A MAAS admin-scoped API key becomes RESIDENT on a DC-local host, by D-138.** D-138 (RULED 2026-07-30) moves the cloud-facing client -- the Juju client, and `phase-03`..`phase-06`'s `openstack` CLI work -- INTO each DC, because `voffice1` has no L3 path to any DC node plane and SEC-010/D-052/D-125 deliberately forbid creating one. The client needs its DC's MAAS credential where it runs, so `juju-vr1-dc0`'s key (SEC-018) now lives on the dc0 client host, and `juju-vr1-dc1`'s (SEC-019) will on dc1's. **The exposure is wider than the DC it sits in: the key is MAAS ADMIN-scoped (full machine/power/deploy) and the MAAS region is SHARED by BOTH DCs**, so a host inside dc0 holding it has region-wide MAAS admin, including over dc1's hardware. This is a NEW residency, not a new key -- SEC-018/-019 already carry the keys' own rotation obligations. | 2026-07-30 Stage-5 bootstrap failure + D-138 ruling; capture `docs/audit/stage5-bootstrap-reachability-20260730.txt` | operator | **OPEN.** (1) **ISOLATION IS THE LOAD-BEARING CONTROL:** each DC's client host receives ONLY THAT DC's credential. Never copy the whole Juju client credential store (`~/.local/share/juju/credentials.yaml` on voffice1 holds BOTH `vr1-dc0-cred` and `vr1-dc1-cred`) to a DC host -- that single act would put dc1's credential on dc0's rack and destroy the per-DC isolation SEC-018/-019 exist to create. Verify after any client rebuild that `juju credentials --client` on a DC host lists exactly one. (2) ROTATE at v1 close, or immediately if the DC client host is rebuilt or shared. (3) SCOPE HARDENING CANDIDATE, Roosevelt-relevant: MAAS has no per-DC admin scope, so the blast radius is regional by construction; a narrower grant (or a per-DC MAAS region) is the fix and is deferred, not solved. (4) The residency must be re-asserted at every DC standup as part of D-138's definition-of-done. |
| SEC-027 | 2026-07-30 | **Per-DC MAAS region controller credentials -- a NEW credential class, three secrets per DC.** D-132's amendment (RULED 2026-07-30) gives each DC its own MAAS region controller VM at utility-band `.6`, running its own `regiond` + PostgreSQL. Each region therefore mints and holds: (a) a PostgreSQL role password for the `maas` DB role, (b) a MAAS region `admin` account password (a HUMAN login), and (c) that admin's MAAS API key (3-part, service). dc0's are MINTED and consolidated to `~/vr1-dc0-creds/` (`maas-region-db-password`, `maas-region-admin-password`, `maas-region-api-key.txt`, all 0600, sha256-verified identical to the on-VM source, never printed). dc1's are NOT YET MINTED -- its region VM is authored but not applied. **These are NOT the Office1 region's credentials and must never be conflated with them**: the entire point of the per-DC region is that each DC's provider API is independent of Office1. | 2026-07-30 Stage-5 region build; D-132 amendment + addendum; capture `docs/audit/stage5-bootstrap-reachability-20260730.txt` | operator | **OPEN -- rotation obligation + two structural notes.** (1) ROTATE all three at v1 close, or immediately if the region VM is rebuilt or shared. (2) **RESIDUAL ARGV EXPOSURE, recorded rather than hidden:** `maas init` offers NO stdin or file option for `--database-uri`, so the DB password is briefly visible in that host's own `ps` output at init time and is then written to `/var/snap/maas/current/regiond.conf`. The URI was assembled inside the remote shell from a 0600 file so it never reached the operator terminal or the session transcript, but the argv exposure on the VM itself is unavoidable with this tool. Identical in shape to the existing Office1 region -- not a new exposure class. (3) **PROVENANCE DEBT, deliberate:** all five matrix rows carry `mint-ref=operator-terminal` because NO repo script mints these yet. That is truthful, not an oversight -- writing the region install into a runbook or script is what clears it, and until then these are not reproducible from the repo if the jumphost is rebuilt (research FINDING 1 class). (4) The `region` host-role token was added to `scripts/creds-matrix.py`'s enum for this class; `creds-manifests/vm-secret-locations` gained four rows, which is definition-of-done for a new mint site (an unlisted location is not audited -- how SEC-022 happened). |
| SEC-028 | 2026-07-30 | **Per-DC JUJU SERVICE credential, and the RACK becomes a credential-bearing host for the first time.** D-132 q1 gives each DC its own MAAS region with its own database, so the Office1 `juju-vr1-dc0` user and its API key do NOT authenticate against dc0's region -- a fact that reads as a network fault at bootstrap rather than as an auth failure. dc0's `juju-vr1-dc0` was therefore MINTED IN THE NEW REGION 2026-07-30 (superuser, mirroring the measured Office1 shape, which MAAS 3.7 requires for machine allocate/deploy) together with its 3-part API key and account password. Minted ON the region VM, never printed, moved host-to-host with all three sha256 digests compared, staging shredded. Consolidated to `~/vr1-dc0-creds/` on the jumphost (0600) and distributed to the dc0 RACK (0600 in a 0700 folder) because **D-138 moved the cloud-facing Juju client INTO the DC** -- so `rack` is now a credential-bearing host-role, which it had never been. dc1's is NOT YET MINTED (its region VM is authored, not applied). | 2026-07-30 Stage-5 Juju re-point; D-132 q1 + D-138; captures in `docs/audit/dc0-node-carve-20260730.txt` and this session's changelog | operator ("Mint juju-vr1-dc0 on the new region (Recommended)") | **OPEN -- rotation obligation + three notes.** (1) ROTATE at v1 close, or immediately if the rack or region VM is rebuilt or shared. (2) **BLAST RADIUS IS DC-LOCAL BY CONSTRUCTION and this is strictly BETTER than the alternative considered:** the rejected option was pointing juju at the region ADMIN key already consolidated on vcloud, which would have placed an admin-scoped credential on the rack -- the residency SEC-026 exists to constrain. This key administers dc0's region ONLY; no cross-DC reuse, per SEC-012/-016/-018/-019. (3) **`vm-secret-locations` gained its FIRST `rack` rows** (four, both DCs) -- definition-of-done for a new mint site, since an unlisted location is not audited (how SEC-022 happened). It also now covers the rack's `~/.local/share/juju/*` credential store, where juju keeps its own runtime copy. (4) dc1's four matrix rows are the FORWARD REGISTER and FAIL S2 by design until dc1's region exists -- that failure is D-137 working, and the rows must NOT be deleted to go green. **These two NEW S2 findings are NOT covered by the operator's 2026-07-30 P5 acceptance, which enumerated six and said so explicitly.** |
| SEC-029 | 2026-07-30 | **Octavia PKI overlay resident on the DC RACK.** The overlay carries the Octavia controller CERT and its PRIVATE KEY, and `juju deploy` consumes it at deploy time. D-138 moved the cloud-facing juju client INTO the DC, and the deploy CANNOT run from `voffice1` at all -- there is no L3 path from the headend to the controller API on the provider plane. dc0's overlay was therefore copied to the dc0 rack 2026-07-30 (`~/repo-stage/overlays/vr1-dc0-octavia-pki.yaml`, `0600`, sha256 compared end to end and equal). dc1's is NOT yet copied. | 2026-07-30 Stage-5 bundle deploy; D-138; this session's changelog | operator ("Copy dc0's PKI overlay to the dc0 rack (Recommended)") | **OPEN -- residency, not a new key.** (1) This is a COPY of material that already exists on the headend (SEC-004 `n-overlay-in-clone`), not a new mint, so the rotation obligation remains the existing `octavia-pki.sh reissue` path -- **and that path must now update BOTH copies, or the rack will deploy a superseded cert.** (2) Per-DC isolation holds BY CONSTRUCTION: each rack receives ONLY its own DC's overlay, never the pair; a cross-DC copy would destroy the independence that `octavia-pki.sh verify`'s A11 assertion exists to prove. (3) `~/repo-stage/` on the rack is a STAGING directory created because the rack has NO REPO CLONE -- the same D-138 tooling gap as queued finding F1. If a clone is later placed on DC client hosts, this path moves and the `vm-secret-locations` row must move with it. |
| SEC-030 | 2026-07-31 | **Local permission rules bypass the committed ask-gating -- ACCEPTED, KNOWN posture, recorded so it is explicit rather than implicit.** `.claude/settings.json` (committed team policy) deliberately puts `juju deploy`, `maas admin machine delete` and `maas admin * update/create/release` in **ask**, and the controller-destruction command in **deny**. `.claude/settings.local.json` (GITIGNORED, 286 allow rules accumulated across sessions, largely from interactive don't-ask-again approvals) contains `Bash(ssh *)`, `Bash(ssh voffice1 *)`, `Bash(ssh vr1-dc0-rack *)`, `Bash(ssh vr1-dc0-maas *)` and `Bash(python3 *)`. Because an `ssh <host> '<cmd>'` invocation matches on the OUTER ssh command, a destructive `maas admin machine delete` issued over ssh never reaches the ask rule; `python3 *` permits arbitrary execution outright. **CLAUDE.md hard rule 3 states the ask rules enforce per-mutation approval and are not to be worked around, so this is a real divergence between stated and enforced posture.** | 2026-07-31 permission carry-over review; continuation of `queued-findings-20260730-dc0-region-migration` F1 | operator ("Leave them as they are") | **OPEN -- ACCEPTED BY RULING, NOT AN OVERSIGHT.** (1) The operator was shown the five rules verbatim and the gating they bypass, and ruled to keep them: the mutation gate in practice is the operator's conversational approval of each presented command, not the rule engine. That has held all session. (2) **The compensating control is the PRESENTATION discipline** -- every mutation is still stated, justified as minimal, and approved before running. If that discipline lapses, nothing else catches it. (3) **NOT promoted to committed team policy** (ruling: "Promote a curated safe subset"): 36 read-only rules were promoted to `settings.json`; these five stay local-only so the bypass is NOT baked into team policy or carried to Roosevelt. (4) **`Read(//home/jessea123/**)` was deliberately EXCLUDED from promotion** -- it grants read of `~/vr1-*-creds/`, `~/vault-init/`, `~/as-executed/` and `~/tenant-*`, every one of which CLAUDE.md forbids reading into context. Promoting it would have put a secrets-read permission into team policy. (5) Four local wildcards were NARROWED before promotion because they permit mutation: `ip route *`, `ip neigh *`, `bridge fdb *`, `tc qdisc *` all allow add/del on the host running every DC node; only the `show`/`get` verbs were promoted. (6) A rebuild still loses the ~250 unpromoted local rules -- that half of F1 remains open by choice. |
| SEC-031 | 2026-08-02 | **The vr1-dc1 OPNsense edge is currently an OPEN ROUTER serving its management GUI to the simulated ISP.** The 2026-08-01 hard power cut (in-place tofu resize bouncing the containment VM) destroyed the edge's FreeBSD base-system user database -- `/etc/master.passwd` (`pwd_mkdb: line #1 too long`) and `/etc/group` (`pw: group 'wheel' does not exist`) -- so `Configuring firewall.....failed.` on every boot since and NO pf ruleset is in force. MEASURED against a control, which is what makes this a finding rather than a worry: from vcloud, the healthy office1 edge (same 26.7 image, same build path) answers WAN ICMP with 100% loss and its GUI with `000`; the dc1 edge answers WAN ICMP at **0% loss** and its GUI **200 in 0.014s**. It also forwards without translating -- simultaneous tcpdump on both taps shows the same packet leaving the WAN leg with source still `10.12.64.2`. Bounded by lab topology: the "ISP" is the `vr1-dc1-uplink` libvirt NAT net on vcloud, not the internet, and D-125 egress isolation is unaffected. Discovered by the read-only dc1 edge assessment, `docs/audit/dc1-edge-assessment-20260802.txt`. | docs/audit/dc1-edge-assessment-20260802.txt | operator | **CLOSED 2026-08-07 -- edge REBUILT, pf ruleset restored, exposure ended.** The stated closure condition (pf ruleset restored) is measurably met. Path taken was REBUILD not in-place repair (operator-directed to the proven dc0 procedure): tofu destroy+recreate the edge onto a fresh `opnsense-26.7-nano.qcow2` (edge-only plan, machine-asserted 2/0/2), D-112(c) console bootstrap (`scripts/opnsense-console-rebuild.py`), API mint, WAN-then-LAN addressing -> pf reload produced automatic outbound NAT. VERIFIED: `dc-egress-check dc1` **pass=8 fail=0**; the edge no longer serves its GUI on the WAN and the `.6` region reaches the internet (`ping 1.1.1.1` OK, `curl images.maas.io` 200). Procedure now a first-class runbook (`runbooks/dc-edge-rebuild.md`) + tool so it is not re-derived. Evidence: `docs/changelog-20260807-dc1-region-sequence.md` Item 4; `docs/audit/dc1-dhcp-handover-20260807.txt` sibling captures. Config.xml itself was INTACT (fsck names every inode it deletes; `/conf/config.xml` not among them) but the rebuild recreates the config via addressing regardless. |
| SEC-032 | 2026-08-02 | **dc0 edge credentials have been RESIDENT ON THE RACK, undisclosed, since 2026-07-20 -- the dc0 analog of SEC-015, which dc1 opened and remediated the same phase.** Found while staging the dc0 edge rebuild: `ls` (never a read) of `vr1-dc0-rack:~/vr1-dc0-creds/` shows `vr1-dc0-edge_ed25519` (the PRIVATE half, 432 B, 0600) and `opnsense-api.txt` (173 B, 0600), both dated 2026-07-20 -- i.e. left in place at the original edge build. dc1's build shredded both equivalents in the same phase and disclosed the transient as SEC-015; dc0's never did, and 13 days passed with nothing noticing. **THIS ALSO ANSWERS SEC-021(a), which asked for "a decision between RE-MINT and locating an off-jumphost copy": the off-jumphost copy is this one.** `creds-manifests/vm-secret-locations:123` already declares `rack vr1-dc0 ssh vr1-dc0-rack:~/vr1-dc0-creds/*` as an audited location, so this was visible to a `--remote` sweep and simply had not been reconciled -- the gap is reconciliation, not detection. **The 2026-07-20 `opnsense-api.txt` is now DEAD MATERIAL:** the rebuild destroyed the config.xml that held its hash, so it authenticates to nothing. It was deliberately NOT deleted (hard rule 1 -- a finding is logged, not actioned mid-step, and it is the physical evidence behind SEC-021(a)). The edge PRIVATE KEY was likewise left in place: `opnsense-bootstrap-apikey.sh` and `opnsense-set-interface-v4.sh` both SSH the edge DIRECT with no ProxyJump, and the edge LAN is reachable only from the rack, so rack residency is a structural requirement of the current tooling, not an oversight to be quietly cleaned up. **REMEDIATED IN PASSING:** SEC-021(a)'s access gap is closed -- a freshly minted key is now consolidated at `~/vr1-dc0-creds/opnsense-api.txt` on the jumphost (0600, 173 B, shape-verified 1x`key=`/1x`secret=` at 80 chars each, sha256-compared to the source, secret never printed). `creds-matrix.py` E1 for `dc0-edge-api` is consequently CLEAN where it previously read EXPECTED-BUT-ABSENT. | measured read-only during the dc0 edge rebuild, `docs/audit/dc0-edge-rebuild-20260802.txt`; SEC-015 (the dc1 precedent); SEC-021(a); `creds-manifests/vm-secret-locations:123` | operator | **OPEN -- three items, none actioned here.** (1) Rule on the dead 2026-07-20 `opnsense-api.txt` on the rack: shred (the dc1 precedent) or retain as SEC-021 evidence. (2) The `S2 vr1-dc0 EXPECTED-BUT-ABSENT: 'opnsense-api.txt'` finding is STILL RED and is now red for a DIFFERENT reason than when the operator accepted it on 2026-07-30 -- then the credential did not exist; now it exists and `creds-manifests/vr1-dc0.manifest` simply does not declare it. The manifest is DERIVED (`creds-matrix.py --render`), so this is a one-line regeneration, deliberately left to the operator rather than rendered inside a rebuild task. `creds-matrix.tsv:76`'s note key `n-dc0-edge-api-absent` and its `creds-matrix-notes.md:123` text are now factually stale for the same reason -- REPLACE the wording with the new invariant, never delete the row to go green (the note itself says so). (3) Decide whether rack-resident edge keys are the accepted steady state for BOTH DCs (making SEC-015's remediation the outlier) or whether the tooling should gain a ProxyJump path so the key never leaves the jumphost. Custody detail off-repo per D-069. |
| SEC-033 | 2026-08-04 | **The tls-certificates relation databag exposes vault's GLOBAL-CLIENT private key in cleartext to any juju model reader.** `juju show-unit <any tls-certificates consumer>` (measured identical on ovn-central/0, mysql-innodb-cluster/0, keystone/0) prints vault's `ca` + `client.cert` + `client.key` from the relation databag -- the last being the SHARED global-client PRIVATE key. Inherent to the interface-tls-certificates GLOBAL-CLIENT model (vault distributes one shared client key to every consumer over the relation); NOT newly introduced and NOT deploy-specific -- the same holds on any Charmed-OpenStack + vault deploy. Impact: the global client key is only as protected as juju model access; a read-only juju user can extract it. Discovered while reading the certificates relation databag during the ovn-central cert re-evaluation. | measured read-only 2026-08-04, `docs/audit/ovn-central-cert-reeval-20260803.md`; interface-tls-certificates global-client model | operator | OPEN -- interface/upstream-level, not a deploy defect. Mitigate via juju model RBAC (bound who holds admin/read on the model). Roosevelt analog: identical on every DC's cloud. Accept-or-mitigate is an operator ruling; nothing to action in the deploy itself. |
