# 2026-07-15 -- creds-audit: enforce per-site credential consolidation (SEC-009), so the DC0/DC1 spread cannot recur

## Why

SEC-009 established the convention "ALL of a site's secrets live in `~/<site>-creds/`
(0700, files 0600), no loose env files in `~`." But nothing ENFORCED it, and it had
two blind spots that a checklist alone would not catch:

1. **VM-minted secrets.** A secret generated ON a site VM (office1-netbox's sandbox
   NetBox token at `/root/netbox-secrets/api.token`) has no forcing function to get
   pulled into the jumphost folder. It went un-consolidated until the C2 fidelity
   re-check needed it (SEC-009 addendum, 2026-07-15).
2. **Silent sprawl.** A loose `*.env`/`*.token` in `~` outside a `*-creds/` folder --
   the ORIGINAL SEC-009 exposure -- had no standing detector.

Standing up DC0 and DC1 multiplies both (each mints MAAS API keys, per-DC NetBox
tokens, node/edge SSH keys, OPNsense creds, tailscale authkeys on multiple VMs). The
operator asked directly how we stop the spread recurring for the DCs. Answer: a
manifest + an audit + a mint-time runbook step.

## What was built

- **`creds-manifests/<site>.manifest`** -- declares EXACTLY the secrets a site must
  hold: `<filename> <octal-mode> <source>`, where source is `local` or `<vm>:<path>`
  for VM-minted secrets (making the easy-to-miss class explicit and provenance-tracked).
  `creds-manifests/vr1-office1.manifest` seeded with 11 entries from the live folder;
  it audits CLEAN.
- **`scripts/creds-audit.sh <site>`** -- verifies the live `~/<site>-creds/` against
  the manifest: folder is 0700, every entry present with its declared mode and
  non-empty; flags any UNDECLARED file in the folder; and scans the home root for
  SPRAWL (`*.env`/`*.token`/`*authkey*`/`*appcred*`/`*-cred*.txt` loose outside any
  `*-creds/`). **Reads NO secret values -- metadata only** (stat/size), verified by a
  harness assertion (T7: no cat/head/tail/od/xxd in the script). Env overrides
  `CREDS_ROOT`/`MANIFEST_DIR` make it fully offline-testable.
- **`tests/creds-audit/run-tests.sh`** (7/7) -- synthetic folders under a tmp root:
  complete+correct -> CLEAN; missing (VM-sourced) entry -> fail; wrong mode -> fail;
  undeclared file -> fail; home-root sprawl -> fail; non-0700 folder -> fail;
  metadata-only. Auto-discovered by the gauntlet.
- **Runbook discipline** -- `runbooks/dc-dc-phase3-maas-enlist-deploy.md` GATE now
  carries a non-negotiable close-out: consolidate + manifest each secret AT MINT TIME,
  and `bash scripts/creds-audit.sh $DC` MUST be CLEAN before a DC is declared done.
- **`docs/security-ledger.md`** -- SEC-009 ENFORCEMENT note recording the above.

## Verification

- Harness: `tests/creds-audit/run-tests.sh` -> 7/7 PASS.
- Live: `bash scripts/creds-audit.sh vr1-office1` -> CLEAN (folder 0700, 11/11 entries
  present + correct + non-empty, no undeclared files, no sprawl). This also CONFIRMS
  the SEC-009 sandbox-token gap is now closed and tool-verified.
- Gauntlet ALL GREEN; repo-lint 0 fail.

## Revert

- `git rm scripts/creds-audit.sh tests/creds-audit/run-tests.sh creds-manifests/vr1-office1.manifest docs/changelog-20260715-creds-audit.md`
  (remove `creds-manifests/` if now empty).
- `git checkout HEAD -- docs/security-ledger.md runbooks/dc-dc-phase3-maas-enlist-deploy.md`
  reverts the SEC-009 enforcement note and the runbook close-out.

Nothing here reads, moves, or mutates any secret; the audit is read-only metadata
inspection and the manifest holds only filenames/modes/provenance.
