Updated by fix-consistency on 2026-10-03: - update .custodian-brief.md for ops-mason Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 292700@bnt-lap001 Assistant-Session: 42de6b79-9019-4cfd-b49a-6db3bbfe9341
12 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 | approved | Bernd Worsch (founder), stated in tenant-engine session | 2026-10-03 | 2026-10-01 | 2026-10-03 |
Construction plan: tenant-engine audit-core sender token projection
APPROVED 2026-10-03 (see approval note below). 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.
Approval record
Approved by Bernd Worsch (founder), stated in the tenant-engine session on 2026-10-03 ("I approve, lets go"). Approval covers declaring the objects in section 3 rows 1-6 and 8 in the owning repos. It does NOT cover any apply, and it does NOT cover the row-7 seed, which still requires the founder's attended wrapped platform-admin step.