From 6f529413c8f48cd67f5655b662d24cad732662c9 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 9 Oct 2026 11:19:30 +0200 Subject: [PATCH] MASON-WP-0004-T02: draft custody descriptions and a metadata-only apply tool custody-describe.py drafts the five custody fields from CCRs, the routing catalog and the owner proposals (no OpenBao access), and applies accepted entries with bao kv metadata patch only, silently. 29/38 drafts complete; gaps left empty for owners. 4 tests. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 327203@bnt-lap001 Assistant-Session: 55028ae3-4914-4bd1-8f5c-815f3527aef0 --- manifests/custody-descriptions.yaml | 528 ++++++++++++++++++ scripts/custody-describe.py | 152 +++++ tests/test_custody_describe.py | 45 ++ ...-0004-credential-inventory-descriptions.md | 15 +- 4 files changed, 739 insertions(+), 1 deletion(-) create mode 100644 manifests/custody-descriptions.yaml create mode 100755 scripts/custody-describe.py create mode 100644 tests/test_custody_describe.py diff --git a/manifests/custody-descriptions.yaml b/manifests/custody-descriptions.yaml new file mode 100644 index 0000000..cafcf38 --- /dev/null +++ b/manifests/custody-descriptions.yaml @@ -0,0 +1,528 @@ +operators/nextcloud: + description: '' + missing: + - description + - used_by + - rotation + - on_loss + on_loss: '' + owner: railiance-platform + review: draft + rotation: '' + sources: [] + used_by: '' +operators/nextcloud/backup: + description: '' + missing: + - description + - used_by + - rotation + - on_loss + on_loss: '' + owner: railiance-platform + review: draft + rotation: '' + sources: [] + used_by: '' +platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets: + description: llm-connect provider access through OpenBao workload KV and External Secrets + missing: [] + on_loss: 'Re-issue per rotation: Replace OPENROUTER_API_KEY directly in OpenBao and record non-secret + rotation evidence.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0003-llm-connect-openrouter-api-key.yaml#lifecycle.rotate + sources: + - CCR-2026-0003-llm-connect-openrouter-api-key.yaml + - ops-warden catalog openrouter-llm-connect + used_by: ExternalSecret to Secret llm-connect-provider-secrets in the activity-core namespace +platform/workloads/activity-core/ops-run-workers/rein-aharness-metered-railiance01: + description: Deliver the bearer token of ops_run queue worker rein-aharness-metered@railiance01 to the + activity-core runtime Secret through External Secrets. + missing: [] + on_loss: 'Re-issue per rotation: activity-core re-mints the worker token at its path. ESO syncs it within + 1h, and the queue accepts the new token per ACTIVITY_CORE_WORKERS.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0030-activity-core-ops-run-worker-rein-aharness-metered-railiance01.yaml#lifecycle.rotate + sources: + - CCR-2026-0030-activity-core-ops-run-worker-rein-aharness-metered-railiance01.yaml + used_by: ExternalSecret actcore-ops-run-worker-tokens merges ACTIVITY_CORE_WORKER_TOKEN_METERED into + Secret actcore-runtime-secret in namespace activity-core. +platform/workloads/activity-core/ops-run-workers/rein-aharness-railiance01: + description: Deliver the bearer token of ops_run queue worker rein-aharness@railiance01 to the activity-core + runtime Secret through External Secrets. + missing: [] + on_loss: 'Re-issue per rotation: activity-core re-mints the worker token at its path. ESO syncs it within + 1h, and the queue accepts the new token per ACTIVITY_CORE_WORKERS.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0029-activity-core-ops-run-worker-rein-aharness-railiance01.yaml#lifecycle.rotate + sources: + - CCR-2026-0029-activity-core-ops-run-worker-rein-aharness-railiance01.yaml + used_by: ExternalSecret actcore-ops-run-worker-tokens merges ACTIVITY_CORE_WORKER_TOKEN into Secret + actcore-runtime-secret in namespace activity-core. +platform/workloads/agent-harness/forgejo-deploy-key: + description: agent-harness Forgejo deploy key (write sandbox; binky-control at cutover) + missing: + - used_by + on_loss: 're-establish by railiance-platform: On railiance01 generate a new ed25519 keypair under ~/.local/agent-harness/ssh/ + (mode 600); do not paste the private key into chat or Git.' + owner: railiance-platform + review: draft + rotation: ops-warden/wiki/playbooks/agent-harness-secrets.md#lane-2-forgejo-deploy-key + sources: + - ops-warden catalog agent-harness-forgejo-deploy + used_by: '' +platform/workloads/approval-engine/audit-sender: + description: Provide only the approval-engine sender with source=approval-engine, tenants=[tenant:platform], + may_write=true, may_read=false, evidence_kind=load-bearing, secret_policy=redact. + missing: [] + on_loss: 'Re-issue per rotation: Reviewed overlap-first rotation: append a replacement to this sender + only, deliver it, prove acceptance, then remove the predecessor and prove refusal. The first-provision + helper refuses rotation and unexpected existing values.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0021-approval-engine-audit.yaml#lifecycle.rotate + sources: + - CCR-2026-0021-approval-engine-audit.yaml + used_by: 'ClusterSecretStore openbao-approval-engine-audit restricted to namespace approval-engine; + ExternalSecret and Secret approval-engine/approval-engine-audit, key audit-token. Source: manifests/factory-audit-senders.yaml. + A missing namespace stays a workload-owner prerequisite; this packet creates none.' +platform/workloads/approval-engine/operator-client: + description: Hold the approval-engine-operator confidential client secret in platform custody and project + it into the KeyCape runtime so KeyCape can verify presented client_credentials for the operator client. + missing: [] + on_loss: 'Re-issue per rotation: KeyCape mints a replacement client secret; platform writes the new + KV version under the same attended authority. Rotation is independent of CCR-2026-0017.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml#lifecycle.rotate; + railiance-platform/credential-change-requests/CCR-2026-0020-approval-engine-operator-client-read.yaml#lifecycle.rotate + sources: + - CCR-2026-0018-keycape-approval-engine-operator-client.yaml + - CCR-2026-0020-approval-engine-operator-client-read.yaml + used_by: 'ClusterSecretStore openbao-keycape-approval-engine-operator, limited to namespace sso, to + ExternalSecret sso/keycape-approval-engine-operator-client and Secret sso/keycape-approval-engine-operator-client + with key client-secret. KeyCape resolves it as KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET through + a secretKeyRef. Manifests: argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml + and keycape-approval-clients.externalsecrets.yaml.' +platform/workloads/audit-core/senders: + description: audit-core sender registry (write and operator-read tokens) + missing: [] + on_loss: audit-core re-mints sender tokens and every projected copy is reseeded; senders queue to their + local outboxes meanwhile. See ops-mason/plans/audit-core-openbao-runtime-custody.md. + owner: ops-mason + review: draft + rotation: ops-warden/wiki/playbooks/audit-core-senders.md#worker-checklist + sources: + - ops-warden catalog audit-core-senders + - ops-mason plan (hand-filled 2026-10-09) + used_by: audit-core on railiance01 (sender registry authority); seed source for per-sender projections + such as tenant-engine/audit-core-sender +platform/workloads/core-hub/forgejo-source-read: + description: Hold a dedicated Forgejo PAT scoped exactly read:repository and project it through External + Secrets into namespace core-hub as the Secret that the rapp-core-hub publisher mounts read-only at + /var/run/secrets/repo-manager/forgejo/token. + missing: [] + on_loss: 'Re-issue per rotation: Every 180 days or on owner change. Mint a replacement read:repository + PAT for core-hub-source, write it to OpenBao from stdin, let ESO refresh (5m), pass make verify-publisher, + then revoke the predecessor PAT and record evidence.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0031-core-hub-forgejo-source-read.yaml#lifecycle.rotate + sources: + - CCR-2026-0031-core-hub-forgejo-source-read.yaml + - ops-warden catalog core-hub-forgejo-source-read + used_by: ClusterSecretStore openbao-core-hub-forgejo-source (conditions limited to namespace core-hub) + feeding ExternalSecret core-hub/repo-manager-forgejo-source, which materializes Secret repo-manager-forgejo-source + with the single key token (remoteRef workloads/core-hub/forgejo-source-read property FORGEJO_SOURCE_TOKEN), + explicit empty target metadata template, creationPolicy Owner, deletionPolicy Retain. rapp-core-hub + then sets publisher.existingSecret to repo-manager-forgejo-source with tokenKey token. Both ma… +platform/workloads/core-hub/runtime: + description: Project the existing Core Hub protected-API token into the private railiance01 runtime + through a namespace-limited ExternalSecret while preserving the token across the relocation window. + missing: [] + on_loss: 'Re-issue per rotation: Coordinate a new random token with all protected API consumers, update + the OpenBao field through attended custody, wait for ESO sync, roll Core Hub and consumers, verify + protected calls, then invalidate the old token.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0013-core-hub-runtime-api-token.yaml#lifecycle.rotate + sources: + - CCR-2026-0013-core-hub-runtime-api-token.yaml + used_by: ClusterSecretStore openbao-core-hub-runtime, limited to namespace core-hub, to ExternalSecret + core-hub/core-hub-api-token and Secret core-hub-api-token. The rapp package consumes this Secret separately + from runtime and migration PostgreSQL lease Secrets. +platform/workloads/coulomb-social/backup-primary: + description: Read this application backup publisher key pair into the admitted node delivery process; + no other workload credential or escrow. + missing: [] + on_loss: 'Re-issue per rotation: Separate owner-approved CAS rotation and atomic host replacement contract + required. This helper refuses different existing bytes; retain old provider identity until new publication/recovery + acceptance, then revoke predecessor at provider.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0036-coulomb-social-backup-primary.yaml#lifecycle.rotate + sources: + - CCR-2026-0036-coulomb-social-backup-primary.yaml + used_by: /etc/coulomb-social-backup/primary.env +platform/workloads/coulomb/whynot-design/npm-publish: + description: npm package publishing through ops-warden caller-scoped fetch/exec + missing: + - used_by + on_loss: 'Re-issue per rotation: Replace NPM_AUTH_TOKEN value directly in OpenBao and record non-secret + rotation evidence.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0001-whynot-design-npm-publish.yaml#lifecycle.rotate + sources: + - CCR-2026-0001-whynot-design-npm-publish.yaml + - ops-warden catalog whynot-design-npm-publish + used_by: '' +platform/workloads/email-connect/backup-primary: + description: Read this application backup publisher key pair into the admitted node delivery process; + no other workload credential or escrow. + missing: [] + on_loss: 'Re-issue per rotation: Separate owner-approved CAS rotation and atomic host replacement contract + required. This helper refuses different existing bytes; retain old provider identity until new publication/recovery + acceptance, then revoke predecessor at provider.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0037-email-connect-backup-primary.yaml#lifecycle.rotate + sources: + - CCR-2026-0037-email-connect-backup-primary.yaml + used_by: /etc/email-connect-backup/primary.env +platform/workloads/email-connect/transactional: + description: Deliver EMAIL_CONNECT_INGEST_TOKEN and IONOS SMTP username/password into the email-connect + namespace via External Secrets so the transactional receiver can send invitation and verification + mail without baking secrets into the image, Git, or user-engine. + missing: [] + on_loss: 'Re-issue per rotation: Overlap-first: put new SMTP password and/or ingest token in OpenBao; + wait for ESO refresh (or force sync); roll email-connect and user-engine as needed; revoke old provider + password / retire old bearer.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0010-email-connect-transactional.yaml#lifecycle.rotate + sources: + - CCR-2026-0010-email-connect-transactional.yaml + - ops-warden catalog email-connect-transactional + used_by: ClusterSecretStore openbao-email-connect (tokenSecretRef openbao-email-connect-eso-token; namespace + condition email-connect) at railiance-platform/argocd/platform-addons/openbao-secretstore/openbao-email-connect.clustersecretstore.yaml + → ExternalSecret email-connect/email-connect-runtime → Secret email-connect-runtime. Package manifests + in email-connect/deploy/k8s/railiance/ (not applied until this CCR is approved and the KV secret is + provisioned). +platform/workloads/forgejo/forgejo-admin: + description: Forgejo admin API token for package prune, registry ops, org webhooks, and operator bootstrap + on workstations and activity-core worker + missing: [] + on_loss: 'Re-issue per rotation: Mint a new PAT in Forgejo, update API_TOKEN in OpenBao, record non-secret + rotation evidence, and restart dependent automation after downstream consumers load from OpenBao.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0006-forgejo-admin-api-token-lane.yaml#lifecycle.rotate + sources: + - CCR-2026-0006-forgejo-admin-api-token-lane.yaml + - ops-warden catalog forgejo-admin-api-token + used_by: railiance-platform tools/cmd/forgejo-package-prune, activity-core weekly-forgejo-package-prune + shell resolver, and railiance-apps Forgejo tools (load_token pattern after lane verified) +platform/workloads/glas-harness/claude-agent-dev: + description: Dedicated Claude Code inference for the Glas local agent-dev profile, delivered by the + sandbox owner. + missing: [] + on_loss: 'Re-issue per rotation: Create a replacement provider key, write with expected-version CAS, + stop old runs, verify new delivery and inference, then revoke the predecessor at Anthropic and prove + its denial.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0016-glas-claude-anthropic.yaml#lifecycle.rotate + sources: + - CCR-2026-0016-glas-claude-anthropic.yaml + used_by: Sand-boxer delivers ANTHROPIC_API_KEY only to the selected Claude workload; neither the Glas + caller nor the runtime receives an OpenBao token. +platform/workloads/inbound-library/imap: + description: inbound-library AppRole for non-interactive library@coulomb.social IMAP read + missing: [] + on_loss: 'rotate by ops-mason: bao write -f auth/approle/role/inbound-library/secret-id -> new secret_id; + deliver mode-0600 to ~/.local/inbound-library/approle; shred old secret_id file.' + owner: ops-mason + review: draft + rotation: ops-warden/wiki/playbooks/inbound-library-imap-approle.md#worker-checklist + sources: + - ops-warden catalog inbound-library-imap-approle + - ops-mason plan (hand-filled 2026-10-09) + used_by: inbound-library systemd user timer on the workstation, via AppRole inbound-library (reads once + per run, then revokes its token) +platform/workloads/informed-decision/audit-sender: + description: Provide only the informed-decision sender with source=informed-decision, tenants=[tenant:platform], + may_write=true, may_read=false, evidence_kind=load-bearing, secret_policy=redact. + missing: [] + on_loss: 'Re-issue per rotation: Reviewed overlap-first rotation: append a replacement to this sender + only, deliver it, prove acceptance, then remove the predecessor and prove refusal. The first-provision + helper refuses rotation and unexpected existing values.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0022-informed-decision-audit.yaml#lifecycle.rotate + sources: + - CCR-2026-0022-informed-decision-audit.yaml + used_by: 'ClusterSecretStore openbao-informed-decision-audit restricted to namespace informed-decision; + ExternalSecret and Secret informed-decision/informed-decision-audit, key token. Source: manifests/factory-audit-senders.yaml. + A missing namespace stays a workload-owner prerequisite; this packet creates none.' +platform/workloads/informed-decision/sitting-requester: + description: create-only sitting requester KeyCape verifier custody; approval:create only; subject informed-decision; + audience approval-engine. + missing: [] + on_loss: 'Re-issue per rotation: Rotate through KeyCape and platform using a new version with predecessor + refusal proof.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0026-informed-decision-sitting-requester-verifier.yaml#lifecycle.rotate; + railiance-platform/credential-change-requests/CCR-2026-0027-informed-decision-sitting-requester-reader.yaml#lifecycle.rotate + sources: + - CCR-2026-0026-informed-decision-sitting-requester-verifier.yaml + - CCR-2026-0027-informed-decision-sitting-requester-reader.yaml + - CCR-2026-0032.yaml + used_by: sso/keycape-informed-decision-sitting-requester-client via ESO; env KEYCAPE_INFORMED_DECISION_SITTING_REQUESTER_CLIENT_SECRET +platform/workloads/issue-core/issue-core/issue-core-runtime: + description: issue-core runtime ingestion through OpenBao workload KV and External Secrets + missing: [] + on_loss: 'Re-issue per rotation: Replace issue-core runtime secret values directly in OpenBao and record + non-secret rotation evidence.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0002-issue-core-ingestion-api-key.yaml#lifecycle.rotate + sources: + - CCR-2026-0002-issue-core-ingestion-api-key.yaml + - ops-warden catalog issue-core-ingestion-api-key + used_by: ExternalSecret issue-core/issue-core-runtime -> Secret issue-core-runtime in the issue-core + namespace +platform/workloads/keycape/secrets-engine-openbao: + description: Hold the secrets-engine-openbao client secret authoritatively in platform KV and deliver + it to the KeyCape issuer through sso-only ESO + missing: [] + on_loss: 'Re-issue per rotation: Roll-forward only: custody child writes a new version with cas= + under a standing rotation approval for this CCR (event-driven + 90d); never a read or restore of an + old version' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0033.yaml#lifecycle.rotate; railiance-platform/credential-change-requests/CCR-2026-0034.yaml#lifecycle.rotate + sources: + - CCR-2026-0033.yaml + - CCR-2026-0034.yaml + used_by: sso/keycape-secrets-engine-openbao-client key client-secret; issuer-side delivery only +platform/workloads/net-kingdom/keycape-factor-issuer: + description: '' + missing: + - description + - used_by + - rotation + - on_loss + on_loss: '' + owner: railiance-platform + review: draft + rotation: '' + sources: [] + used_by: '' +platform/workloads/net-kingdom/keycape-factor-read: + description: Deliver only a provider-issued coulomb factor-read JWT to KeyCape, separating it from issuer + credentials and signing keys. + missing: [] + on_loss: 'Re-issue per rotation: Issue replacement before expiry, CAS update exact KV, verify ESO projection + and consumer acceptance; retain no plaintext artifacts.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0023-keycape-factor-read.yaml#lifecycle.rotate + sources: + - CCR-2026-0023-keycape-factor-read.yaml + used_by: Namespace-restricted ClusterSecretStore openbao-keycape-factor-read -> sso/keycape-factor-read + Secret admin-token. Mount only the JWT in KeyCape; provider password remains in separate custody. +platform/workloads/policy-nexus/forgejo-source-read: + description: Hold a dedicated Forgejo PAT with read:repository only and deliver it as the FORGEJO_SOURCE_TOKEN + secret to the policy-nexus Actions workflow. + missing: [] + on_loss: 'Re-issue per rotation: Mint a replacement read:repository-only PAT, update OpenBao and the + Actions secret through attended custody, pass one candidate build, then revoke the predecessor.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0014-policy-nexus-forgejo-source-read.yaml#lifecycle.rotate + sources: + - CCR-2026-0014-policy-nexus-forgejo-source-read.yaml + - ops-warden catalog policy-nexus-forgejo-source-read + used_by: Repository Actions secret FORGEJO_SOURCE_TOKEN on coulomb/policy-nexus. Delivery is attended + and must not expose the value in command output, process arguments, Git, State Hub, or workflow logs. +platform/workloads/railiance-clock/authority-signing: + description: Hold one ES256 authority private signing key in OpenBao and deliver it only to the private + railiance01 Railiance Clock service; no public listener or consumer-wide distribution. + missing: [] + on_loss: 'Re-issue per rotation: Stop authority, perform separately versioned attended CAS rotation + and atomic host replacement, then admit new public key and epoch for consumers.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0028-railiance-clock-authority-signing.yaml#lifecycle.rotate + sources: + - CCR-2026-0028-railiance-clock-authority-signing.yaml + used_by: /var/lib/railiance-clock/signing.pem +platform/workloads/railiance-telemetry/smtp: + description: Native password-reset notifier; password mounted read-only under /run/secrets/authelia-smtp/password. + missing: + - used_by + - on_loss + on_loss: '' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0035-authelia-platform-smtp.yaml#lifecycle.rotate + sources: + - CCR-2026-0035-authelia-platform-smtp.yaml + used_by: '' +platform/workloads/railiance/backup/object-storage: + description: Object-store credentials for Railiance backup destinations. + missing: [] + on_loss: 'Re-issue per rotation: Overlap-first. Put a new key in OpenBao, wait for ESO refresh, revoke + the old Scaleway key.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0012-backup-object-storage.yaml#lifecycle.rotate + sources: + - CCR-2026-0012-backup-object-storage.yaml + used_by: 'ClusterSecretStore openbao-backup-object-storage (first namespace condition: databases) → + ExternalSecret databases/platform-pg-backup-s3 → Secret platform-pg-backup-s3 (ACCESS_KEY_ID / ACCESS_SECRET_KEY + projected from ACCESS_KEY / SECRET_KEY). Drafts live under railiance-platform/argocd/platform-addons/openbao-secretstore/ + and rapp-postgres/helm/platform-pg-backup-s3.externalsecret.yaml. Do not apply until this CCR is approved + and the KV values are real.' +platform/workloads/railiance/backup/offsite-lane: + description: age-encrypted offsite backup upload through Nextcloud WebDAV file drop + missing: [] + on_loss: 'Re-issue per rotation: Mint a new Nextcloud upload token, update NC_WEBDAV_TOKEN in OpenBao, + and record non-secret rotation evidence.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0004-railiance-backup-offsite-lane.yaml#lifecycle.rotate + sources: + - CCR-2026-0004-railiance-backup-offsite-lane.yaml + - ops-warden catalog railiance-backup-offsite-lane + used_by: railiance-platform lib/railiance-backup-common.sh and forgejo-backup via RAILIANCE_BACKUP_NC_TOKEN + env (fetched through OpenBao or warden access) +platform/workloads/railiance/scaleway/bootstrap: + description: Create reef-storage object-store members; not the Barman runtime key + missing: [] + on_loss: 'Re-issue per rotation: Put a new bootstrap key; do not reuse the scoped backup key.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0011-scaleway-object-storage-bootstrap.yaml#lifecycle.rotate + sources: + - CCR-2026-0011-scaleway-object-storage-bootstrap.yaml + - ops-warden catalog scaleway-bootstrap + used_by: reef-storage/tools/create-platform-audit-bucket.sh (reads, never prints) +platform/workloads/rapp-qonto/keycape-client: + description: rapp-qonto KeyCape workload client + missing: + - used_by + on_loss: 'rotate by key-cape: Generate a fresh newline-free secret through an approved execution transport; + never print it. NOT automatable — no admitted execution and rollback contract (see delegation.blocked_on).' + owner: key-cape + review: draft + rotation: ops-warden/wiki/CredentialRouting.md#routing-catalog-index + sources: + - ops-warden catalog rapp-qonto-keycape-client + used_by: '' +platform/workloads/reuse/reuse-surface/runtime-secrets: + description: reuse-surface federation hub runtime secrets through OpenBao workload KV and External Secrets + missing: [] + on_loss: 'Re-issue per rotation: Replace field values directly in OpenBao, wait for ESO refresh, rollout + reuse-surface, then run make reuse-forgejo-webhook when rotating the webhook HMAC.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0005-reuse-surface-runtime-secrets-lane.yaml#lifecycle.rotate + sources: + - CCR-2026-0005-reuse-surface-runtime-secrets-lane.yaml + - ops-warden catalog reuse-surface-hub-write-token + used_by: ExternalSecret reuse/reuse-surface-runtime -> Secret reuse-surface-env; ClusterSecretStore + openbao-reuse uses private railiance01 OpenBao and exact reuse/reuse-surface-eso Kubernetes identity. + Caller-facing OIDC policy workload-kv-read-reuse-surface-runtime remains separately retained. +platform/workloads/secrets-engine/approval-client: + description: Hold the secrets-engine-approval confidential client secret in platform custody and project + it into the KeyCape runtime so KeyCape can verify presented client_credentials without the value living + in Git, a chart value, or a hand-created Kubernetes Secret. + missing: [] + on_loss: 'Re-issue per rotation: KeyCape mints a replacement client secret; platform writes the new + KV version under the same attended authority; ESO refresh delivers it and KeyCape restarts or re-reads. + Rotation is independent of the operator client.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml#lifecycle.rotate; + railiance-platform/credential-change-requests/CCR-2026-0019-secrets-engine-approval-client-read.yaml#lifecycle.rotate + sources: + - CCR-2026-0017-keycape-secrets-engine-approval-client.yaml + - CCR-2026-0019-secrets-engine-approval-client-read.yaml + used_by: 'ClusterSecretStore openbao-keycape-secrets-engine-approval, limited to namespace sso, to ExternalSecret + sso/keycape-secrets-engine-approval-client and Secret sso/keycape-secrets-engine-approval-client with + key client-secret. KeyCape resolves it as KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET through a + secretKeyRef, matching the live KEYCAPE_RAPP_QONTO_CLIENT_SECRET shape observed on image main-153258b. + Manifests: argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstor…' +platform/workloads/secrets-engine/approval-requester: + description: T03 create-only requester KeyCape verifier custody; execute only the three fixed T03 request + records. + missing: [] + on_loss: 'Re-issue per rotation: Rotate through KeyCape and platform using a new version with predecessor + refusal proof.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0024-t03-requester-verifier.yaml#lifecycle.rotate; + railiance-platform/credential-change-requests/CCR-2026-0025-t03-requester-reader.yaml#lifecycle.rotate + sources: + - CCR-2026-0024-t03-requester-verifier.yaml + - CCR-2026-0025-t03-requester-reader.yaml + used_by: sso/keycape-secrets-engine-requester-client via ESO; env KEYCAPE_SECRETS_ENGINE_REQUESTER_CLIENT_SECRET +platform/workloads/state-hub/repository-rename-preflight: + description: Deliver one HMAC signing key to the transitional State Hub API; no rename execution, general + credential vending, or second identity authority. + missing: [] + on_loss: 'Re-issue per rotation: Stop every API replica and verify no matching pods before CAS rotation; + wait ESO, recreate API replicas with one version, verify old token invalidation and health.' + owner: railiance-platform + review: draft + rotation: railiance-platform/credential-change-requests/CCR-2026-0015-state-hub-preflight-signing.yaml#lifecycle.rotate + sources: + - CCR-2026-0015-state-hub-preflight-signing.yaml + used_by: Namespace SecretStore openbao-state-hub-rename-preflight -> ExternalSecret/Secret state-hub-rename-preflight; + required explicit API-only secretKeyRef. Owner creation, Retain deletion policy. +platform/workloads/telemetry/grafana-admin: + description: '' + missing: + - description + - used_by + - rotation + - on_loss + on_loss: '' + owner: railiance-platform + review: draft + rotation: '' + sources: [] + used_by: '' +platform/workloads/tenant-engine/audit-core-sender: + description: tenant-engine's audit-core sender token, a copy of its entry in the audit-core sender registry. + Not the registry itself. + missing: [] + on_loss: Reseed from platform/workloads/audit-core/senders with the seed helper (founder, attended). + audit-core re-mints if the registry entry is also gone; emission waits in tenant-engine's local outbox + meanwhile. + owner: railiance-platform + review: draft + rotation: ops-mason/plans/tenant-engine-audit-core-sender-projection.md; reseed with railiance-platform/scripts/tenant_engine_audit_core_sender_seed.py + sources: + - ops-mason plan (hand-filled 2026-10-09) + used_by: ESO ClusterSecretStore openbao-tenant-engine-audit-core-sender (k8s role tenant-engine-audit-core-sender-eso) + -> Secret tenant-engine/tenant-engine-audit-core-sender key token +platform/workloads/user-engine/runtime: + description: 'user-engine runtime secrets: the portal edge marker and the identity-provisioner service + token. Not a user credential.' + missing: + - on_loss + on_loss: '' + owner: railiance-platform + review: draft + rotation: 'ops-mason/plans/user-engine-openbao-runtime-custody.md (rows 8-9: ExternalSecret is the sole + writer and rotation path)' + sources: + - ops-mason plan (hand-filled 2026-10-09) + used_by: user-engine and identity-provisioner via External Secrets Operator diff --git a/scripts/custody-describe.py b/scripts/custody-describe.py new file mode 100755 index 0000000..0e8f210 --- /dev/null +++ b/scripts/custody-describe.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""custody-describe — draft and apply the five custody fields (MASON-WP-0004-T02). + + ./scripts/custody-describe.py draft > manifests/custody-descriptions.yaml + ./scripts/custody-describe.py check manifests/custody-descriptions.yaml + ./scripts/custody-describe.py apply manifests/custody-descriptions.yaml # founder + +`draft` reads only documents: railiance-platform CCRs, the ops-warden routing +catalog and the owner proposals. It never calls OpenBao. Every entry it writes +is `review: draft`; a field it cannot source stays empty rather than guessed, +because a confidently wrong `used_by` is worse than an empty one. + +`apply` writes custom_metadata only (`bao kv metadata patch`), for entries +marked `review: accepted` with all five fields filled. It never reads or writes +a value, prints nothing but paths and booleans, and runs under the founder's +admin session (warden access openbao-platform-admin-login --exec), since +ops-mason-survey cannot write metadata. +""" + +from __future__ import annotations + +import glob +import os +import re +import subprocess +import sys + +import yaml + +REQUIRED = ("description", "owner", "used_by", "rotation", "on_loss") +MAX_VALUE = 512 # OpenBao custom_metadata value limit +HOME = os.path.expanduser("~") +CCR_DIR = f"{HOME}/railiance-platform/credential-change-requests" +CATALOG = f"{HOME}/ops-warden/registry/routing/catalog.yaml" +PROPOSALS = os.path.join(os.path.dirname(__file__), "..", "docs", "credential-owner-proposals-2026-10-09.md") + + +def _clip(text: str | None) -> str: + text = " ".join(str(text or "").split()) + return text if len(text) <= MAX_VALUE else text[: MAX_VALUE - 1] + "…" + + +def _first_sentence(text: str | None) -> str: + text = " ".join(str(text or "").split()) + m = re.match(r"(.+?\.)(\s|$)", text) + return _clip(m.group(1) if m else text) + + +def _proposals() -> dict[str, str]: + """Path -> proposed owner, from section C of the owner proposals.""" + owners = {} + for line in open(PROPOSALS): + m = re.match(r"\| `([^`]+)` \| [^|]+ \| ([a-z0-9-]+) \(consumer", line) + if m: + owners[m.group(1)] = m.group(2) + return owners + + +def _ccrs() -> dict[str, list[tuple[str, dict]]]: + by_path: dict[str, list[tuple[str, dict]]] = {} + for f in sorted(glob.glob(f"{CCR_DIR}/CCR-*.yaml")): + d = yaml.safe_load(open(f)) + if not isinstance(d, dict): + continue + bao = d.get("openbao") or {} + path = bao.get("kv_path") or bao.get("path") + if path: + by_path.setdefault(path, []).append((os.path.basename(f), d)) + return by_path + + +def _catalog() -> dict[str, dict]: + cat = yaml.safe_load(open(CATALOG)) + entries = cat.get("entries") if isinstance(cat, dict) else cat + return {str(e.get("path_template")): e for e in entries or [] if e.get("path_template")} + + +def draft() -> dict: + ccrs, catalog, out = _ccrs(), _catalog(), {} + for path, owner in _proposals().items(): + entry = {"review": "draft", "sources": []} + fields = dict.fromkeys(REQUIRED, "") + fields["owner"] = owner + if path in ccrs: + # The first CCR placed the path; later ones add readers. + name, d = ccrs[path][0] + fields["description"] = _first_sentence((d.get("target") or {}).get("purpose") or d.get("title")) + fields["used_by"] = _clip((d.get("delivery") or {}).get("target")) + refs = [f"railiance-platform/credential-change-requests/{n}#lifecycle.rotate" for n, _ in ccrs[path]] + fields["rotation"] = _clip("; ".join(refs[:2])) + rotate = (d.get("lifecycle") or {}).get("rotate") + if rotate: + fields["on_loss"] = _clip(f"Re-issue per rotation: {rotate}") + entry["sources"] += [n for n, _ in ccrs[path]] + cat = catalog.get(path) + if cat: + fields["description"] = fields["description"] or _first_sentence(cat.get("title")) + rot = cat.get("rotation") or {} + if not fields["rotation"] and cat.get("wiki_ref"): + fields["rotation"] = _clip(f"ops-warden/{cat['wiki_ref']}") + if not fields["on_loss"] and rot.get("steps"): + fields["on_loss"] = _clip(f"{rot.get('method', '')} by {rot.get('owner', '')}: {rot['steps'][0]}") + entry["sources"].append(f"ops-warden catalog {cat.get('id')}") + entry.update(fields) + entry["missing"] = [k for k in REQUIRED if not entry[k]] + out[path] = entry + return out + + +def check(doc: dict) -> int: + bad = 0 + for path, e in doc.items(): + missing = [k for k in REQUIRED if not e.get(k)] + long = [k for k in REQUIRED if len(str(e.get(k, ""))) > MAX_VALUE] + state = e.get("review") + if missing or long or state != "accepted": + bad += 1 + print(f"{'ok ' if not (missing or long) and state == 'accepted' else '-- '}{path} review={state}" + f"{' missing=' + ','.join(missing) if missing else ''}{' too-long=' + ','.join(long) if long else ''}") + print(f"{len(doc) - bad}/{len(doc)} ready to apply") + return 0 if bad == 0 else 1 + + +def apply(doc: dict) -> int: + failed = 0 + for path, e in doc.items(): + if e.get("review") != "accepted" or any(not e.get(k) for k in REQUIRED): + print(f"skip {path}") + continue + mount, rest = path.split("/", 1) + args = ["bao", "kv", "metadata", "patch", f"-mount={mount}"] + args += [f"-custom-metadata={k}={_clip(e[k])}" for k in REQUIRED] + args.append(rest) + ok = subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 + failed += not ok + print(f"{'patched' if ok else 'FAILED '} {path}") + return 1 if failed else 0 + + +def main(argv: list[str]) -> int: + if len(argv) >= 1 and argv[0] == "draft": + yaml.safe_dump(draft(), sys.stdout, sort_keys=True, allow_unicode=True, width=100) + return 0 + if len(argv) == 2 and argv[0] in ("check", "apply"): + doc = yaml.safe_load(open(argv[1])) or {} + return check(doc) if argv[0] == "check" else apply(doc) + print("usage: custody-describe.py draft | check FILE | apply FILE", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tests/test_custody_describe.py b/tests/test_custody_describe.py new file mode 100644 index 0000000..6591793 --- /dev/null +++ b/tests/test_custody_describe.py @@ -0,0 +1,45 @@ +import importlib.util +from pathlib import Path +from unittest.mock import patch, MagicMock + +_spec = importlib.util.spec_from_file_location("custody_describe", Path(__file__).parents[1] / "scripts" / "custody-describe.py") +cd = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(cd) + +FULL = dict(description="d", owner="o", used_by="u", rotation="r", on_loss="l") + + +def test_apply_patches_only_accepted_complete_entries_and_only_metadata(): + doc = { + "platform/workloads/a/b": {"review": "accepted", **FULL}, + "platform/workloads/c/d": {"review": "draft", **FULL}, + "operators/e": {"review": "accepted", **dict(FULL, used_by="")}, + } + with patch.object(cd.subprocess, "run", return_value=MagicMock(returncode=0)) as run: + assert cd.apply(doc) == 0 + assert run.call_count == 1 + args = run.call_args.args[0] + assert args[:5] == ["bao", "kv", "metadata", "patch", "-mount=platform"] + assert args[-1] == "workloads/a/b" + assert not any(a in ("get", "put", "patch-data") or "/data/" in a for a in args) + assert run.call_args.kwargs["stdout"] is cd.subprocess.DEVNULL and run.call_args.kwargs["stderr"] is cd.subprocess.DEVNULL + + +def test_apply_reports_failure_without_output(): + doc = {"platform/workloads/a/b": {"review": "accepted", **FULL}} + with patch.object(cd.subprocess, "run", return_value=MagicMock(returncode=2)): + assert cd.apply(doc) == 1 + + +def test_check_refuses_drafts_missing_and_overlong_fields(): + doc = { + "p/ok": {"review": "accepted", **FULL}, + "p/draft": {"review": "draft", **FULL}, + "p/long": {"review": "accepted", **dict(FULL, on_loss="x" * 600)}, + } + assert cd.check(doc) == 1 + assert cd.check({"p/ok": doc["p/ok"]}) == 0 + + +def test_clip_respects_openbao_value_limit(): + assert len(cd._clip("y" * 2000)) == cd.MAX_VALUE diff --git a/workplans/MASON-WP-0004-credential-inventory-descriptions.md b/workplans/MASON-WP-0004-credential-inventory-descriptions.md index 50140af..e1d0ee0 100644 --- a/workplans/MASON-WP-0004-credential-inventory-descriptions.md +++ b/workplans/MASON-WP-0004-credential-inventory-descriptions.md @@ -90,7 +90,7 @@ id: MASON-WP-0004-T02 status: wait needs_human: true decision_id: 62aa95e4-3ccc-4867-8806-5e3db005b483 -blocking_reason: "Requires a valid scoped metadata grant (operator) before the described paths can be written and verified." +blocking_reason: "Drafts ready (manifests/custody-descriptions.yaml, 29/38 complete). Founder reviews and marks entries accepted, then applies with scripts/custody-describe.py apply under the admin wrapper; owners fill the 9 gaps (T03)." priority: medium state_hub_task_id: "a6a944d7-21c5-5043-a78d-b3809de0ee64" ``` @@ -106,6 +106,19 @@ Acceptance: `custody-inventory.py` reports these paths without a `!`, and each 2026-10-03: human gate routed to hub decision 62aa95e4-3ccc-4867-8806-5e3db005b483 (D1: Attended OpenBao provisioning window: one session clears the pending credential lanes) by CUST-WP-0075; resolving that decision raises C-40 here. +2026-10-09: drafted. `scripts/custody-describe.py draft` builds +`manifests/custody-descriptions.yaml` from the railiance-platform CCRs +(`target.purpose`, `delivery.target`, `lifecycle.rotate`), the ops-warden +catalog and the owner proposals, never from OpenBao; four ops-mason-sourced +entries were filled by hand from `plans/`. 29 of 38 have all five fields; 9 +have gaps left empty on purpose (nextcloud x2, keycape-factor-issuer, +grafana-admin, smtp, and four `used_by`). Every entry is `review: draft`. +`apply` writes custom_metadata only, for `review: accepted` entries, silently +(`tests/test_custody_describe.py`, suite 157 passed). Founder run, after +review: `warden access openbao-platform-admin-login --exec -- python3 +/home/worsch/ops-mason/scripts/custody-describe.py apply +/home/worsch/ops-mason/manifests/custody-descriptions.yaml`. + ## Resolve the rest with their owners ```task