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
|
|
@ -154,7 +154,8 @@ did before. Production additionally needs:
|
|||
| Variable | Meaning |
|
||||
| --- | --- |
|
||||
| `SECRETS_ENGINE_APPROVAL_URL` | approval-engine base URL (claim + consume) |
|
||||
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | mode-0600 credential, outside Git |
|
||||
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | explicit bearer credential outside Git; mutually exclusive with the client-secret provider |
|
||||
| `SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE` | protected temporary KeyCape approval-client secret; fresh in-memory token per request ([contract](approval-service-auth.md)) |
|
||||
| `SECRETS_ENGINE_AUTHORIZATION_SUBJECT_ID` / `_SUBJECT_TYPE` | the acting principal |
|
||||
| `SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE` / `_VERSION` | the live pin (step 2) |
|
||||
| `SECRETS_ENGINE_PDP_URL` / `_PDP_TOKEN_FILE` | the per-consumer access-engine pin |
|
||||
|
|
|
|||
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.
|
||||
|
|
@ -39,6 +39,8 @@ secrets-engine --version
|
|||
| `SECRETS_ENGINE_CATALOG` | `./catalog` | catalog directory |
|
||||
| `SECRETS_ENGINE_EVIDENCE` | `./.evidence` | local non-secret evidence log |
|
||||
| `SECRETS_ENGINE_UNSAFE_DEMO` | _(unset)_ | allow a prod-labeled lane only when Hub is disabled and OpenBao is loopback; throwaway demos only |
|
||||
| `SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE` | _(unset)_ | separate temporary approval-client secret; [per-request exchange](approval-service-auth.md) |
|
||||
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | _(unset)_ | explicit approval bearer file; conflicts with approval client-secret provider |
|
||||
| `SECRETS_ENGINE_KEYCAPE_TOKEN_URL` | _(unset)_ | KeyCape token endpoint for `service-jwt` |
|
||||
| `SECRETS_ENGINE_KEYCAPE_ISSUER` | _(unset)_ | KeyCape issuer; must match the JWT login contract |
|
||||
| `SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE` | _(unset)_ | mode-0600 out-of-repo client secret |
|
||||
|
|
|
|||
34
docs/evidence/2026-09-09-approval-identity-exercise.json
Normal file
34
docs/evidence/2026-09-09-approval-identity-exercise.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"schema_version": 1,
|
||||
"target": "disposable local processes; synthetic credentials",
|
||||
"started_at": "2026-09-09T01:49:09.187005+00:00",
|
||||
"keycape_image": "forgejo.coulomb.social/coulomb/key-cape@sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611",
|
||||
"approval_engine_commit": "b46b0f26669dc83c944ee5145426bad03d5ef720",
|
||||
"keycape_contract_commit": "0f5535eed95f1223c83a28f5a0bd6fa594cecae8",
|
||||
"consumer_source_sha256": {
|
||||
"approval_auth.py": "6f3b033e7928e1c527bc19f5a1e01bf243540769a8646bd8cd47268da25637b5",
|
||||
"approval_consume.py": "cd79b3e4534a5abf55af2b5ab2939c485a3140a1174f593e0aa7719250051d92",
|
||||
"config.py": "2f2f1b60664d89bbb806aed0c768378923062077740e1f96e4c359238eb06955",
|
||||
"service_auth.py": "653723ef5babce2157771416d93cb15cdbb7caca3c45509c3730520c55e4515c"
|
||||
},
|
||||
"limitations": [
|
||||
"PDP sequencing double",
|
||||
"local Approval Engine source, not deployed image",
|
||||
"no live custody or client-side read grant",
|
||||
"no OpenBao effect or model execution"
|
||||
],
|
||||
"checks": {
|
||||
"operator_issued_and_approved_via_verified_jwt": true,
|
||||
"wrong_action_refused_before_consume": true,
|
||||
"actual_consumer_claim_check_consume": true,
|
||||
"same_digest_retry_idempotent": true,
|
||||
"different_digest_refused": true,
|
||||
"spent_claim_refused": true,
|
||||
"operator_consume_scope_denied_by_issuer": true,
|
||||
"wrong_secret_refused": true,
|
||||
"no_access_token_file_created": true
|
||||
},
|
||||
"status": "passed",
|
||||
"cleanup_complete": true,
|
||||
"finished_at": "2026-09-09T01:49:11.402008+00:00"
|
||||
}
|
||||
|
|
@ -31,3 +31,11 @@ those named providers and `--auth service-jwt` fail-closes. See
|
|||
|
||||
Canonical provider contract:
|
||||
`key-cape/docs/openbao-service-auth-contract.md` (reviewed 2026-08-23).
|
||||
|
||||
## Approval identity is separate
|
||||
|
||||
[Approval service authentication](approval-service-auth.md) uses its own
|
||||
client-secret input, resource audience, exact tenant:platform and per-request
|
||||
read/consume scopes. Never put that credential into the OpenBao provider.
|
||||
Both providers now validate KeyCape’s actual `level`/`methods` assurance shape;
|
||||
the old `aal`/`method` fixture was incompatible with the issuer.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue