diff --git a/plans/tenant-engine-audit-core-sender-projection.md b/plans/tenant-engine-audit-core-sender-projection.md new file mode 100644 index 0000000..f4248a4 --- /dev/null +++ b/plans/tenant-engine-audit-core-sender-projection.md @@ -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---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. diff --git a/workplans/MASON-WP-0007-tenant-engine-audit-core-sender-projection.md b/workplans/MASON-WP-0007-tenant-engine-audit-core-sender-projection.md new file mode 100644 index 0000000..092edc6 --- /dev/null +++ b/workplans/MASON-WP-0007-tenant-engine-audit-core-sender-projection.md @@ -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 +```