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
193 lines
12 KiB
Markdown
193 lines
12 KiB
Markdown
---
|
|
id: tenant-engine-audit-core-sender-projection
|
|
demand_source: tenant-engine/TEN-IN-0005
|
|
consumer_repo: tenant-engine
|
|
credential_type: openbao-kubernetes-kv
|
|
status: approved
|
|
approved_by: Bernd Worsch (founder), stated in tenant-engine session
|
|
approved_at: "2026-10-03"
|
|
created: "2026-10-01"
|
|
updated: "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. Reusing `external-secrets-audit-core` (reads that path) or pointing
|
|
an ExternalSecret `property:` at it would let the tenant-engine identity read
|
|
every other sender's token, including the read-capable `operator` token.
|
|
Rejected. ESO `property:` / `template` filtering is a delivery convenience,
|
|
not an access boundary.
|
|
- **`openbao-tenant-engine-database` is the wrong store to extend.**
|
|
(`rapp-tenant-engine/manifests/tenant-engine-database-secrets.yaml`) It is a
|
|
ClusterSecretStore on mount `database`, authenticated by static token Secret
|
|
`external-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-connect` and `openbao-activity-core` to
|
|
ESO `auth.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 (exact `platform/data/...` read + `auth/token/lookup-self` read +
|
|
`auth/token/revoke-self` update). `auth/kubernetes/config` uses the OpenBao
|
|
pod's own token, so there is no reviewer JWT or login token to expire. The
|
|
same pattern serves `openbao-forgejo`, `openbao-reuse`,
|
|
`openbao-target-revenue`. Lane records live in
|
|
`railiance-platform/openbao/eso-auth-recovery/lanes.json`; policies in
|
|
`railiance-platform/openbao/policies/workload-kv-read-*-eso.hcl`.
|
|
- **Reference shapes in ops-mason.** `audit-core-openbao-runtime-custody` and
|
|
`user-engine-openbao-runtime-custody` used AppRole bridges (secret_id bootstrap
|
|
Secrets in `external-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 may `create serviceaccounts/token` in
|
|
`tenant-engine` (RPF-WP-0045 confirmed this for two other namespaces only);
|
|
whether SA `tenant-engine/tenant-engine-eso` exists (not in
|
|
`rapp-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 in
|
|
`openbao/eso-auth-recovery/lanes.json`, or its credential-change request
|
|
path if that supports Kubernetes-auth roles; to confirm with
|
|
railiance-platform). ops-mason's `KubernetesKVSpec` builder can write the role
|
|
against a pinned existing policy but writes only
|
|
`bound_service_account_*`, `policies`, `ttl`; it does not set `audiences`,
|
|
`max_ttl`/`explicit_max_ttl` or `token_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 to
|
|
`tenant-engine-database-secrets.yaml`). ops-mason's `plane apply` refuses
|
|
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):
|
|
|
|
```hcl
|
|
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-token` or 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 role `tenant-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>-eso` and 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.
|