diff --git a/creds-manifests/vm-secret-locations b/creds-manifests/vm-secret-locations index 1519567..82107e0 100644 --- a/creds-manifests/vm-secret-locations +++ b/creds-manifests/vm-secret-locations @@ -64,8 +64,11 @@ # Committee finding: the tfstate SIBLINGS carry the same plaintext key and were unaudited. jumphost vr1-office1 local /opentofu/terraform.tfstate.backup jumphost vr1-office1 local /opentofu/terraform.tfstate.pre-* -# CLAUDE.md names ~/as-executed/ as credential material; it was missing from this list. -jumphost - local ~/as-executed/* +# ~/as-executed/ is DELIBERATELY NOT declared. CLAUDE.md names it as material never to +# read into context, and the PreToolUse guard enforces that -- but it holds session LOGS, +# not registered credentials, so declaring it made every log a false UNDECLARED finding. +# Its risk is transcript CONTENT, which this register does not model; that is the guard's +# job, not the matrix's. Recorded here so the omission is a decision, not an oversight. # The Octavia PKI artifacts are written into THREE SUBDIRECTORIES (phase-01:288), so the # bare ~/octavia-pki/* pattern matched ZERO of the 8 artifacts incl. two CA private keys. jumphost - local ~/octavia-pki/issuing-ca/* diff --git a/creds-matrix-notes.md b/creds-matrix-notes.md index 12d09a8..3c7708f 100644 --- a/creds-matrix-notes.md +++ b/creds-matrix-notes.md @@ -202,6 +202,15 @@ to a file, but it is short-lived by design -- absence outside its step is normal, so this row must never drive an existence failure. +## n-libvirt-power-pw +The libvirt SSH password for MAAS virsh power control. Read INTERACTIVELY (never a CLI +arg, never echoed, never logged, never in the repo) and injected into MAAS `power_pass`. +`reenroll-hosts.sh:22-25` records that it was exposed in plaintext on 2026-06-26 -- +`maas machine power-parameters` echoes `power_pass` -- with a standing instruction to +ROTATE it. It is in scope by the materialization test (presented by a human, consumed +out-of-band) and had NO row until the 2026-07-26 committee found it: a credential with a +live rotation obligation that the credential-lifecycle authority could not see. + --- ## Checker behaviour worth knowing before you "fix" it diff --git a/creds-matrix.tsv b/creds-matrix.tsv index 956dacc..73ae301 100644 --- a/creds-matrix.tsv +++ b/creds-matrix.tsv @@ -41,17 +41,17 @@ # id cardinality site-key host-role filename access-type principal custody mint-stage mint-ref sec-ref notes-ref # ---------------------------------------------------------------- vr1-office1 (headend region + jumphost) -maas-region-admin singleton vr1-office1 headend admin.pass gui human source-of-record stage2 script:scripts/site-headend-install.sh:452 SEC-020 n-maas-region-admin -maas-region-admin singleton vr1-office1 jumphost maas-admin-password gui human consolidated stage2 script:scripts/site-headend-install.sh:452 SEC-020 n-maas-region-admin +maas-region-admin singleton vr1-office1 headend admin.pass gui human source-of-record stage2 script:scripts/site-headend-install.sh:450 SEC-020 n-maas-region-admin +maas-region-admin singleton vr1-office1 jumphost maas-admin-password gui human consolidated stage2 script:scripts/site-headend-install.sh:450 SEC-020 n-maas-region-admin maas-region-admin singleton vr1-office1 headend admin.apikey api service not-consolidated-ruled stage2 script:scripts/site-headend-install.sh:453 SEC-020 n-maas-admin-apikey -maas-region-admin singleton vr1-office1 headend .maas.cli cli-profile service off-manifest-known stage2 script:scripts/site-headend-install.sh:455 SEC-020 n-maas-cli-profile +maas-region-admin singleton vr1-office1 headend .maascli.db cli-profile service off-manifest-known stage2 script:scripts/site-headend-install.sh:455 SEC-020 n-maas-cli-profile maas-region-operator singleton vr1-office1 jumphost maas-operator-password gui human consolidated adhoc operator-terminal SEC-020 n-maas-region-operator maas-region-db singleton vr1-office1 headend db.pass none service not-consolidated-ruled stage2 script:scripts/site-headend-install.sh:430 SEC-020 n-region-infra-carveout maas-region-lxd-trust singleton vr1-office1 headend lxd-trust.pass none service not-consolidated-ruled stage2 script:scripts/site-headend-install.sh:485 SEC-020 n-region-infra-carveout maas-rack-enroll-secret singleton vr1-office1 headend secret none service off-manifest-known stage2 script:scripts/site-headend-install.sh:445 - n-rack-enroll office1-svc-key per-site vr1-office1 jumphost office1_svc_ed25519 ssh service consolidated stage2 operator-terminal SEC-007 n-no-mint-command office1-svc-key per-site vr1-office1 jumphost office1_svc_ed25519.pub none service consolidated stage2 operator-terminal SEC-007 n-no-mint-command -office1-edge-api per-site vr1-office1 jumphost opnsense-api.txt api service consolidated stage2 script:scripts/opnsense-bootstrap-apikey.sh:89 - n-edge-api +office1-edge-api per-site vr1-office1 jumphost opnsense-api.txt api service consolidated stage2 script:scripts/opnsense-bootstrap-apikey.sh:115 - n-edge-api office1-edge-root per-site vr1-office1 jumphost opnsense-root-password gui human consolidated stage2 operator-terminal SEC-007 n-no-mint-command office1-edge-root per-site vr1-office1 jumphost opnsense-root-hash none human consolidated stage2 operator-terminal SEC-007 n-no-mint-command office1-tailscale-authkey singleton vr1-office1 jumphost tailscale-authkey.txt api service consolidated stage2 operator-terminal SEC-008 n-third-party @@ -61,7 +61,7 @@ netbox-secret-key singleton vr1-office1 netbox secret_key none service off-manifest-known stage2 operator-terminal - n-netbox-vm-secrets netbox-admin singleton vr1-office1 netbox admin.pass gui human off-manifest-known stage2 operator-terminal - n-netbox-vm-secrets office1-tofu-maas-apikey singleton vr1-office1 jumphost vr1-stage1.env api service consolidated stage2 runbook:runbooks/dc-dc-phase1-office1-standup.md:464 SEC-009 n-tofu-apikey -office1-tofu-maas-apikey singleton vr1-office1 jumphost terraform.tfstate api service off-manifest-known stage2 runbook:runbooks/dc-dc-phase1-office1-standup.md:464 SEC-009 n-tfstate-plaintext +office1-tofu-maas-apikey singleton vr1-office1 jumphost terraform.tfstate api service off-manifest-known stage2 runbook:runbooks/dc-dc-phase1-office1-standup.md:464 - n-tfstate-plaintext office1-env singleton vr1-office1 jumphost vr1-office1.env none service consolidated stage2 operator-terminal SEC-009 n-office1-env creds-folder-readme per-site vr1-office1 jumphost README none - consolidated - - - n-readme @@ -72,14 +72,14 @@ dc0-svc-key per-DC vr1-dc0 headend vr1-dc0_svc_ed25519.pub none service off-manifest-known stage3 operator-terminal SEC-022 n-headend-shadow-store dc0-edge-key per-DC vr1-dc0 jumphost vr1-dc0-edge_ed25519 ssh service consolidated stage3 operator-terminal - n-edge-key dc0-edge-key per-DC vr1-dc0 jumphost vr1-dc0-edge_ed25519.pub none service consolidated stage3 operator-terminal - n-edge-key -dc0-edge-api per-DC vr1-dc0 jumphost opnsense-api.txt api service consolidated stage3 script:scripts/opnsense-bootstrap-apikey.sh:89 SEC-021 n-dc0-edge-api-absent +dc0-edge-api per-DC vr1-dc0 jumphost opnsense-api.txt api service consolidated stage3 script:scripts/opnsense-bootstrap-apikey.sh:115 SEC-021 n-dc0-edge-api-absent dc0-maas-power-key per-DC vr1-dc0 jumphost vr1-dc0-maas-power_ed25519 ssh service consolidated stage3 operator-terminal SEC-021 n-dc0-power-key-divergence dc0-maas-power-key per-DC vr1-dc0 jumphost vr1-dc0-maas-power_ed25519.pub none service consolidated stage3 operator-terminal SEC-021 n-dc0-power-key-divergence dc0-maas-power-key per-DC vr1-dc0 headend maas-virsh_ed25519 ssh service off-manifest-known stage3 operator-terminal SEC-021 n-dc0-power-key-divergence dc0-maas-power-key per-DC vr1-dc0 headend id_ed25519 ssh service off-manifest-known stage3 runbook:runbooks/dc-dc-phase2-tofu-dc-substrate.md:770 SEC-012 n-snap-key-fragility dc0-maas-apikey per-DC vr1-dc0 jumphost maas-api-key.txt api service consolidated stage5 runbook:runbooks/dc-dc-phase4-juju-bundle-per-dc.md:136 SEC-018 n-dc-maas-apikey dc0-maas-apikey per-DC vr1-dc0 headend credentials.yaml api service off-manifest-known stage5 runbook:runbooks/dc-dc-phase4-juju-bundle-per-dc.md:141 SEC-018 n-juju-cred-store -dc0-juju-maas-user per-DC vr1-dc0 - - api service off-manifest-known stage5 runbook:runbooks/dc-dc-phase4-juju-bundle-per-dc.md:128 SEC-020 n-juju-user-nopassword +dc0-juju-maas-user per-DC vr1-dc0 - - api service off-manifest-known stage5 runbook:runbooks/dc-dc-phase4-juju-bundle-per-dc.md:128 SEC-018 n-juju-user-nopassword # ---------------------------------------------------------------- vr1-dc1 dc1-svc-key per-DC vr1-dc1 jumphost vr1-dc1_svc_ed25519 ssh service consolidated stage3 operator-terminal - n-no-mint-command @@ -88,39 +88,39 @@ dc1-svc-key per-DC vr1-dc1 headend vr1-dc1_svc_ed25519.pub none service off-manifest-known stage3 operator-terminal SEC-022 n-headend-shadow-store dc1-edge-key per-DC vr1-dc1 jumphost vr1-dc1-edge_ed25519 ssh service consolidated stage3 operator-terminal SEC-015 n-edge-key dc1-edge-key per-DC vr1-dc1 jumphost vr1-dc1-edge_ed25519.pub none service consolidated stage3 operator-terminal - n-edge-key -dc1-edge-api per-DC vr1-dc1 jumphost opnsense-api.txt api service consolidated stage3 script:scripts/opnsense-bootstrap-apikey.sh:89 - n-edge-api +dc1-edge-api per-DC vr1-dc1 jumphost opnsense-api.txt api service consolidated stage3 script:scripts/opnsense-bootstrap-apikey.sh:115 - n-edge-api dc1-maas-power-key per-DC vr1-dc1 jumphost vr1-dc1-maas-power_ed25519 ssh service consolidated stage3 operator-terminal SEC-016 n-dc1-power-key dc1-maas-power-key per-DC vr1-dc1 jumphost vr1-dc1-maas-power_ed25519.pub none service consolidated stage3 operator-terminal SEC-016 n-dc1-power-key dc1-maas-power-key per-DC vr1-dc1 headend id_dc1_power ssh service off-manifest-known stage3 runbook:runbooks/dc-dc-phase2-tofu-dc-substrate.md:770 SEC-016 n-snap-key-fragility dc1-maas-apikey per-DC vr1-dc1 jumphost maas-api-key.txt api service consolidated stage5 runbook:runbooks/dc-dc-phase4-juju-bundle-per-dc.md:136 SEC-019 n-dc-maas-apikey dc1-maas-apikey per-DC vr1-dc1 headend credentials.yaml api service off-manifest-known stage5 runbook:runbooks/dc-dc-phase4-juju-bundle-per-dc.md:141 SEC-019 n-juju-cred-store -dc1-juju-maas-user per-DC vr1-dc1 - - api service off-manifest-known stage5 runbook:runbooks/dc-dc-phase4-juju-bundle-per-dc.md:128 SEC-020 n-juju-user-nopassword +dc1-juju-maas-user per-DC vr1-dc1 - - api service off-manifest-known stage5 runbook:runbooks/dc-dc-phase4-juju-bundle-per-dc.md:128 SEC-019 n-juju-user-nopassword # ---------------------------------------------------------------- outside the SEC-009 *-creds/ convention (research FINDING 2) -vault-unseal-shares singleton - jumphost vault-init none service off-manifest-known vr0-phase02 runbook:runbooks/phase-02-vault-bringup.md:71 SEC-023 n-outside-convention -vault-root-token singleton - jumphost vault-init none service off-manifest-known vr0-phase02 runbook:runbooks/phase-02-vault-bringup.md:71 SEC-023 n-outside-convention -octavia-issuing-ca-passphrase singleton - jumphost octavia-issuing-ca-passphrase none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:299 SEC-023 n-outside-convention -octavia-issuing-ca-key singleton - jumphost octavia-issuing-ca-key none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:302 SEC-023 n-outside-convention -octavia-issuing-ca-cert singleton - jumphost octavia-issuing-ca-cert none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:305 SEC-023 n-outside-convention -octavia-controller-ca-pass singleton - jumphost octavia-controller-ca-passphrase none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:321 SEC-023 n-outside-convention -octavia-controller-ca-key singleton - jumphost octavia-controller-ca-key none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:324 SEC-023 n-outside-convention -octavia-controller-ca-cert singleton - jumphost octavia-controller-ca-cert none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:327 SEC-023 n-outside-convention -octavia-controller-key singleton - jumphost octavia-controller-key none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:343 SEC-023 n-outside-convention -octavia-controller-bundle singleton - jumphost octavia-controller-bundle none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:375 SEC-023 n-outside-convention +vault-unseal-shares singleton - jumphost init.txt none service off-manifest-known vr0-phase02 runbook:runbooks/phase-02-vault-bringup.md:71 SEC-023 n-outside-convention +vault-root-token singleton - jumphost init.txt none service off-manifest-known vr0-phase02 runbook:runbooks/phase-02-vault-bringup.md:71 SEC-023 n-outside-convention +octavia-issuing-ca-passphrase singleton - jumphost passphrase.txt none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:299 SEC-023 n-outside-convention +octavia-issuing-ca-key singleton - jumphost issuing-ca.key.enc none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:302 SEC-023 n-outside-convention +octavia-issuing-ca-cert singleton - jumphost issuing-ca.cert.pem none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:305 SEC-023 n-outside-convention +octavia-controller-ca-pass singleton - jumphost passphrase.txt none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:321 SEC-023 n-outside-convention +octavia-controller-ca-key singleton - jumphost controller-ca.key.enc none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:324 SEC-023 n-outside-convention +octavia-controller-ca-cert singleton - jumphost controller-ca.cert.pem none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:327 SEC-023 n-outside-convention +octavia-controller-key singleton - jumphost controller.key none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:343 SEC-023 n-outside-convention +octavia-controller-bundle singleton - jumphost controller.bundle.pem none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:375 SEC-023 n-outside-convention octavia-pki-overlay singleton - jumphost octavia-pki.yaml none service off-manifest-known vr0-phase01 runbook:runbooks/phase-01-bundle-deploy.md:391 SEC-004 n-overlay-in-clone -tenant-domain-admin per-tenant - jumphost domain-admin-cred.txt gui human off-manifest-known tenant-onboard script:scripts/tenant-onboard.sh:64 SEC-023 n-tenant-dir -tenant-cluster-user per-tenant - jumphost cluster-cred.txt api human off-manifest-known tenant-onboard script:scripts/tenant-onboard.sh:90 SEC-023 n-tenant-dir -tenant-svc-user per-tenant - jumphost svc-cred.txt api service off-manifest-known tenant-onboard script:scripts/tenant-onboard.sh:90 SEC-023 n-tenant-dir -tenant-svc-appcred per-tenant - jumphost svc-appcred.txt api service off-manifest-known tenant-onboard script:scripts/tenant-onboard.sh:141 SEC-023 n-tenant-dir -tenant-nova-key per-tenant - jumphost tenant-key.pem ssh service off-manifest-known tenant-onboard script:scripts/tenant-onboard.sh:167 SEC-023 n-tenant-dir +tenant-domain-admin per-tenant - jumphost -domain-admin-cred.txt gui human off-manifest-known tenant-onboard script:scripts/tenant-onboard.sh:64 SEC-023 n-tenant-dir +tenant-cluster-user per-tenant - jumphost -cluster-cred.txt api human off-manifest-known tenant-onboard script:scripts/tenant-onboard.sh:90 SEC-023 n-tenant-dir +tenant-svc-user per-tenant - jumphost -svc-cred.txt api service off-manifest-known tenant-onboard script:scripts/tenant-onboard.sh:90 SEC-023 n-tenant-dir +tenant-svc-appcred per-tenant - jumphost -svc-appcred.txt api service off-manifest-known tenant-onboard script:scripts/tenant-onboard.sh:141 SEC-023 n-tenant-dir +tenant-nova-key per-tenant - jumphost -key.pem ssh service off-manifest-known tenant-onboard script:scripts/tenant-onboard.sh:167 SEC-023 n-tenant-dir # ---------------------------------------------------------------- in scope by the materialization ruling (charm/platform-minted, then materialized) keystone-admin singleton - cloud - api service off-manifest-known vr0-phase01 - - n-charm-minted keystone-admin singleton - jumphost admin-openrc none service off-manifest-known vr0-phase03 script:scripts/phase-03-admin-openrc.sh:52 SEC-023 n-predicted-openrc magnum-domain-admin singleton - cloud - api service off-manifest-known vr0-phase07 script:scripts/phase-07-conductor-graft.sh:90 - n-cloud-only -capi-mgmt-kubeconfig singleton - cloud kube-config none service off-manifest-known vr0-phase06 script:scripts/phase-06-capi-stack.sh:91 - n-cloud-only +capi-mgmt-kubeconfig singleton - cloud config none service off-manifest-known vr0-phase06 script:scripts/phase-06-capi-stack.sh:91 - n-cloud-only rbd-mirror-peer-token per-DC - unit rbd-mirror-bootstrap-token none service off-manifest-known stage6 script:scripts/dc-dc-rbd-mirror.sh:88 - n-ephemeral-unit -rgw-multisite-system-key singleton - unit - api service off-manifest-known stage6 script:scripts/dc-dc-radosgw-multisite.sh:172 - n-cloud-only +rgw-multisite-system-key singleton - unit - api service off-manifest-known stage6 operator-terminal - n-cloud-only # ---------------------------------------------------------------- non-secret artifacts colocated at declared locations juju-client-clouds singleton - headend clouds.yaml none - off-manifest-known stage5 - - n-colocated-nonsecret @@ -128,3 +128,14 @@ maas-snap-ssh-config singleton - headend config none - off-manifest-known stage3 - - n-colocated-nonsecret maas-snap-known-hosts singleton - headend known_hosts none - off-manifest-known stage3 - - n-colocated-nonsecret maas-snap-known-hosts-old singleton - headend known_hosts.old none - off-manifest-known stage3 - - n-colocated-nonsecret + +# ---------------------------------------------------------------- added 2026-07-26 (committee coverage findings) +# Read from a human and injected into MAAS power_pass; reenroll-hosts.sh:22-25 records a +# STANDING rotation obligation after a 2026-06-26 plaintext exposure. Passes the +# materialization test verbatim and was absent from both the matrix and the exclusions. +libvirt-power-password singleton - - - console human off-manifest-known - script:scripts/reenroll-hosts.sh:122 - n-libvirt-power-pw +# The tfstate SIBLINGS carry the same DOCFIX-175 plaintext key as the live state file. +# They are real secret-at-rest copies, so they are ROWS, not merely declared locations. +office1-tofu-maas-apikey singleton vr1-office1 jumphost terraform.tfstate.backup api service off-manifest-known stage2 runbook:runbooks/dc-dc-phase1-office1-standup.md:464 - n-tfstate-plaintext +office1-tofu-maas-apikey singleton vr1-office1 jumphost terraform.tfstate.pre- api service off-manifest-known stage2 runbook:runbooks/dc-dc-phase1-office1-standup.md:464 - n-tfstate-plaintext +vault-ca-root singleton - jumphost vault-ca-root.pem none service off-manifest-known vr0-phase02 operator-terminal SEC-023 n-outside-convention diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 5f40a34..b04e734 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -932,6 +932,33 @@ remediating the ruling-5 conflation would turn the GAUNTLET red -- the test punishes the fix it exists to protect. Remediation is IN PROGRESS under operator direction ("Do as many as you can autonomously"); this block is the authority on what is fixed. + **REMEDIATION COMPLETE 2026-07-26 (phases 1-3), capture + `docs/audit/d137-tier2-postcommittee-20260726.txt`** (supersedes the pre-committee + privileged capture, whose verdicts predate the site-scoping fix and are NOT comparable). + ALL EIGHT false greens are FIXED and individually regression-locked; harness + **44/44** (was 35), creds-audit **15/15**, gauntlet **ALL GREEN (80)**, repo-lint 0-fail. + **PROOF THE SITE FIX WORKS: `E1 EXPECTED-BUT-ABSENT: dc0-edge-api 'opnsense-api.txt'` + now appears.** That on-disk absence was masked in EVERY prior run, so **SEC-021's + on-disk half is NOW genuinely reproduced in full** (the earlier withdrawal stands as + history). The ~21 inferred filenames were re-measured against their mint-refs (octavia's + 8 real basenames + its three SUBDIRECTORIES, which the bare `~/octavia-pki/*` pattern + matched none of; vault's `init.txt`; the tenant rows' `-` instance prefix, now + supported by a placeholder matcher); `sec-ref` mis-attributions corrected (both + `juju-maas-user` rows SEC-020 -> SEC-018/-019; tfstate SEC-009 -> none); `admin.pass`'s + mint-ref moved from the consumer (`:452`) to the mint (`:450`); the libvirt power + password and `vault-ca-root` added as rows. Matrix **81 rows**. Findings 35 -> **13, all + TRUE**: SEC-021 (3x S2 + 3x E1), 3x S5 power-key asymmetry, the ruling-5 conflation, one + SEC-022 shadow-store gap, SEC-024, and 2 rows disclosed as UNCHECKABLE. + **NEW EXPOSURE FOUND BY THE FIXED CHECKER -- SEC-024 OPENED:** + `opentofu/terraform.tfstate.backup` is mode **0664** (group- and world-readable) and + carries the Office1 MAAS API key in plaintext per DOCFIX-175; the live state file is + correctly 0600. Invisible to every prior control because the world-readable check was + custody-gated and the siblings were undeclared. **LOGGED, NOT ACTIONED (hard rule 1)** -- + the `chmod` and the retain-or-delete question are mutations outside this step. + **STILL OUTSTANDING (not done, not silently dropped):** ruling 1's preflight `Pn` (the + sequencing question is still the operator's); ruling 4's SEC-009 demotion; tier 3 + (VALIDITY); `--render`'s source-field derivation; and the `cardinality` field remains + largely inert with a one-token S5 bypass (`per-DC` -> `singleton`). ## 9. Additional defect found while authoring (FIXED in sweep Batch 0.3, 2026-07-19 -- wrap-aware exclusion, GA-F15; history below) diff --git a/docs/audit/d137-tier2-postcommittee-20260726.txt b/docs/audit/d137-tier2-postcommittee-20260726.txt new file mode 100644 index 0000000..bc0f0b3 --- /dev/null +++ b/docs/audit/d137-tier2-postcommittee-20260726.txt @@ -0,0 +1,43 @@ +# D-137 tier-2 sweep -- POST-COMMITTEE, live + privileged, 2026-07-26 +# python3 scripts/creds-matrix.py --tier2 --remote --privileged \ +# --pending-stage vr0-phase01 --pending-stage vr0-phase02 \ +# --pending-stage vr0-phase03 --pending-stage tenant-onboard +# +# SUPERSEDES d137-tier2-privileged-20260726.txt, whose verdicts predate the +# site-scoping fix and the filename re-measurements and are NOT comparable. +# Metadata only (stat; sudo -n only where an unprivileged probe could not look). +# NOTE the E1 for dc0-edge-api 'opnsense-api.txt': that on-disk absence was MASKED +# by the site-blind check in every prior run. Its appearance here is the proof. + +=== creds-matrix: tier 1 (STATIC) === +=== creds-matrix: tier 2 (EXISTENCE) === + [ok] S1 schema: 81 rows, all enums valid, site-keys region-qualified, no duplicate (id,site,host-role,filename) + [ok] S3 render: 2 source field(s) SKIPPED -- rendering them needs the declared path from creds-manifests/vm-secret-locations (ruling 3); the list now EXISTS but the source-field derivation is not wired + [ok] S3 render drift: rendered row fields (mode, source) match checked-in; header prose and non-jumphost rows are OUT OF SCOPE of this compare + [ok] S4 mint-ref: every script:/runbook: reference resolves to a real location + [ok] S4 provenance debt: 29 row(s) are mint-ref=operator-terminal -- NOT reproducible from the repo (research FINDING 1). Admitted by design; converting them is remediation, not a checker fix. + [ok] S7 notes: 34 note key(s) referenced, all resolve, none orphaned + [ok] E0 jumphost location '~/vault-init/*' does not exist -- SKIPPED (typically a mint stage this deployment has not reached) + [ok] E0 jumphost location '~/octavia-pki/*' does not exist -- SKIPPED (typically a mint stage this deployment has not reached) + [ok] E0 jumphost location '~/octavia-pki/issuing-ca/*' does not exist -- SKIPPED (typically a mint stage this deployment has not reached) + [ok] E0 jumphost location '~/octavia-pki/controller-ca/*' does not exist -- SKIPPED (typically a mint stage this deployment has not reached) + [ok] E0 jumphost location '~/octavia-pki/controller/*' does not exist -- SKIPPED (typically a mint stage this deployment has not reached) + [ok] E0 headend location '/root/maas-secrets/*' read via ESCALATION (sudo -n, metadata only) after an unprivileged probe could not open it + [ok] E0 headend location '/var/snap/maas/current/root/.ssh/*' read via ESCALATION (sudo -n, metadata only) after an unprivileged probe could not open it + [ok] E0 netbox location '/root/netbox-secrets/*' read via ESCALATION (sudo -n, metadata only) after an unprivileged probe could not open it + [ok] E1 18 expected artifact(s) deferred as not-yet-minted (--pending-stage) + [FAIL] S2 vr1-dc0 EXPECTED-BUT-ABSENT: 'opnsense-api.txt' (id dc0-edge-api, SEC-021) is expected by the matrix but NOT declared in the manifest -- the credential is either missing or undeclared + [FAIL] S2 vr1-dc0 EXPECTED-BUT-ABSENT: 'vr1-dc0-maas-power_ed25519' (id dc0-maas-power-key, SEC-021) is expected by the matrix but NOT declared in the manifest -- the credential is either missing or undeclared + [FAIL] S2 vr1-dc0 EXPECTED-BUT-ABSENT: 'vr1-dc0-maas-power_ed25519.pub' (id dc0-maas-power-key, SEC-021) is expected by the matrix but NOT declared in the manifest -- the credential is either missing or undeclared + [FAIL] S5 ASYMMETRY: vr1-dc0 declares id=dcN-maas-power-key file=id_ed25519 on headend (custody=off-manifest-known) with no counterpart in vr1-dc1 -- a per-DC credential must exist at BOTH DCs in the same shape + [FAIL] S5 ASYMMETRY: vr1-dc0 declares id=dcN-maas-power-key file=maas-virsh_ed25519 on headend (custody=off-manifest-known) with no counterpart in vr1-dc1 -- a per-DC credential must exist at BOTH DCs in the same shape + [FAIL] S5 ASYMMETRY: vr1-dc1 declares id=dcN-maas-power-key file=id_dcN_power on headend (custody=off-manifest-known) with no counterpart in vr1-dc0 -- a per-DC credential must exist at BOTH DCs in the same shape + [FAIL] S6 IDENTITY CONFLATION: id 'maas-region-admin' serves 2 principal types (human via gui; service via api, cli-profile) -- ruling 5 requires one identity to serve one principal type + [FAIL] E1 EXPECTED-BUT-ABSENT: dc0-edge-api 'opnsense-api.txt' expected at jumphost/vr1-dc0, not found (mint-stage stage3, SEC-021) + [FAIL] E1 EXPECTED-BUT-ABSENT: dc0-maas-power-key 'vr1-dc0-maas-power_ed25519' expected at jumphost/vr1-dc0, not found (mint-stage stage3, SEC-021) + [FAIL] E1 EXPECTED-BUT-ABSENT: dc0-maas-power-key 'vr1-dc0-maas-power_ed25519.pub' expected at jumphost/vr1-dc0, not found (mint-stage stage3, SEC-021) + [FAIL] E1 EXPECTED-BUT-ABSENT: dc1-svc-key 'vr1-dc1_svc_ed25519.pub' expected at headend/vr1-dc1, not found (mint-stage stage3, SEC-022) + [FAIL] E2 WORLD-READABLE: office1-tofu-maas-apikey 'terraform.tfstate.backup' at jumphost/vr1-office1 is mode 664 + [FAIL] E4 UNCHECKABLE: 2 row(s) have no declared location for their (role, site) and can never be verified -- add a location row or correct the matrix: capi-mgmt-kubeconfig 'config' (cloud/-); rbd-mirror-peer-token 'rbd-mirror-bootstrap-token' (unit/-) + +FAIL: creds-matrix tier 1 -- 81 row(s), 15 check group(s) clean, 13 finding(s) diff --git a/docs/changelog-20260726-d137-tier1.md b/docs/changelog-20260726-d137-tier1.md index b002397..d7f48d5 100644 --- a/docs/changelog-20260726-d137-tier1.md +++ b/docs/changelog-20260726-d137-tier1.md @@ -417,3 +417,56 @@ **Revert:** `git revert` this commit; the checker returns to role-keyed tier 2 and the harness to 35 cases. + +## Item 15 -- phase 3: re-measure the register; SEC-024 opened + +**~21 inferred filenames corrected against their own mint-refs** (the session had owned +six; those were only the rows the first sweep physically touched): +- **octavia, 8 rows** -> `passphrase.txt` x2, `issuing-ca.key.enc`, `issuing-ca.cert.pem`, + `controller-ca.key.enc`, `controller-ca.cert.pem`, `controller.key`, + `controller.bundle.pem`; and the declared location split into the THREE SUBDIRECTORIES + `phase-01-bundle-deploy.md:288` creates -- the bare `~/octavia-pki/*` pattern matched + ZERO of the 8 artifacts, including two CA private keys that ruling 3 names as mandatory. +- **vault, 2 rows** -> `init.txt` (the previous value was the DIRECTORY name). +- **tenant, 5 rows** -> `-` instance prefix, with a placeholder matcher added + (T44) so per-tenant rows can match a real basename at all. +- `kube-config` -> `config`; `.maas.cli` -> `.maascli.db` (measured: `~/.maas.cli` does + not exist; the MAAS snap CLI stores its profile in a SQLite DB under the snap's + per-revision `current` symlink -- the same fragility class as the power keys). + +**`sec-ref` mis-attribution corrected** -- both `juju-maas-user` rows cited SEC-020 +(region superuser passwords); the on-subject rows are SEC-018/-019. The tfstate row's +SEC-009 removed (that row is DOCFIX-175). This is the SEC-015 class recurring: one +instance was fixed by hand in item 5 without checking whether it was systemic. It was. + +**`mint-ref` corrected** -- `admin.pass` cited `:452`, a CONSUMER; the mint is `:450` +(`newpass`). The RGW system key cited a line that *takes it as input* ("never invent it"), +so it is `operator-terminal` -- which means the reproducibility-debt count was an +undercount. `opnsense-bootstrap-apikey.sh:89` is a variable assignment; the mint is `:115`. + +**Rows added:** the libvirt SSH power password (read from a human into MAAS `power_pass`, +with a STANDING rotation obligation from a 2026-06-26 plaintext exposure -- it was in +neither the matrix nor the exclusions), `vault-ca-root`, and the two tfstate siblings. +`~/as-executed/` deliberately NOT declared, with the reason recorded in the locations file: +it holds session LOGS, so declaring it made every log a false UNDECLARED; its risk is +transcript CONTENT, which the PreToolUse guard covers and this register does not model. + +**Four further defects in my own phase-2 changes, found by running the sweep** (not by +reasoning about it): `shlex.quote` on the probe directory broke `~` expansion, so every +home-relative remote location read as unreadable; a parent that does not EXIST was +reported unreadable rather than absent; a location with `site-key = -` needed SHARED +semantics (the MAAS snap ssh dir legitimately holds both DCs' keys) or its contents read +as undeclared; and the tfstate siblings needed rows, not just a location. + +**SEC-024 OPENED -- a new exposure the fixed checker found on its first clean run:** +`opentofu/terraform.tfstate.backup` is mode **0664** and carries the MAAS API key in +plaintext (DOCFIX-175); the live state file is 0600. Invisible to every prior control +because the world-readable check was custody-gated and the siblings were undeclared. +**Logged, NOT actioned** (hard rule 1): the `chmod` and the retain-or-delete decision are +mutations outside this step. + +Findings 35 -> **13, all true**. Matrix 81 rows. Harness 44/44, creds-audit 15/15, +gauntlet ALL GREEN (80), repo-lint 0-fail. + +**Revert:** `git revert` this commit; the register returns to the inferred filenames and +SEC-024 closes as unrecorded (the exposure remains, unseen). diff --git a/docs/security-ledger.md b/docs/security-ledger.md index 0a69e9e..1f1c117 100644 --- a/docs/security-ledger.md +++ b/docs/security-ledger.md @@ -41,6 +41,8 @@ | 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` is mode 0664 (group- and world-readable) and carries the Office1 MAAS API key in PLAINTEXT.** 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. The two `terraform.tfstate.pre-*` snapshots are the same class and carry the same key. | measured 2026-07-26; `creds-matrix.py --tier2` E2 WORLD-READABLE; DOCFIX-175; capture `docs/audit/d137-tier2-postcommittee-20260726.txt` | operator | **OPEN -- logged, NOT actioned (hard rule 1).** Remediation is a `chmod` on the jumphost plus a decision about whether the historical snapshots should be retained at all; both are mutations and neither is in this step's scope. Rotation of the underlying key remains the SEC-018/-019 obligation. Custody off-repo per D-069. | + **STANDING CONVENTION (SEC-009, 2026-07-15): per-site credential/env consolidation.** ALL sensitive files AND environment/config files for a site live in a single `~/-creds/` folder on vcloud, mode 0700, files 0600 (public keys 0644). No loose env files in `~`. Sites: `~/vr1-office1-creds/` diff --git a/scripts/creds-matrix.py b/scripts/creds-matrix.py index b906e09..f7d7e57 100644 --- a/scripts/creds-matrix.py +++ b/scripts/creds-matrix.py @@ -35,7 +35,7 @@ --pending-stage vr0-phase01 ... # the full three-site existence sweep Exit: 0 clean, 1 findings, 2 usage/IO error. ASCII-only output. """ -import sys, os, re, glob, shlex, argparse, subprocess +import sys, os, re, glob, shlex, fnmatch, argparse, subprocess COLUMNS = ("id", "cardinality", "site-key", "host-role", "filename", "access-type", "principal", "custody", "mint-stage", "mint-ref", "sec-ref", "notes-ref") @@ -58,6 +58,27 @@ # The manifest mode column is DERIVED, not a matrix field: a public key half is 0644, # everything else 0600. That rule reproduces all three checked-in manifests exactly, so # it stays a derivation rather than a 13th column. +# A filename may carry a `` placeholder for a per-tenant / per-node instance name +# (`-svc-cred.txt`). Without this the per-tenant rows could never match a real +# basename, so at the first tenant onboard they would have produced five false absences +# AND five false undeclared findings on the SAME five real files -- noise that trains +# operators to ignore the most sensitive rows in the register (committee finding). +TEMPLATE_RE = re.compile(r"<[a-z]+>") + + +def fn_matches(declared, actual): + """Exact match, unless the declared name carries a `` instance placeholder. + + Per-tenant and per-node rows name an instance they cannot know at authoring time. + Without this the tenant rows could never match a real basename, so the first tenant + onboard would have produced five false absences AND five false undeclared findings on + the SAME five real files -- noise that trains operators to ignore the most sensitive + rows in the register (committee finding).""" + if "<" not in declared: + return declared == actual + return fnmatch.fnmatchcase(actual, TEMPLATE_RE.sub("*", declared)) + + def derived_mode(filename): return "644" if filename.endswith(".pub") else "600" @@ -503,6 +524,8 @@ # location that exists and holds secrets -- measured on the region # secrets dir (SEC-020), which read "does not exist" on the first sweep. parent = os.path.dirname(dx.rstrip("/")) or "/" + if not os.path.isdir(parent): + return ("absent", {}) # the whole tree is absent if not os.access(parent, os.R_OK | os.X_OK): return ("unreadable", {}) return ("absent", {}) @@ -529,10 +552,11 @@ # traversable; otherwise the truth is that we could not look. Guarded by # has_magic because a multi-word glob expansion makes `[ -d ... ]` a syntax # error, which silently disabled this whole state machine (committee finding). - q = shlex.quote(d) + q = d # validated by SAFE_PATH_RE; NOT quoted -- see note above probe = ('p=$(dirname %s); ' 'if [ ! -d %s ]; then ' - ' if [ -r "$p" ] && [ -x "$p" ]; then echo "__STATE__ absent"; ' + ' if [ ! -d "$p" ]; then echo "__STATE__ absent"; ' + ' elif [ -r "$p" ] && [ -x "$p" ]; then echo "__STATE__ absent"; ' ' else echo "__STATE__ unreadable"; fi; ' 'elif [ ! -r %s ] || [ ! -x %s ]; then echo "__STATE__ unreadable"; fi; ' % (q, q, q, q)) @@ -542,7 +566,7 @@ # __DONE__ proves the command ran on the host, so "found nothing" is a real # observation and only its ABSENCE means we could not reach the host. cmd = probe + ('for f in %s; do [ -f "$f" ] && stat -c "%%n %%a" "$f"; done; ' - 'echo __DONE__' % pattern) + 'echo __DONE__' % pattern) # pattern validated, deliberately unquoted if privileged: cmd = "sudo -n sh -c " + shlex.quote(cmd) try: @@ -649,14 +673,22 @@ if not r.has_artifact(): continue scope = (r.host_role, r.site_key) - if scope not in by_scope: + if scope not in by_scope and (r.host_role, "-") not in by_scope: # NOT silently skipped: a row whose scope has no declared location can never # be checked, and the module's contract is that nothing passes quietly. undeclarable.append("%s '%s' (%s/%s)" % (r.id, r.filename, r.host_role, r.site_key)) continue - if r.filename in observed.get(scope, {}): - got = observed[scope][r.filename] + pools = [observed.get(scope, {})] + if r.site_key != "-": + pools.append(observed.get((r.host_role, "-"), {})) # shared location + hit = got = None + for pool in pools: + hit = next((f for f in pool if fn_matches(r.filename, f)), None) + if hit: + got = pool[hit] + break + if hit: want = derived_mode(r.filename) if r.custody == "consolidated" and got != want: fails.append("E2 MODE: %s '%s' at %s/%s is %s, want %s" @@ -693,7 +725,13 @@ undeclared = 0 for scope in sorted(observed): for name in sorted(observed[scope]): - if name not in declared.get(scope, set()): + names = set(declared.get(scope, set())) + if scope[1] == "-": + # a shared location is satisfied by ANY site's row at that role + for (role2, _s2), decl in declared.items(): + if role2 == scope[0]: + names |= decl + if not any(fn_matches(d, name) for d in names): undeclared += 1 fails.append("E3 UNDECLARED: '%s' present at %s/%s but in NO matrix row " "for that site -- an undeclared secret is the exact class " diff --git a/tests/creds-matrix/run-tests.sh b/tests/creds-matrix/run-tests.sh index 0ddcf69..1cbcede 100644 --- a/tests/creds-matrix/run-tests.sh +++ b/tests/creds-matrix/run-tests.sh @@ -428,6 +428,18 @@ || { no "T43 uncheckable rows must be disclosed (rc=$RC)"; sed 's/^/ /' "$D/out"; } +# T44 -- per-tenant/per-node rows name an instance they cannot know at authoring time. +# Exact matching made the five tenant rows unmatchable, so the first onboard would have +# produced five false absences AND five false undeclared findings on the SAME real files. +D=$(mkenv); mkloc "$D" +printf 'x\n' > "$D/store/acme-handover.txt"; chmod 600 "$D/store/acme-handover.txt" +row 'tenant-thing per-tenant vr1-office1 jumphost -handover.txt api service off-manifest-known tenant-onboard operator-terminal - -' "$D" +run "$D" --tier2 +[ "$RC" = 0 ] && grep -q 'E1/E3 existence' "$D/out" \ + && ok "T44 a instance placeholder matches the real basename (no false absent+undeclared pair)" \ + || { no "T44 instance templating must match (rc=$RC)"; sed 's/^/ /' "$D/out"; } + + echo if [ "$F" = 0 ]; then echo "creds-matrix: $P/$P PASS"; exit 0; fi echo "creds-matrix: FAILURES: $F (passed $P)"; exit 1