feat: exchange scoped approval service tokens per request
Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
3a19069b4b
commit
7688445184
14 changed files with 859 additions and 35 deletions
83
docs/approval-service-auth.md
Normal file
83
docs/approval-service-auth.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Approval consumer identity
|
||||
|
||||
Implemented for SECRETS-WP-0009-T03 on 2026-09-09. KeyCape's live verifier
|
||||
registration is accepted under KEY-WP-0013-T02 and CCR-2026-0017; this consumer
|
||||
implementation does not grant retrieval of its confidential client secret.
|
||||
|
||||
The CLI exchanges a separately supplied `secrets-engine-approval` client secret
|
||||
for a fresh access token immediately before each approval HTTP request. Claim
|
||||
requests ask for `approval:read`; consume requests ask for `approval:consume`.
|
||||
The token exists only in memory for that request. There is no refresh token,
|
||||
access-token file, background renewal, retry loop, or fallback identity.
|
||||
|
||||
| Input | Value |
|
||||
| --- | --- |
|
||||
| `SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE` | Explicit, protected temporary file outside Git, no group/other access |
|
||||
| `SECRETS_ENGINE_KEYCAPE_ISSUER` | `https://kc.coulomb.social` |
|
||||
| `SECRETS_ENGINE_KEYCAPE_TOKEN_URL` | `https://kc.coulomb.social/token` |
|
||||
| `SECRETS_ENGINE_APPROVAL_URL` | Admitted HTTPS endpoint or literal loopback owner tunnel |
|
||||
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | Unset when using the client-secret provider |
|
||||
|
||||
The existing token-file provider remains explicitly selectable by configuring
|
||||
only `SECRETS_ENGINE_APPROVAL_TOKEN_FILE`. Configuring both providers fails
|
||||
before either credential is read. An exchange failure never reads that file,
|
||||
`BAO_TOKEN`, or `SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE` (the different
|
||||
OpenBao identity's credential).
|
||||
|
||||
The approval profile fixes client `secrets-engine-approval`, audience
|
||||
`approval-engine`, subject `service:secrets-engine`, tenant `tenant:platform`,
|
||||
role `secrets-engine`, service principal, empty groups and one requested scope.
|
||||
It enforces RS256 shape, at most 900 seconds of life, strict expiry and at most
|
||||
30 seconds of future issue time. The KeyCape assurance shape is
|
||||
`level: aal1`, `methods: [client_secret]`, `mfa: false`, `source: key-cape`.
|
||||
The same wire-shape correction applies to the existing OpenBao exchange; its
|
||||
client, audience, tenant and authorization remain separate.
|
||||
|
||||
Claim parsing is a preflight, not signature verification or authorization.
|
||||
Approval Engine verifies signature, issuer, audience and claims against JWKS.
|
||||
The existing exact-action claim, access-engine decision and atomic consume gates
|
||||
still precede a production OpenBao operation.
|
||||
|
||||
Credentials are sent through a non-redirecting transport. The KeyCape endpoint
|
||||
must be the configured HTTPS issuer's `/token`. The new consumer refuses
|
||||
cluster Service DNS from this workstation and plaintext non-loopback approval
|
||||
endpoints. A literal loopback address alone cannot identify the responder: the
|
||||
operator must establish a `kubectl port-forward` for the admitted cluster and
|
||||
named Approval Engine pod. No tunnel or cluster deployment is created here.
|
||||
|
||||
## Reproducible component exercise
|
||||
|
||||
Run from this repository with Docker available and the owner source checkouts:
|
||||
|
||||
```bash
|
||||
uv run --with 'PyJWT[crypto]>=2.7,<3' python tools/exercise_approval_identity.py \
|
||||
--keycape-source /home/worsch/key-cape \
|
||||
--approval-engine-source /home/worsch/approval-engine \
|
||||
--receipt /tmp/<new-metadata-receipt>.json
|
||||
```
|
||||
|
||||
The exercise starts the immutable admitted KeyCape image behind local HTTPS,
|
||||
uses synthetic signing keys/client secrets, and serves the real Approval Engine
|
||||
source with its JWT/JWKS verifier and SQLite store. It drives the actual Secrets
|
||||
Engine production gate, with only the PDP represented by a sequencing double.
|
||||
Operator create/approve, consumer claim/consume, same-digest retry, different
|
||||
digest, spent claim, wrong action, wrong secret and operator-scope denial are
|
||||
checked. All temporary processes and synthetic credentials are removed.
|
||||
|
||||
This exercise caught the previous fixture-only `aal`/`method` assumption:
|
||||
the real issuer has always emitted `level`/`methods`. The regression suite now
|
||||
refuses that obsolete shape. See
|
||||
[the component receipt](evidence/2026-09-09-approval-identity-exercise.json).
|
||||
|
||||
## Live handoff
|
||||
|
||||
RPF-WP-0035-T06 owns the separate client-side read admission for the existing
|
||||
`platform/workloads/secrets-engine/approval-client`, field `CLIENT_SECRET`.
|
||||
The admission must name the actual consumer/placement, bounded reader, protected
|
||||
temporary delivery and cleanup, refusal/revocation checks and lifecycle owner.
|
||||
Do not use the verifier's `sso` Secret or reseed version-1 custody.
|
||||
|
||||
Approval Engine's operator is a separate identity and read lane. Audit receiver
|
||||
and sender custody remains AUDIT-WP-0009-T09 / APPROVAL-WP-0002-T01. The live
|
||||
Approval Engine deployment and native OpenBao delivery remain open. Synthetic
|
||||
acceptance grants no production access, action approval or model spending.
|
||||
Loading…
Add table
Add a link
Reference in a new issue