diff --git a/plans/audit-core-openbao-runtime-custody.md b/plans/audit-core-openbao-runtime-custody.md new file mode 100644 index 0000000..2c32185 --- /dev/null +++ b/plans/audit-core-openbao-runtime-custody.md @@ -0,0 +1,120 @@ +--- +id: audit-core-openbao-runtime-custody +demand_source: audit-core/workplans/AUDIT-WP-0005-T02 +consumer_repo: audit-core +credential_type: openbao-approle-kv +status: reviewed +approved_by: null +approved_at: null +created: "2026-08-13" +updated: "2026-08-13" +--- + +# Construction plan: audit-core OpenBao runtime custody + +## 1. Demand + +audit-core on railiance01 needs durable PostgreSQL custody and a sender +registry without the founder typing OpenBao tokens or running `bao kv put`. +The founder’s remaining acts are: approve this plan, and unseal OpenBao if +it is sealed. Mechanics belong to ops-mason (structure) and ops-warden +(routing / founder desk), not to an ad-hoc terminal session. + +## 2. Existing-structure survey + +Checked 2026-08-13 against live railiance01 and the routing catalog: + +- **Database leases already exist.** `rapp-postgres` provisioned `audit_core` + with `audit_core_app` / `audit_core_migrate`, and OpenBao already vends + `database/creds/audit-core-runtime` and `database/creds/audit-core-migration` + through railiance-platform grants `rapp-postgres/audit-core-runtime` and + `rapp-postgres/audit-core-migration`. `warden route show + database-dynamic-credentials` points at that playbook. Do not create a + second database engine or a KV copy of a rotating lease. +- **ESO token already exists.** `external-secrets/openbao-audit-core-eso-token` + was minted 2026-08-13 with policy `external-secrets-audit-core` (read on + the two database cred paths and on `platform/data/workloads/audit-core/senders`). + ClusterSecretStore YAML is drafted; it is not yet applied. +- **KV senders path does not exist.** A founder `bao kv put` against + `platform/workloads/audit-core/senders` 403’d because the workstation + `~/.vault-token` is not an operator identity. That is the failure this + plan exists to retire. +- **Catalog.** `database-dynamic-credentials` already covers the lease. + There is no `audit-core-senders` entry. `openbao-api-key` is the generic + template and must not become the concrete lane. +- **Auth topology.** Same as user-engine / rapp-qonto: railiance01 ESO + cannot use CoulombCore Kubernetes auth. AppRole is the transitional + bridge; same-cluster Kubernetes auth remains the target. +- **Does an existing lane already satisfy this?** The database half, yes — + bind to the existing OpenBao database roles. The sender-registry half, + no — there is no path, no AppRole, and no catalog entry. Reuse the + email-connect / user-engine *shape* (namespace-limited store, 15m + tokens, eight uses), do not widen those identities. + +## 3. Proposed changes + +| # | Action | Object | Reuse-vs-new rationale | +|---|---|---|---| +| 1 | reuse | OpenBao roles `audit-core-runtime` / `audit-core-migration` | Already vended; a KV copy of a lease recreates the delivery gap | +| 2 | reuse | policy `external-secrets-audit-core` | Already written; covers database creds + senders read | +| 3 | create | AppRole `external-secrets-audit-core` | Replace the long-lived ESO token with the same 15m/30m/8-use bridge as user-engine and rapp-qonto | +| 4 | deliver | Secret `external-secrets/openbao-audit-core-approle` | Mason write-only delivery of role_id/secret_id; never logged | +| 5 | create | ClusterSecretStore `openbao-audit-core` | Namespace-limited to `audit-core`; AppRole auth (token store is the interim until this builds) | +| 6 | reuse | ClusterSecretStore `openbao-audit-core-database` + ExternalSecret | Namespaced VaultDynamicSecret cannot read the ESO token in `external-secrets`; CSS can. One `dataFrom` extract per lease. Applied 2026-08-13, SecretSynced | +| 7 | create | empty KV path `platform/workloads/audit-core/senders` | Structure only. Values are **not** founder-pasted: first deploy mints sender tokens in-cluster into Secret `audit-core-senders`. A later wrap-migrate (user-engine pattern) can move them into this path without printing | +| 8 | propose | ops-warden catalog `audit-core-senders` | Pointer, `warden_executes: false`, starts `draft` | +| 9 | retire | founder `bao kv put` / token-prompt as the T02 delivery path | That is the error-prone interaction this demand forbids | + +No sibling workload path is readable. Mason does not print, log, or return +sender tokens or database passwords. + +## 4. Review notes + +- **Naming:** `external-secrets-audit-core` / `openbao-audit-core` matches + email-connect and user-engine. Database grant names stay with rapp-postgres. +- **TTL/scoping:** 15-minute tokens, 30-minute max, eight uses. Store + condition is `audit-core` only. +- **Redundancy:** does not create a second database role or a KV cache of + the lease. Does not widen `openbao-email-connect` or + `openbao-user-engine-runtime`. +- **Compaction:** retires the attended `bao kv put` path as the way T02 + completes. The already-minted static ESO token is interim and is revoked + after the AppRole store is Ready. +- **Secret values:** sender tokens are generated by us, not imported from a + vendor. Founder paste-once is the wrong desk for them. Generate in-cluster; + optional later wrap-migrate into the empty KV path. +- **Founder remaining acts:** approve this plan; unseal OpenBao if sealed. + No token paste, no `bao` command review. + +## 5. Executive summary + +**One-line ask:** approve letting ops-mason finish audit-core’s OpenBao +lane (AppRole + namespace-limited store + empty senders path + catalog +pointer) so the receiver can run without you typing OpenBao tokens. + +**Who gets access:** only External Secrets, through AppRole +`external-secrets-audit-core`. Application pods keep seeing mounted +files and `AUDIT_CORE_SENDERS`. Nothing else gains a new identity. + +**To what:** read `database/creds/audit-core-runtime`, +`database/creds/audit-core-migration`, and (later) +`platform/workloads/audit-core/senders`. No sibling workload or tenant +path. + +**For how long:** each AppRole login is 15 minutes, 30 minutes maximum, +eight uses. The AppRole secret ID lasts until rotated or replaced by +same-cluster Kubernetes auth. + +**Blast radius if the credential leaks:** append to the audit trail and +read the sender registry; cannot rewrite or delete stored events (append-only +trigger). Revoke the AppRole secret ID, rotate sender tokens overlap-first, +and roll the receiver. Database leases expire on their own. + +**Cost to reverse:** delete the AppRole, its bootstrap Secret, the +ClusterSecretStore, and the empty KV path. Database roles and the +`audit_core` database stay (owned by rapp-postgres). No other consumer +is on this identity. + +**Decision:** approve, reject, or send back to phase 1. +Approval authorizes Mason build of structure and in-cluster sender +minting. It does not authorize printing any secret value.