Newer
Older
openstack-caracal-dc-dc / docs / D-137-implementation-plan.md

D-137 implementation plan -- credential matrix build spec

Status of this document. Build spec for D-137, ADOPTED 2026-07-26 with all five sub-rulings recorded. docs/design-decisions.md D-137 is the ruling authority; this file is the executable detail. Written 2026-07-26 so the build survives a fresh session -- the design originated in a plan file outside the repo, which the GA-R4 bookend path would not surface. Precedent: docs/D-068-vault-migration-plan-draft.md.

NOTHING IN HERE IS BUILT YET. No matrix file, no checker, no --render, no preflight Pn, no SEC-009 demotion.

Why (the short version)

creds-audit is DECLARATION-based: the manifest IS the declaration, so an undeclared secret is structurally invisible. It reported CLEAN on 2026-07-15 while four region-VM secrets minted 2026-07-13 sat undeclared; admin.pass surfaced 12 days later only because someone looked (SEC-020). The operator's insight closed the other half: a discovery sweep can never detect a credential that was never minted at all -- absence is invisible to discovery. Hence a forward EXPECTED-state register (the matrix), reconciled against measured state.

The five ruled decisions (authority: D-137 Status block)

  1. Enforcement = blocking in preflight. The check runs as a new Pn in scripts/preflight.sh and HARD-FAILS (exit 1) on expected-but-absent, undeclared, or per-DC-asymmetric credentials. No PreToolUse guard. Advisory was rejected as the posture that demonstrably failed.
  2. Manifests are DERIVED from the matrix. Matrix is single source; --render regenerates creds-manifests/*.manifest; the gauntlet FAILS if checked-in differs from rendered. ACCEPTED COST: manifests become generated, so their governance prose (SEC linkage, mint dates, off-manifest carve-outs, un-consolidated findings) must become MATRIX FIELDS or be regenerated into the rendered header -- it must not be dropped. That prose is load-bearing; vr1-dc1.manifest:18-20 is what recorded the dc0 backfill debt.
  3. --remote is bounded to declared locations. New creds-manifests/vm-secret-locations (<host-role> <path>); never walks outside it. No tenant surface, D-069 preserved. The list MUST include (faithful-implementation note in the ruling): per-site creds folders on the jumphost AND the headend (SEC-022 shadow stores), the region's /root/maas-secrets/ (SEC-020), and the three dirs outside the SEC-009 convention (the Vault init dir, the Octavia PKI dir, the per-tenant dirs -- capture FINDING 2), plus overlays/octavia-pki.yaml in the clone. Adding a row is DoD for any new mint site.
  4. D-137 is the policy authority. SEC-009's convention block demotes to a POINTER; the ledger returns to being purely an exposure/rotation register. The demotion MOVES the rule text -- SEC-009's founding history (the NetBox-token miss) stays as history.
  5. Identity folded in. Invariant: one identity serves one principal type. The principal column enforces it -- a credential serving both human and service is a FAIL. Absorbs the SEC-020 conflation question; no separate D-number.

Matrix schema (logical keys ONLY -- SEC-004, repo is currently PUBLIC)

creds-matrix.tsv, positional records in the manifests' idiom (do NOT invent a format language; scripts/site-ssh-config.sh:44-50 uses the same shape):

id | cardinality | site-key | host-role | filename | access-type | principal | mint-stage | mint-ref | sec-ref
  • cardinality: singleton | per-site | per-DC | per-node | per-tenant. REQUIRED for Roosevelt transfer -- without it the matrix cannot express "9 nodes".
  • site-key: region-qualified ONLY (vr1-dc0), bare dc0 REJECTED -- import the discipline at scripts/lib-net.sh:160-162 / lib-hosts.sh:153-155.
  • host-role: role token (jumphost, headend, rack, edge) -- NOT voffice1 or an IP. This still expresses "the copy is on the wrong role" without publishing topology.
  • access-type: gui | api | ssh | cli-profile | console | none.
  • principal: human | service. Ruling 5's invariant keys off this.
  • mint-ref: MUST admit THREE provenance kinds -- script:line, runbook:step, and operator-terminal. Capture FINDING 1: ssh-keygen returns ZERO hits repo-wide, so 12 declared secrets have NO mint command; those rows are the DEBT the matrix exists to surface and must be admitted, not treated as parse errors.
  • NO grants, hostnames, IPs, custody detail, or values (ruling: logical keys only).

Checker: scripts/creds-matrix.py

Model on scripts/provider-bundle-check.py -- the repo's canonical declarative gate: expectations as top-of-file constants keyed by a region-qualified slug, --dc selector with a safe default, oks/fails accumulators, [ok]/[FAIL] lines, ONE verdict line, return 1 if fails, and checks that SELF-SKIP with an explicit ok when a precondition is absent (:281) rather than silently passing -- needed for sites not yet deployed.

Import the BOTH-BOUNDS discipline from netbox/sandbox-fidelity-check.py:131-143, which records the identical false-green in-repo: "EXPECTED_NEW was an upper bound masquerading as an assertion. It must be BOTH bounds." A credential matrix has the same failure mode one level up: enumerate no sites -> nothing undeclared -> false CLEAN. Reuse its four-way drift vocabulary -- MISSING / UNEXPECTED-EXTRA / EXPECTED-BUT-ABSENT / FIELD DRIFT. Provenance drift is a FIELD mismatch, not a presence mismatch.

Three tiers, increasing cost

  1. STATIC (offline, gauntlet): matrix parses; every row has a manifest row; rendered manifests == checked-in (ruling 2); every script:line/runbook:step mint-ref resolves to a real location; per-DC rows are SYMMETRIC; principal invariant (ruling 5).
  2. EXISTENCE (local + remote, preflight Pn): every expected credential present at the expected host-role with expected mode; nothing undeclared at any declared location INCLUDING remote ones (ruling 3).
  3. VALIDITY (stage close, --probe): provenance digests match across hosts; per-row behavioral probes where defined. Motivated by: creds-audit PARSES provenance and NEVER verifies it (fsrc used only in the MISSING message), so the SEC-020 stale-trap warning is unchecked today.

Hard constraints inherited

  • Metadata only, enforced by source-grep. tests/creds-audit/run-tests.sh:71-73 fails on any cat|head|tail|less|more|od|xxd|hexdump in the audit source. Remote provenance verification MUST compare sha256sum digests over ssh -- never transfer content. A new checker should carry the same guard test.
  • Gauntlet registration is pure glob over tests/*/run-tests.sh (scripts/run-tests-all.sh:22); only tail -1 is shown, so the LAST line must self-describe. Failure-detail excerpting matches ^\s*(FAIL|\[XX\]|MISS|LEAK|COUNT) -- it does NOT match [FAIL], which is why creds-audit failures currently surface with no detail. Lead detail lines with bare FAIL.
  • preflight aggregation is note $? worst-exit (1 dominates 2 dominates 0); add one banner + one call honouring the 0/1/2 contract.
  • repo-lint L1: new design-decisions.md text must be pure ASCII. L10: any commit touching docs/audit/, a **Status: line, or a gate row must touch CURRENT-STATE in the SAME commit.
  • Harness style: mutation-of-a-real-artifact fixtures + rc AND output-regex assertions, one case per invariant, each named for the real defect it encodes (tests/provider-bundle-check/run-tests.sh).

Files

NEW: creds-matrix.tsv, creds-manifests/vm-secret-locations, scripts/creds-matrix.py, tests/creds-matrix/run-tests.sh. MODIFY: scripts/preflight.sh (new Pn), scripts/creds-audit.sh (--all, --remote, VERIFY provenance, widen sprawl globs -- currently miss admin.pass, *.apikey, *.key, *.pem, *_ed25519, admin-openrc), creds-manifests/*.manifest (become generated), docs/security-ledger.md (SEC-009 demotion per ruling 4), docs/CURRENT-STATE.md (C1).

Verification / acceptance

  • bash tests/creds-matrix/run-tests.sh -- every invariant FAILS when violated.
  • ACCEPTANCE TEST: scripts/creds-matrix.py --all must reproduce SEC-021/-022/-023 and the openrc case as NAMED failures on today's tree. A checker that goes green on the current tree is wrong.
  • bash scripts/run-tests-all.sh ALL GREEN; bash scripts/repo-lint.sh 0-fail; bash scripts/preflight.sh exercises the new Pn.

EXPECTED RED BY DESIGN -- do not "fix" by weakening the check

The first full run SHOULD fail, and these are the defects, not bugs in the checker:

  • admin serves a human GUI row AND a service api row -> ruling-5 invariant FAIL. Remediation is the operator/admin split already begun 2026-07-25, i.e. migrate the 19 maas admin call sites to a service identity.
  • 12 rows carry mint-ref=operator-terminal -> reproducibility debt to convert.
  • SEC-021 (dc0 opnsense-api.txt ABSENT; power-key name/host divergence), SEC-022 (headend shadow stores), SEC-023 (sprawl blind spots + predicted ~/admin-openrc).

Going green is a REMEDIATION project separate from the build. Do not scope-creep it into the build; land the checker red, record the failures, then gate remediation per row.

Fresh-session start

  1. Normal bootstrap: git pull, bash scripts/repo-lint.sh, read docs/CURRENT-STATE.md in full, then docs/session-ledger.md + bash scripts/ledger-scan.sh.
  2. Read docs/design-decisions.md D-137 (the ruling authority -- all five sub-rulings and their verbatim operator selections), then this file.
  3. Read the research capture docs/audit/creds-creation-points-20260725.md -- it is the matrix SEED (55 MINT sites classified by host, destination, stage, human/service).
  4. Build tier 1 first (offline, no live access needed), harness it, then tier 2, then tier 3.