MASON-WP-0007: plan tenant-engine audit-core sender token projection (draft, not applied)

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
This commit is contained in:
tegwick 2026-10-01 21:38:01 +02:00
parent 98a3e2ded8
commit 2149ee2718
2 changed files with 298 additions and 0 deletions

View file

@ -0,0 +1,185 @@
---
id: tenant-engine-audit-core-sender-projection
demand_source: tenant-engine/TEN-IN-0005
consumer_repo: tenant-engine
credential_type: openbao-kubernetes-kv
status: draft
approved_by: null
approved_at: null
created: "2026-10-01"
updated: "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. 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.

View file

@ -0,0 +1,113 @@
---
id: MASON-WP-0007
type: workplan
title: "Project the tenant-engine audit-core sender token into its namespace"
domain: infotech
repo: ops-mason
status: blocked
flavor: planning
owner: claude
topic_slug: netkingdom
created: "2026-10-01"
updated: "2026-10-01"
---
# MASON-WP-0007 — Project the tenant-engine audit-core sender token
## Goal
Deliver the tenant-engine sender token, and only that token, as Secret
`tenant-engine-audit-core-sender` key `token` in namespace `tenant-engine`,
through External Secrets using Kubernetes auth (no login-token dependency),
without any agent or founder handling the value.
Demand source: tenant-engine `TEN-IN-0005`; audit-core mint receipt
`audit-core/docs/evidence/2026-09-15-tenant-engine-sender-mint.json`.
Construction plan: `plans/tenant-engine-audit-core-sender-projection.md`
(draft). Routing entry: `audit-core-senders`.
## Boundaries
- No OpenBao write and no cluster apply happens in this workplan until the
plan is approved. No secret value is read or recorded anywhere.
- The registry path `platform/workloads/audit-core/senders` is audit-core's
authority and is not widened. The existing `openbao-tenant-engine-database`
store and its static token are not reused.
- Policy and role are declared by railiance-platform; the SA, store and
ExternalSecret by the tenant-engine rApp manifests. ops-mason plans, verifies
structure by metadata, and records evidence.
## Tasks
## Complete the live survey and review the plan
```task
id: MASON-WP-0007-T01
status: todo
priority: high
```
With a scoped `./scripts/bao-session.sh grant MASON-WP-0007-T01` session,
confirm metadata only: `platform/workloads/tenant-engine/` is unused, no
conflicting policy/role names, `auth/kubernetes` role shape of
`email-connect-transactional-eso`. Ask railiance-platform whether the CCR path
or `lanes.json` is the right surface for a Kubernetes-auth role, and whether
ESO may create SA tokens in `tenant-engine`. Then render `build_spec` and its
digest if the ops-mason builder is to be used.
## Approve the plan
```task
id: MASON-WP-0007-T02
status: wait
needs_human: true
depends_on: [MASON-WP-0007-T01]
blocked_on: founder
blocking_reason: "Founder approval of the construction plan (status: approved, approved_by, approved_at)."
priority: high
```
## Policy and Kubernetes-auth role declared by railiance-platform
```task
id: MASON-WP-0007-T03
status: wait
depends_on: [MASON-WP-0007-T02]
blocked_on: message-from:railiance-platform
blocking_reason: "railiance-platform declares policy workload-kv-read-tenant-engine-audit-core-sender-eso and role tenant-engine-audit-core-sender-eso (ESO Kubernetes auth lane), or confirms the ops-mason builder may write the role."
priority: high
```
## Manifests in the tenant-engine rApp repository
```task
id: MASON-WP-0007-T04
status: wait
depends_on: [MASON-WP-0007-T02]
blocked_on: message-from:tenant-engine
blocking_reason: "tenant-engine/rapp-tenant-engine declares SA tenant-engine-eso, ClusterSecretStore openbao-tenant-engine-audit-core-sender and ExternalSecret tenant-engine-audit-core-sender per the plan, reconciled by ArgoCD."
priority: high
```
## Attended seed of the dedicated KV path
```task
id: MASON-WP-0007-T05
status: wait
needs_human: true
depends_on: [MASON-WP-0007-T03]
blocked_on: founder
blocking_reason: "Wrapped platform-admin process copies the tenant-engine token from the registry document to platform/workloads/tenant-engine/audit-core-sender without printing it; ops-mason-build is denied value reads, so only an attended admin lane can do it."
priority: high
```
## Verify and hand off
```task
id: MASON-WP-0007-T06
status: wait
depends_on: [MASON-WP-0007-T03, MASON-WP-0007-T04, MASON-WP-0007-T05]
blocked_on: other-task
blocking_reason: "Needs T03-T05. Verify by metadata only: KV version exists, store Ready, ExternalSecret SecretSynced, Secret exists with key name token; negative check that the role cannot read the registry path. Add the ops-warden audit-core-senders pointer line and tell tenant-engine."
priority: medium
```