railiance-platform/docs/credential-lane-designs/keycape-approval-clients.md
codex f3ba7ca882
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
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 <noreply@anthropic.com>
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
2026-09-08 14:53:32 +02:00

158 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/<workload>/<lane>` 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 -- <command>
```
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 6090 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.