--- 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: built approved_by: "Bernd Worsch" approved_at: "2026-08-13" 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. ## 6. Build result Built 2026-08-13 after founder approve, via `ops_mason.executor.build_approle_kv_lane` with `reuse_policy=True` so the existing `external-secrets-audit-core` policy (database creds + senders read) was not rewritten. - reused policy `external-secrets-audit-core`; - created AppRole `external-secrets-audit-core` (15m / 30m / 8 uses); - delivered role-id/secret-id into Secret `external-secrets/openbao-audit-core-approle`; local delivery dir shredded; - switched ClusterSecretStores `openbao-audit-core` and `openbao-audit-core-database` to AppRole auth; both `Valid`; - ExternalSecrets remain `SecretSynced`; receiver `/readyz` still `custody_class=archive`; - deleted interim Secret `external-secrets/openbao-audit-core-eso-token`; - catalog entry `audit-core-senders` already drafted in ops-warden (`warden route find "audit-core senders" --all`). Empty KV path `platform/workloads/audit-core/senders` was not written: mason does not put values, and senders already live in-cluster. A later wrap-migrate can create that path without a founder paste. No secret value was printed, committed, or logged. The 15-minute platform-admin child used for the build was revoked.