Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 292700@bnt-lap001 Assistant-Session: 42de6b79-9019-4cfd-b49a-6db3bbfe9341
11 KiB
| id | demand_source | consumer_repo | credential_type | status | approved_by | approved_at | created | updated |
|---|---|---|---|---|---|---|---|---|
| tenant-engine-audit-core-sender-projection | tenant-engine/TEN-IN-0005 | tenant-engine | openbao-kubernetes-kv | draft | null | null | 2026-10-01 | 2026-10-01 |
Construction plan: tenant-engine audit-core sender token projection
DRAFT. Nothing here is applied. No OpenBao or cluster object was read or
changed to write it; the survey is from repository files only and must be
re-checked live (phase 2) with the scoped ops-mason-build grant.
1. Demand
audit-core admitted tenant-engine as an attributive sender and minted its
token on 2026-09-15 (AUDIT-WP-0010-T02). The token sits inside the sender
registry document at platform/workloads/audit-core/senders (KV v2, registry
version 9 at mint). tenant-engine's Deployment (commit 9fabfd7) mounts an
optional Secret tenant-engine-audit-core-sender, key token, namespace
tenant-engine, at /var/run/secrets/audit-core-sender/token. Nothing creates
that Secret, so audit emission stays in the local outbox. The demand: deliver
exactly that one token, and nothing else, into that Secret, with no
founder-typed value and no expiring login token.
2. Existing-structure survey
- The registry path cannot be scoped to one sender. It is one JSON document
holding every sender (
user-engine,operator,approval-engine,informed-decision,tenant-engine). OpenBao policies grant on a path, not a field. Reusingexternal-secrets-audit-core(reads that path) or pointing an ExternalSecretproperty:at it would let the tenant-engine identity read every other sender's token, including the read-capableoperatortoken. Rejected. ESOproperty:/templatefiltering is a delivery convenience, not an access boundary. openbao-tenant-engine-databaseis the wrong store to extend. (rapp-tenant-engine/manifests/tenant-engine-database-secrets.yaml) It is a ClusterSecretStore on mountdatabase, authenticated by static token Secretexternal-secrets/openbao-tenant-engine-eso-token. That token is the expiry-bound parent token (expires 2026-10-25) and its policy is database credential reads. Widening it to KV would bind a new secret to the expiring token and violate the one-identity-per-lane rule applied to email-connect/user-engine. Left untouched. Its own migration off the static token is a separate matter (see section 4).- A non-token-bound pattern exists and is current. RPF-WP-0045 (finished
2026-09-23) moved
openbao-email-connectandopenbao-activity-coreto ESOauth.kubernetes: dedicated ServiceAccount in the consumer namespace,audiences: [openbao], one OpenBao role per lane bound to that single SA and namespace,ttl/max_ttl/explicit_max_ttl=15m,token_no_default_policy=true, one policy (exactplatform/data/...read +auth/token/lookup-selfread +auth/token/revoke-selfupdate).auth/kubernetes/configuses the OpenBao pod's own token, so there is no reviewer JWT or login token to expire. The same pattern servesopenbao-forgejo,openbao-reuse,openbao-target-revenue. Lane records live inrailiance-platform/openbao/eso-auth-recovery/lanes.json; policies inrailiance-platform/openbao/policies/workload-kv-read-*-eso.hcl. - Reference shapes in ops-mason.
audit-core-openbao-runtime-custodyanduser-engine-openbao-runtime-custodyused AppRole bridges (secret_id bootstrap Secrets inexternal-secrets), explicitly "transitional until same-cluster Kubernetes auth". That bridge is no longer the target and is not proposed here. - Catalog.
audit-core-senders(owner ops-mason, pointer only) already exists in ops-warden. This lane adds a consumer under it; it does not need a new entry, only a pointer line once built. - Does an existing lane already satisfy this? No. No path, policy, role or store holds or may read the tenant-engine token alone.
- Unknown, to verify live: whether
platform/workloads/tenant-engine/has any metadata; whether ESO's controller maycreate serviceaccounts/tokenintenant-engine(RPF-WP-0045 confirmed this for two other namespaces only); whether SAtenant-engine/tenant-engine-esoexists (not inrapp-tenant-engine/manifests/as of this draft).
3. Proposed changes
| # | Action | Object | Reuse-vs-new rationale |
|---|---|---|---|
| 1 | create | KV v2 path platform/workloads/tenant-engine/audit-core-sender, one field token |
Only a dedicated path can be read by one identity without exposing other senders. Value is a copy of the tenant-engine token already in the registry. |
| 2 | create | policy workload-kv-read-tenant-engine-audit-core-sender-eso: read on platform/data/workloads/tenant-engine/audit-core-sender, plus auth/token/lookup-self read and auth/token/revoke-self update |
Same shape as workload-kv-read-email-connect-transactional-eso. No metadata, no list, no wildcard. |
| 3 | create | Kubernetes auth role tenant-engine-audit-core-sender-eso on auth/kubernetes: SA tenant-engine-eso, namespace tenant-engine, audience openbao, ttl/max_ttl/explicit_max_ttl 15m, token_no_default_policy=true, policy from row 2 |
Not bound to any static token; no AppRole secret_id to rotate. |
| 4 | create (consumer repo) | ServiceAccount tenant-engine/tenant-engine-eso (no mounted token needs, no RBAC) |
Dedicated identity so the role cannot be satisfied by the app's own SA. |
| 5 | create (consumer repo) | ClusterSecretStore openbao-tenant-engine-audit-core-sender: provider vault, server http://openbao.openbao.svc:8200, path: platform, version: v2, auth.kubernetes {mountPath: kubernetes, role: tenant-engine-audit-core-sender-eso, serviceAccountRef {name: tenant-engine-eso, namespace: tenant-engine, audiences: [openbao]}}, conditions: namespaces: [tenant-engine] |
Separate from openbao-tenant-engine-database; namespace-limited. ClusterSecretStore (not namespaced SecretStore) follows the three recovered stores. |
| 6 | create (consumer repo) | ExternalSecret tenant-engine/tenant-engine-audit-core-sender: refreshInterval: 1h, target Secret tenant-engine-audit-core-sender, creationPolicy: Owner, deletionPolicy: Retain, one data entry secretKey: token from workloads/tenant-engine/audit-core-sender property: token |
Produces exactly the Secret/key the Deployment mounts. |
| 7 | seed | the KV value from the registry, in one wrapped platform-admin process: read registry doc, select sender tenant-engine, write token to row 1's path, return only version numbers |
Same technique as the user-engine migration. ops-mason cannot do this: ops-mason-build denies every */data/* read. |
| 8 | propose | ops-warden audit-core-senders playbook: one line naming this lane as the consumer path for tenant-engine |
Pointer only. |
Not proposed: widening external-secrets-audit-core, reusing the database
store or its static token, any AppRole bridge, any bao kv put by the founder.
Where each object is declared (ownership):
- Rows 1-3, 7: OpenBao side. Policy and role are railiance-platform's
declarative surface (policy file under
openbao/policies/, lane entry inopenbao/eso-auth-recovery/lanes.json, or its credential-change request path if that supports Kubernetes-auth roles; to confirm with railiance-platform). ops-mason'sKubernetesKVSpecbuilder can write the role against a pinned existing policy but writes onlybound_service_account_*,policies,ttl; it does not setaudiences,max_ttl/explicit_max_ttlortoken_no_default_policy. Use the railiance-platform lane unless the builder is extended first. - Rows 4-6: production-tier manifests, reconciled by ArgoCD from the rApp
manifest repository (
rapp-tenant-engine/manifests/, next totenant-engine-database-secrets.yaml). ops-mason'splane applyrefuses Secret kinds, cluster-scoped additions beyond its allowlist and production targets, so ops-mason does not apply these.
Draft policy (for the owner to carry; not committed to any repo by ops-mason):
path "platform/data/workloads/tenant-engine/audit-core-sender" {
capabilities = ["read"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "auth/token/revoke-self" {
capabilities = ["update"]
}
4. Review notes (phase 2, to be completed live)
- Least privilege: one path, one field, one SA, one namespace, 15 minute tokens, no default policy. Compromise of the identity discloses only the tenant-engine sender token, which can only append attributed events.
- Expiry: no dependence on
openbao-tenant-engine-eso-tokenor any login token. This lane keeps working after 2026-10-25. The existing database store does not; it needs its own move to the same Kubernetes-auth pattern (policy and roletenant-engine-database-eso). Recommend raising that to railiance-platform separately; it is outside this demand and is not done here. - Duplication cost: the dedicated path is a second copy of the token. The registry remains the authority audit-core reads. Rotation must be overlap-first: add new token to registry (audit-core runbook steps 1-4), then re-run the wrapped seed (row 7), wait for ESO refresh, then drop the old token from the registry. Record this in the audit-core runbook. Option to remove the duplication later: audit-core could read per-sender KV paths instead of one document; that is an audit-core design decision, not part of this plan.
- Optional mount behaviour: the Secret volume is
optional: true, so the pod runs before the Secret exists and kubelet fills the mount once the Secret appears. Whether tenant-engine re-reads the token file per request or only at start is tenant-engine's to confirm; if only at start, a rollout is needed once after first sync. - Naming: follows
workload-kv-read-<consumer>-<purpose>-esoand role = policy-suffix conventions of RPF-WP-0045. - Not in this plan: the token's content, audit-core's registry, the database lane.
5. Executive summary (draft)
One-line ask: approve a single-purpose OpenBao path, read policy and
Kubernetes-auth role so External Secrets can copy only tenant-engine's own
audit-core sender token into tenant-engine/tenant-engine-audit-core-sender.
Who gets access: only External Secrets, as ServiceAccount
tenant-engine/tenant-engine-eso, via role
tenant-engine-audit-core-sender-eso. The application pod keeps seeing a
mounted file.
To what: read of exactly
platform/workloads/tenant-engine/audit-core-sender. Not the sender registry,
not other senders, not the database lane.
For how long: 15 minute tokens from a Kubernetes service-account login; no static token or secret_id exists to expire or leak.
Blast radius if the identity is abused: tenant-engine's own sender token
can be read, which allows writing audit events as tenant-engine. It cannot
read events. Remedy: rotate the sender token (overlap-first), re-seed, delete
the role.
Cost to reverse: delete the role, the policy, the ExternalSecret/store/SA and the KV path. No registry change.
Decision: approve, reject, or send back. Approval authorizes ops-mason to coordinate and verify (metadata only) and the attended seed in row 7. It does not authorize printing the value.
6. Build result
Not started.