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:
parent
98a3e2ded8
commit
2149ee2718
2 changed files with 298 additions and 0 deletions
185
plans/tenant-engine-audit-core-sender-projection.md
Normal file
185
plans/tenant-engine-audit-core-sender-projection.md
Normal 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.
|
||||
|
|
@ -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
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue