From f3ba7ca88261be888b8b107f837460c9f32328ac Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 8 Sep 2026 14:53:32 +0200 Subject: [PATCH] Admit KeyCape approval-engine client custody paths and delivery Answers KEY-WP-0013-T02. Both proposed KV paths are confirmed unchanged; the field name is corrected to CLIENT_SECRET for the platform uppercase convention and the CCR validator. Kubernetes delivery references are confirmed against the live sso namespace. Attended authority is the governed openbao-platform-admin-login lane, and the rollout is one attended window ordered after the Authelia issuer precondition. Adds CCR-2026-0017/0018, two exact-path read policies, two namespace-limited ClusterSecretStores with Kubernetes auth, two ExternalSecrets, and RPF-WP-0035-T05. Nothing is applied and no value exists: both CCRs remain proposed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WLUjpv3ssxNRAEPPgLFnEB Assistant: claude-code Assistant-Model: opus Assistant-Process: 1275505@bnt-lap001 Assistant-Session: 97265baa-f08f-4032-b290-a1e2965a69c5 --- ...cape-approval-clients.externalsecrets.yaml | 48 +++++ ...e-approval-clients.clustersecretstore.yaml | 61 +++++++ ...eycape-secrets-engine-approval-client.yaml | 171 ++++++++++++++++++ ...ycape-approval-engine-operator-client.yaml | 148 +++++++++++++++ docs/credential-lane-designs/README.md | 5 +- .../keycape-approval-clients.md | 158 ++++++++++++++++ ...-read-keycape-approval-engine-operator.hcl | 7 + ...v-read-keycape-secrets-engine-approval.hcl | 7 + workplans/README.md | 2 +- ...-WP-0035-credential-lane-implementation.md | 47 ++++- 10 files changed, 651 insertions(+), 3 deletions(-) create mode 100644 argocd/platform-addons/openbao-secretstore/keycape-approval-clients.externalsecrets.yaml create mode 100644 argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml create mode 100644 credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml create mode 100644 credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml create mode 100644 docs/credential-lane-designs/keycape-approval-clients.md create mode 100644 openbao/policies/workload-kv-read-keycape-approval-engine-operator.hcl create mode 100644 openbao/policies/workload-kv-read-keycape-secrets-engine-approval.hcl diff --git a/argocd/platform-addons/openbao-secretstore/keycape-approval-clients.externalsecrets.yaml b/argocd/platform-addons/openbao-secretstore/keycape-approval-clients.externalsecrets.yaml new file mode 100644 index 0000000..d70490d --- /dev/null +++ b/argocd/platform-addons/openbao-secretstore/keycape-approval-clients.externalsecrets.yaml @@ -0,0 +1,48 @@ +# CCR-2026-0017 / CCR-2026-0018 (KEY-WP-0013-T02, RPF-WP-0035-T05). +# KeyCape-side delivery of the two approval-engine confidential client secrets. +# The Kubernetes Secret key stays client-secret to match the live +# KEYCAPE_RAPP_QONTO_CLIENT_SECRET secretKeyRef shape; the OpenBao field is +# CLIENT_SECRET because KV field names are uppercase by platform convention. +# +# Apply only after the stores sync and inside the agreed attended rollout window; +# the KeyCape image that reads both environment names rolls out after these sync. +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: keycape-secrets-engine-approval-client + namespace: sso +spec: + refreshInterval: 5m + secretStoreRef: + kind: ClusterSecretStore + name: openbao-keycape-secrets-engine-approval + target: + name: keycape-secrets-engine-approval-client + creationPolicy: Owner + deletionPolicy: Retain + data: + - secretKey: client-secret + remoteRef: + key: workloads/secrets-engine/approval-client + property: CLIENT_SECRET +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: keycape-approval-engine-operator-client + namespace: sso +spec: + refreshInterval: 5m + secretStoreRef: + kind: ClusterSecretStore + name: openbao-keycape-approval-engine-operator + target: + name: keycape-approval-engine-operator-client + creationPolicy: Owner + deletionPolicy: Retain + data: + - secretKey: client-secret + remoteRef: + key: workloads/approval-engine/operator-client + property: CLIENT_SECRET diff --git a/argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml b/argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml new file mode 100644 index 0000000..4b33049 --- /dev/null +++ b/argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml @@ -0,0 +1,61 @@ +# CCR-2026-0017 / CCR-2026-0018 (KEY-WP-0013-T02, RPF-WP-0035-T05). +# Two railiance01-local Kubernetes-auth stores for the KeyCape verifier copies of +# the approval-engine confidential client secrets. Deliberately separate stores, +# roles and policies so either client can be revoked without disturbing the other; +# their scope sets differ and a shared identity would erase that boundary. +# +# Do not apply before both CCRs are approved and the exact-path policies and +# Kubernetes auth roles exist. Both stores are limited to namespace sso and grant +# no access to any other platform workload path. +# +# The existing sso Secret keycape-rapp-qonto-client is hand-created, not managed +# here; adopting it is a separate migration. +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: + name: openbao-keycape-secrets-engine-approval + labels: + app.kubernetes.io/part-of: railiance-gitops + railiance-platform/component: external-secrets + app.kubernetes.io/name: keycape +spec: + provider: + vault: + server: http://openbao.openbao.svc:8200 + path: platform + version: v2 + auth: + kubernetes: + mountPath: kubernetes + role: external-secrets-keycape-secrets-engine-approval + serviceAccountRef: + name: external-secrets + namespace: external-secrets + conditions: + - namespaces: + - sso +--- +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: + name: openbao-keycape-approval-engine-operator + labels: + app.kubernetes.io/part-of: railiance-gitops + railiance-platform/component: external-secrets + app.kubernetes.io/name: keycape +spec: + provider: + vault: + server: http://openbao.openbao.svc:8200 + path: platform + version: v2 + auth: + kubernetes: + mountPath: kubernetes + role: external-secrets-keycape-approval-engine-operator + serviceAccountRef: + name: external-secrets + namespace: external-secrets + conditions: + - namespaces: + - sso diff --git a/credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml b/credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml new file mode 100644 index 0000000..363f079 --- /dev/null +++ b/credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml @@ -0,0 +1,171 @@ +id: CCR-2026-0017 +kind: credential-change-request +schema_version: 1 +request_type: workload-kv-read +title: KeyCape verifier custody for the secrets-engine-approval confidential client +status: proposed +created: '2026-09-08' +updated: '2026-09-08' +requester: + agent: claude + reason: >- + KEY-WP-0013-T02 (State Hub message 278a3ebe-b529-49f6-bd1a-e3ebcf318260) asks + railiance-platform to admit custody for two confidential client_credentials + registrations. This CCR covers the first: client secrets-engine-approval, + subject service:secrets-engine, audience approval-engine, tenant:platform per + resolved decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6, 15m token lifetime. + KeyCape owns the registration and issuance; this request establishes only the + custody path and the KeyCape-side delivery of the client secret it must verify + presented credentials against. It does not admit the client-side lane by which + secrets-engine would read its own copy — that is a separate request with a + separate consumer gate. +review: + required: true + required_approvers: + - platform-operator + - key-cape-owner + comments: + - at: '2026-09-08' + reviewer: railiance-platform (codex/claude) + decision: paths_confirmed_field_corrected + comment: >- + KV path platform/workloads/secrets-engine/approval-client is confirmed + unchanged: it matches the platform/workloads// convention + enforced by scripts/credential-change.py and used by every admitted platform + lane. The proposed field name client_secret is corrected to CLIENT_SECRET; + KV field names are uppercase by convention (PROVISIONER_TOKEN, + CORE_HUB_API_TOKEN) and lowercase names fail FIELD_NAME_RE validation. The + Kubernetes Secret key client-secret and the KeyCape environment name are + confirmed as proposed. +target: + domain: financials + tenant: platform + workload: secrets-engine + environment: production + purpose: >- + Hold the secrets-engine-approval confidential client secret in platform custody + and project it into the KeyCape runtime so KeyCape can verify presented + client_credentials without the value living in Git, a chart value, or a + hand-created Kubernetes Secret. +openbao: + mount: platform + kv_path: platform/workloads/secrets-engine/approval-client + fields: + - CLIENT_SECRET + policy_name: workload-kv-read-keycape-secrets-engine-approval + policy_file: openbao/policies/workload-kv-read-keycape-secrets-engine-approval.hcl + auth: + method: kubernetes + mount: kubernetes + role: external-secrets-keycape-secrets-engine-approval + bound_claims: + service_account_names: + - external-secrets + service_account_namespaces: + - external-secrets + bound_claims_confirmed: true + policies: + - workload-kv-read-keycape-secrets-engine-approval + ttl: 15m +access_frontdoor: + type: ops-warden + catalog_id: keycape-secrets-engine-approval-client + selector: KeyCape secrets-engine-approval confidential client secret + command: warden access keycape-secrets-engine-approval-client --fetch CLIENT_SECRET + resolvable: false + readiness: pending-review +delivery: + surface: external-secrets + target: >- + ClusterSecretStore openbao-keycape-secrets-engine-approval, limited to + namespace sso, to ExternalSecret sso/keycape-secrets-engine-approval-client and + Secret sso/keycape-secrets-engine-approval-client with key client-secret. + KeyCape resolves it as KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET through a + secretKeyRef, matching the live KEYCAPE_RAPP_QONTO_CLIENT_SECRET shape observed + on image main-153258b. Manifests: + argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml + and keycape-approval-clients.externalsecrets.yaml. +risk: + classification: high + notes: + - >- + This is a verifier-side copy. KeyCape holding the client secret lets it + authenticate the client; it does not let KeyCape act as the client, but a leak + of this value allows anyone to present as secrets-engine for approval:read and + approval:consume until the registration is disabled. + - >- + Custody path is under the client's own workload prefix, not KeyCape's, because + the credential belongs to the secrets-engine client identity. The reader + admitted here is the External Secrets Operator on behalf of the sso namespace + only; a future secrets-engine-side read is a separate lane and separate policy. + - >- + The existing keycape-rapp-qonto-client Secret is hand-created and not + ESO-managed. Do not extend this store or ExternalSecret to adopt it; that is a + distinct migration with its own owner. + - >- + The two approval clients are deliberately kept on separate policies, roles and + stores so the operator client can be revoked without disturbing this one. Their + scope sets differ; a shared policy would erase that boundary. + - >- + Compromise response is KeyCape disabling the client registration plus rotation + of this KV version. An OpenBao token expiry does not invalidate an already + issued client secret. +verification: + positive: + - >- + The ExternalSecret in namespace sso syncs CLIENT_SECRET to Secret key + client-secret without printing the value. + - >- + The KeyCape build that reads KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET + starts and issues a token for subject service:secrets-engine with audience + approval-engine, tenant:platform, 15m lifetime, verified against live JWKS + signature. + - Exact claim bindings are asserted by KeyCape without any value or token logged. + negative: + - >- + A namespace outside the approved ClusterSecretStore condition cannot use this + store to read the path. + - >- + A service account outside external-secrets/external-secrets cannot authenticate + through role external-secrets-keycape-secrets-engine-approval. + - >- + The role cannot read the sibling approval-engine operator-client path, any + parent listing, or any other platform workload path. + - >- + The secrets-engine-approval client is denied scopes outside approval:read and + approval:consume, and human identities are denied approval:consume. + activation_conditions: + - >- + KeyCape and railiance-platform agree the single attended rollout window in + docs/credential-lane-designs/keycape-approval-clients.md; the KeyCape image that + reads both environment names is built and pinned but not yet deployed. + - >- + Attended first provision runs only through the governed + openbao-platform-admin-login lane (founder_required, attended OIDC via + netkingdom role=platform-admin), with a unique receipt path and no value in + chat, command arguments or shell history. + - >- + Policy, Kubernetes auth role and ClusterSecretStore applied before the + ExternalSecret; sync confirmed before the KeyCape image is rolled out. + - >- + Positive and negative results recorded with non-secret request ids or + timestamps. + evidence: [] +lifecycle: + deactivate: >- + KeyCape disables the secrets-engine-approval registration; platform detaches + the policy from role external-secrets-keycape-secrets-engine-approval and + removes the ExternalSecret. The KV version is retained until KeyCape confirms + disablement. + rotate: >- + KeyCape mints a replacement client secret; platform writes the new KV version + under the same attended authority; ESO refresh delivers it and KeyCape restarts + or re-reads. Rotation is independent of the operator client. + compromised: >- + Disable the registration at KeyCape first (that is what stops token issuance), + then rotate the KV version and confirm no other namespace consumed the store. +state_hub: + workplan_id: RPF-WP-0035 + task_id: RPF-WP-0035-T05 + related_message: 278a3ebe-b529-49f6-bd1a-e3ebcf318260 + related_decision: 5ed3fb35-eca9-413a-82b9-95171ba85bf6 diff --git a/credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml b/credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml new file mode 100644 index 0000000..ec2662a --- /dev/null +++ b/credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml @@ -0,0 +1,148 @@ +id: CCR-2026-0018 +kind: credential-change-request +schema_version: 1 +request_type: workload-kv-read +title: KeyCape verifier custody for the approval-engine-operator confidential client +status: proposed +created: '2026-09-08' +updated: '2026-09-08' +requester: + agent: claude + reason: >- + Second of the two registrations in KEY-WP-0013-T02 (State Hub message + 278a3ebe-b529-49f6-bd1a-e3ebcf318260): client approval-engine-operator, subject + service:approval-engine-operator, audience approval-engine, tenant:platform per + resolved decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6, 15m token lifetime. + Scopes are approval:create, read, approve, revoke, supersede, observe and emit, + explicitly without approval:consume. KeyCape owns registration and issuance; + this request establishes only custody and the KeyCape-side delivery. +review: + required: true + required_approvers: + - platform-operator + - key-cape-owner + comments: + - at: '2026-09-08' + reviewer: railiance-platform (codex/claude) + decision: paths_confirmed_field_corrected + comment: >- + KV path platform/workloads/approval-engine/operator-client is confirmed + unchanged and conforms to the platform/workloads// + convention. Field client_secret is corrected to CLIENT_SECRET for the same + uppercase convention and validator constraint as CCR-2026-0017. Kubernetes + Secret sso/keycape-approval-engine-operator-client with key client-secret and + the KeyCape environment name are confirmed as proposed. +target: + domain: financials + tenant: platform + workload: approval-engine + environment: production + purpose: >- + Hold the approval-engine-operator confidential client secret in platform + custody and project it into the KeyCape runtime so KeyCape can verify presented + client_credentials for the operator client. +openbao: + mount: platform + kv_path: platform/workloads/approval-engine/operator-client + fields: + - CLIENT_SECRET + policy_name: workload-kv-read-keycape-approval-engine-operator + policy_file: openbao/policies/workload-kv-read-keycape-approval-engine-operator.hcl + auth: + method: kubernetes + mount: kubernetes + role: external-secrets-keycape-approval-engine-operator + bound_claims: + service_account_names: + - external-secrets + service_account_namespaces: + - external-secrets + bound_claims_confirmed: true + policies: + - workload-kv-read-keycape-approval-engine-operator + ttl: 15m +access_frontdoor: + type: ops-warden + catalog_id: keycape-approval-engine-operator-client + selector: KeyCape approval-engine-operator confidential client secret + command: warden access keycape-approval-engine-operator-client --fetch CLIENT_SECRET + resolvable: false + readiness: pending-review +delivery: + surface: external-secrets + target: >- + ClusterSecretStore openbao-keycape-approval-engine-operator, limited to + namespace sso, to ExternalSecret sso/keycape-approval-engine-operator-client and + Secret sso/keycape-approval-engine-operator-client with key client-secret. + KeyCape resolves it as KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET through a + secretKeyRef. Manifests: + argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml + and keycape-approval-clients.externalsecrets.yaml. +risk: + classification: high + notes: + - >- + This client carries the widest approval scope set in the pair — create, + approve, revoke and supersede. A leak allows forging approval lifecycle actions, + which is a stronger outcome than the read/consume client in CCR-2026-0017. + - >- + The absence of approval:consume is a deliberate separation of duties. Any later + request to add consume to this client is a new lane decision, not a scope edit. + - >- + Kept on its own policy, role and store so it can be revoked independently of the + secrets-engine-approval client. + - >- + Compromise response is KeyCape disabling the registration plus rotation of this + KV version; approval actions already emitted are not retracted by rotation and + need approval-engine's own audit review. +verification: + positive: + - >- + The ExternalSecret in namespace sso syncs CLIENT_SECRET to Secret key + client-secret without printing the value. + - >- + The KeyCape build issues a token for subject service:approval-engine-operator + with audience approval-engine, tenant:platform and 15m lifetime, verified + against live JWKS signature. + negative: + - >- + A namespace outside the approved ClusterSecretStore condition cannot use this + store to read the path. + - >- + A service account outside external-secrets/external-secrets cannot authenticate + through role external-secrets-keycape-approval-engine-operator. + - >- + The role cannot read the sibling secrets-engine approval-client path, any parent + listing, or any other platform workload path. + - >- + The operator client is denied approval:consume, and denial is observed rather + than assumed. + activation_conditions: + - >- + Same single attended rollout window and prepared-but-undeployed KeyCape image as + CCR-2026-0017; see docs/credential-lane-designs/keycape-approval-clients.md. + - >- + Attended first provision only through openbao-platform-admin-login + (founder_required, attended OIDC via netkingdom role=platform-admin) with a + unique receipt path. + - >- + Policy, Kubernetes auth role and ClusterSecretStore applied before the + ExternalSecret; sync confirmed before the KeyCape rollout. + - Positive and negative results recorded with non-secret request ids or timestamps. + evidence: [] +lifecycle: + deactivate: >- + KeyCape disables the approval-engine-operator registration; platform detaches the + policy from role external-secrets-keycape-approval-engine-operator and removes + the ExternalSecret. + rotate: >- + KeyCape mints a replacement client secret; platform writes the new KV version + under the same attended authority. Rotation is independent of CCR-2026-0017. + compromised: >- + Disable the registration at KeyCape, rotate the KV version, and refer emitted + approval actions to approval-engine for audit review. +state_hub: + workplan_id: RPF-WP-0035 + task_id: RPF-WP-0035-T05 + related_message: 278a3ebe-b529-49f6-bd1a-e3ebcf318260 + related_decision: 5ed3fb35-eca9-413a-82b9-95171ba85bf6 diff --git a/docs/credential-lane-designs/README.md b/docs/credential-lane-designs/README.md index 9acfbbf..f617ffb 100644 --- a/docs/credential-lane-designs/README.md +++ b/docs/credential-lane-designs/README.md @@ -2,13 +2,16 @@ Reviewed against local owner source on 2026-09-05. These are proposed designs, not approvals or executable CCRs. No live credentials or OpenBao objects were -created. Files here are deliberately outside the production CCR/policy scan. +created. Files here are deliberately outside the production CCR/policy scan, except the +KeyCape approval-client admission below, which carries executable CCRs +(CCR-2026-0017/0018) that remain `proposed` and unapplied. | Design | Design workplan / current implementation task | Consumer dependency | Main unresolved input | | --- | --- | --- | --- | | [Secrets-engine service JWT](secrets-engine-service-jwt.md) | RPF-WP-0032 (finished) / RPF-WP-0035-T02 | SECRETS-WP-0008-T06; SECRETS-WP-0007-T04 | Actual issuer/JWKS, live registration and scoped execution authority | | [Fluid-telegram operator KV](fluid-telegram-operator-kv.md) | RPF-WP-0033 (finished) / RPF-WP-0035-T03 | MASON-WP-0005; FT-WP-0002 | Tenant acceptance, actual OIDC group, write-capable CCR support | | [State Hub preflight signing](state-hub-preflight-signing.md) | RPF-WP-0034 (finished) / RPF-WP-0035-T04 | FLEX-WP-0020-T05; STATE-WP-0085-T09 delivered | Deployment binding, owner-approved custody and rotation window | +| [KeyCape approval-engine clients](keycape-approval-clients.md) | RPF-WP-0035-T05 | KEY-WP-0013-T02; GLAS-WP-0015 | Owner approval of CCR-2026-0017/0018, attended founder window, KeyCape reading build | The completed design workplans are archived. RPF-WP-0035 is the canonical queue for owner review, implementation, and live acceptance still required. diff --git a/docs/credential-lane-designs/keycape-approval-clients.md b/docs/credential-lane-designs/keycape-approval-clients.md new file mode 100644 index 0000000..aca1364 --- /dev/null +++ b/docs/credential-lane-designs/keycape-approval-clients.md @@ -0,0 +1,158 @@ +# KeyCape approval-engine client custody admission + +Answer to KEY-WP-0013-T02 (State Hub message +`278a3ebe-b529-49f6-bd1a-e3ebcf318260`, KeyCape packet +`key-cape: docs/approval-engine-provisioning-request.yaml`). Tracked here as +RPF-WP-0035-T05. Requests: [CCR-2026-0017](../../credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml), +[CCR-2026-0018](../../credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml). + +Nothing below is an activation. Both CCRs are `proposed`; no value has been +generated, no KV version written, no manifest applied. Source preparation is not +live completion. + +## a) Custody paths and field names + +| Item | Proposed by KeyCape | Admitted | Change | +| --- | --- | --- | --- | +| Client 1 path | `platform/workloads/secrets-engine/approval-client` | same | confirmed | +| Client 2 path | `platform/workloads/approval-engine/operator-client` | same | confirmed | +| Field | `client_secret` | `CLIENT_SECRET` | **corrected** | + +Both paths are correct as proposed. They match the +`platform/workloads//` shape that `scripts/credential-change.py` +enforces for the `platform` mount and that every admitted platform lane uses +(`platform/workloads/core-hub/runtime`, `platform/workloads/user-engine/runtime`, +`platform/workloads/telemetry/grafana-admin`). Naming each path under the client's +own workload prefix rather than under KeyCape is also right: the credential +belongs to the client identity; KeyCape is its verifier, not its owner. + +The field name is the one correction. KV field names on this mount are uppercase +(`PROVISIONER_TOKEN`, `CORE_HUB_API_TOKEN`, `NPM_AUTH_TOKEN`), and the CCR +validator rejects anything that does not match `^[A-Z][A-Z0-9_]*$`. So the +OpenBao field is `CLIENT_SECRET`. This does not change the Kubernetes Secret key +or the environment name — see below. + +## b) Kubernetes delivery references + +Confirmed as proposed, with the mapping made explicit: + +| | Client 1 | Client 2 | +| --- | --- | --- | +| Secret | `sso/keycape-secrets-engine-approval-client` | `sso/keycape-approval-engine-operator-client` | +| Secret key | `client-secret` | `client-secret` | +| KeyCape env | `KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET` | `KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET` | +| ClusterSecretStore | `openbao-keycape-secrets-engine-approval` | `openbao-keycape-approval-engine-operator` | +| OpenBao policy | `workload-kv-read-keycape-secrets-engine-approval` | `workload-kv-read-keycape-approval-engine-operator` | +| Kubernetes auth role | `external-secrets-keycape-secrets-engine-approval` | `external-secrets-keycape-approval-engine-operator` | + +`OpenBao CLIENT_SECRET` → `ExternalSecret secretKey client-secret` → +`secretKeyRef` → env. That shape is the one already running in `sso`: the live +`keycape` pod on `main-153258b` resolves +`KEYCAPE_RAPP_QONTO_CLIENT_SECRET` from `secretKeyRef{name: +keycape-rapp-qonto-client, key: client-secret}`. Read-only observation +2026-09-08, and it confirms KeyCape's own statement that the image carries only +that one client reference. + +Manifests are written and client-validated but unapplied: +`argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml` +and `keycape-approval-clients.externalsecrets.yaml`. + +Three deliberate choices, so they are not read as accidents: + +- **Two stores, two roles, two policies.** The scope sets differ — the operator + client can create, approve, revoke and supersede; the secrets-engine client can + read and consume. Separate identities mean either can be revoked without + touching the other. A shared store would erase that boundary. +- **Kubernetes auth, not AppRole.** The neighbouring `sso` store + (`openbao-sso-user-engine-runtime`) and `openbao-rapp-qonto` use AppRole for + cutover reasons. New same-cluster lanes use Kubernetes auth against + `external-secrets/external-secrets`, as `openbao-core-hub-runtime` does, so + there is no AppRole secret to hold in custody. +- **Verifier side only.** This admits the copy KeyCape needs to verify presented + credentials. It does **not** admit the client-side lane by which secrets-engine + or the approval-engine operator would read its own copy. That is a separate + request with its own consumer gate, and no part of it is implied here. + +`creationPolicy: Owner` means the ExternalSecret creates the Secret. Do not +pre-create either Secret by hand. + +## c) Authority for the attended first provision + +Your reading is right, and your refusal to act on the `warden plan` result was +also right: a generic database-lane match is not authorization for this path. + +The authority is the governed **`openbao-platform-admin-login`** lane — +`founder_required`, attended OIDC through `bao login -method=oidc -path=netkingdom +role=platform-admin`, invoked as: + +```bash +warden access openbao-platform-admin-login --exec -- +``` + +with a unique `--receipt` path. Nothing else in this repo carries write authority +against `platform/workloads/`. Two constraints that come with it: + +- It is a **founder-attended** session. No agent, including this one, can execute + it unattended, and neither KeyCape nor railiance-platform can self-authorize it. +- The owner command is never run directly, only through the `warden access + --exec` envelope, so output containment applies (RPF-WP-0017). No value in chat, + command arguments, shell history or logs; receipts carry metadata only. + +Custody writes are seeded with compare-and-set zero on first version. + +## d) Rollout window + +Agreed in shape: **one attended session**, everything below in order, no partial +landing. Registrations and the reading build deploy together because a KeyCape +image that reads an absent env fails closed, and a delivered secret with no build +to read it is custody without a consumer. + +Prerequisites before the window opens: + +1. Both CCRs approved by platform-operator and the KeyCape owner. +2. KeyCape image that reads both environment names is **built and pinned, not + deployed**. `main-153258b` is not that image. +3. Founder available for the attended OpenBao session. +4. **The Authelia issuer precondition from KeyCape message + `c8b1ad10-dae8-48fb-a0ea-7e2a101c54bf` is settled first.** The same rollout + that lands these clients also lands upstream ID-token verification that fails + closed on issuer mismatch. Confirm the `iss` value and pin `authelia.issuer` + in the KeyCape config secret before, not during, this window. A broken human + login and a broken client registration arriving together would be very hard to + tell apart. + +In-window order: + +1. Apply the two policies and the two Kubernetes auth roles (attended, via the + authority in (c)). +2. Seed both KV paths, CAS=0, with the client secrets KeyCape mints. Record + metadata-only receipts. +3. Apply the two ClusterSecretStores; confirm both reach `Valid`. +4. Apply the two ExternalSecrets; confirm `SecretSynced` and that both Secrets + exist with key `client-secret`. No value is printed at any point. +5. Roll out the KeyCape image. +6. KeyCape runs its verification: live JWKS signature, exact claim bindings + (subject, tenant `tenant:platform`, audience `approval-engine`, 15m lifetime), + and denial of excess scopes — operator `consume` and human `consume` — without + logging any value or token. +7. Platform runs its negative checks: cross-path read denied, wrong service + account denied, out-of-namespace store use denied, parent listing denied. +8. Both sides record evidence into the CCRs and a receipt into the workplan. + +Rollback: revert the KeyCape image to `main-153258b`, delete the two +ExternalSecrets, detach the policies from the roles. The KV versions are retained +until KeyCape confirms whether the registrations stay; if they are abandoned, +KeyCape disables the registrations first and platform then destroys the versions. + +**Date is not set here.** It depends on the founder's availability, which is not +mine to schedule. Propose a slot from 2026-09-10 and I will confirm the +platform side; the window needs roughly 60–90 minutes with both owners present. + +## What is not admitted + +- No client-side (secrets-engine / approval-engine) read lane. +- No adoption of the hand-created `sso/keycape-rapp-qonto-client` Secret into ESO. +- No change to the separate `codex-railiance-platform` JWT issuance contract + (KEY-WP-0009-T04) or to RPF-WP-0035-T02's service-JWT lane. +- No approval for `approval:consume` on the operator client, now or by later + scope edit. Adding it is a new lane decision. diff --git a/openbao/policies/workload-kv-read-keycape-approval-engine-operator.hcl b/openbao/policies/workload-kv-read-keycape-approval-engine-operator.hcl new file mode 100644 index 0000000..09a6ec2 --- /dev/null +++ b/openbao/policies/workload-kv-read-keycape-approval-engine-operator.hcl @@ -0,0 +1,7 @@ +path "platform/data/workloads/approval-engine/operator-client" { + capabilities = ["read"] +} + +path "platform/metadata/workloads/approval-engine/operator-client" { + capabilities = ["read"] +} diff --git a/openbao/policies/workload-kv-read-keycape-secrets-engine-approval.hcl b/openbao/policies/workload-kv-read-keycape-secrets-engine-approval.hcl new file mode 100644 index 0000000..e919636 --- /dev/null +++ b/openbao/policies/workload-kv-read-keycape-secrets-engine-approval.hcl @@ -0,0 +1,7 @@ +path "platform/data/workloads/secrets-engine/approval-client" { + capabilities = ["read"] +} + +path "platform/metadata/workloads/secrets-engine/approval-client" { + capabilities = ["read"] +} diff --git a/workplans/README.md b/workplans/README.md index 118da3d..38028a7 100644 --- a/workplans/README.md +++ b/workplans/README.md @@ -11,7 +11,7 @@ plans is not a count of missing implementations or independent incidents. | [RPF-WP-0029](RPF-WP-0029-backup-credential-default-removal.md) | Backup cutover and full offsite application recovery complete; old share invalidation receipt remains | S3 retains custody acceptance; S1 and forge own their backup execution. | | [RPF-WP-0025](RPF-WP-0025-openbao-operator-only-access.md) | Private OpenBao access; fresh attended callback/login then guarded retraction | Coordinate package, issuer, tunnel and DNS owners; keep the window separate. | | [RPF-WP-0015](RPF-WP-0015-audit-core-custody-and-recovery-coordination.md) | Two prepared recovery exercises; registered load driver exists; fresh sender/window/abort approvals and custody readiness remain | S3 contributes lease/ESO and snapshot/unseal proof; S1/S2 and audit-core execute their parts. | -| [RPF-WP-0035](RPF-WP-0035-credential-lane-implementation.md) | Two remaining lanes: secrets-engine JWT and Fluid operator KV | Signing T04 is complete; JWT and Fluid retain separate issuer/consumer gates. | +| [RPF-WP-0035](RPF-WP-0035-credential-lane-implementation.md) | Three remaining lanes: secrets-engine JWT, Fluid operator KV, KeyCape approval clients | Signing T04 is complete; T05 admission answered and awaiting owner approval plus a founder-attended window. | | [RPF-WP-0036](RPF-WP-0036-platform-service-assurance.md) | Implemented local assurance/admission; waits for recurring restore evidence, Q2 reception and owner handoff | Run the assurance commands; live acceptance and external ownership remain gated. | | [RPF-WP-0038](RPF-WP-0038-forgejo-scaleway-primary-coverage.md) | Native backup, full Scaleway archive recovery and 273 MiB Nextcloud essentials recovery verified; scheduled tier cutover remains | Bind recurring caller/dependencies, verified inventory, quota checks and separate owner retention. | diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md index 750bc70..7adf8ea 100644 --- a/workplans/RPF-WP-0035-credential-lane-implementation.md +++ b/workplans/RPF-WP-0035-credential-lane-implementation.md @@ -7,7 +7,7 @@ repo: railiance-platform status: blocked owner: codex created: "2026-09-05" -updated: "2026-09-06" +updated: "2026-09-08" related: - RPF-WP-0032 - RPF-WP-0033 @@ -141,6 +141,51 @@ chart commit `49e3182`, Helm revision 59. No repository rename executed. Evidence: `docs/evidence/RPF-WP-0035-T04-signing-activation-2026-09-05.json`; closure: `history/2026-09-05-preflight-signing-activation-complete.md`. +## Admit KeyCape approval-engine client custody and delivery + +```task +id: RPF-WP-0035-T05 +status: wait +priority: high +``` + +Answers KEY-WP-0013-T02 (State Hub message +`278a3ebe-b529-49f6-bd1a-e3ebcf318260`). Admission: +`docs/credential-lane-designs/keycape-approval-clients.md`; requests +`CCR-2026-0017` (secrets-engine-approval) and `CCR-2026-0018` +(approval-engine-operator). + +Platform owns the two custody paths, exact-path read policies, Kubernetes auth +roles and the KeyCape-side ESO delivery. KeyCape owns the client registrations, +issuance, claim set and disablement; approval-engine owns approval semantics and +audit of emitted actions. + +2026-09-08 admission response: both proposed KV paths confirmed unchanged; field +`client_secret` corrected to `CLIENT_SECRET` for the uppercase KV convention and +CCR validator. Kubernetes delivery references confirmed against the live `sso` +namespace, which already resolves `KEYCAPE_RAPP_QONTO_CLIENT_SECRET` through the +same `secretKeyRef{key: client-secret}` shape on image `main-153258b`. Attended +authority named as the governed `openbao-platform-admin-login` lane +(`founder_required`); KeyCape's refusal to treat its generic `warden plan` +database match as authorization was correct. Rollout agreed as one attended +session with the KeyCape reading build pinned but undeployed, ordered after the +Authelia issuer precondition in message `c8b1ad10-dae8-48fb-a0ea-7e2a101c54bf`. +Policies, ClusterSecretStores and ExternalSecrets are written and validated, +none applied. + +**Unblock:** owner approval of both CCRs, a KeyCape image that reads both +environment names, the settled Authelia `iss` pin, and a founder-attended window +date. Verifier-side custody only — no client-side read lane is admitted, and no +scope beyond the two declared sets. + +**Done when:** both CCRs are approved, the attended window seeds both KV paths +with CAS=0, policies/roles/stores/ExternalSecrets apply and sync, the KeyCape +build resolves both environment names, KeyCape evidences live JWKS signature and +exact claim bindings with excess scopes denied (operator `consume`, human +`consume`), platform evidences cross-path/wrong-identity/out-of-namespace denial, +and every receipt is metadata-only. If either registration is abandoned, KeyCape +disables it before the KV version is destroyed. + ## Dependency review — 2026-09-06 SECRETS-WP-0008-T02 now records the local PIP claim/validation join implemented