Harden the PEP harness and KeyCape registration request

Close remaining in-repo APPROVAL-WP-0002 gaps: drive GH-DEC-2026-003 against
the real HTTP surface, fail closed on JWT/human-consume/static-token paths,
treat audit 200 duplicates as drained, and ask KeyCape for the production
audience and client grants.

Assistant: grok
Assistant-Session: 01a06253-e557-7971-93d9-4f4c2cfbf455
This commit is contained in:
tegwick 2026-09-02 15:46:06 +02:00
parent 2bd2d19a98
commit 2370f69927
11 changed files with 588 additions and 46 deletions

View file

@ -32,3 +32,8 @@ only verifies and enforces them. Requested registrations are:
Client credentials belong in OpenBao/operator custody and must not be placed in
manifests, logs, State Hub, or this repository.
KeyCape's OpenBao service-auth contract currently emits `aud` as the OAuth
`clientId`. That pattern must not be reused here. Tokens presented to this API
MUST have resource-server audience `approval-engine`. Requested non-secret
client fragments are in `docs/keycape-service-registrations.md`.

View file

@ -0,0 +1,67 @@
# Requested KeyCape registrations
Status: requested by `APPROVAL-WP-0002-T01`. Non-secret. KeyCape owns issuance,
client disablement, and the exact claim contract. This file is a consumer
request, not a live registration.
Tokens presented to approval-engine MUST use resource-server audience
`approval-engine`. Do not reuse the OpenBao service-auth pattern that sets
`aud` to the OAuth `clientId`.
Required claims remain those in `docs/caller-authentication.md`: `iss`, `sub`,
`aud`, `exp`, `iat`, `principal_type`, `tenant`, `roles`, `scope`, `assurance`.
`principal_type` for consume callers must be `service` or `agent`.
## Resource server
| Field | Value |
| --- | --- |
| Audience | `approval-engine` |
| Issuer | the deployed KeyCape issuer (manifest uses `https://auth.netkingdom.local`) |
| JWKS | `GET /jwks` on the KeyCape service |
| Scopes | `approval:create`, `approval:read`, `approval:approve`, `approval:revoke`, `approval:supersede`, `approval:consume`, `approval:observe`, `approval:emit` |
## Clients
Confidential client secrets stay in OpenBao/operator custody. `secretRef`
names below are placeholders for that custody path.
```yaml
clients:
- clientId: secrets-engine-approval
displayName: secrets-engine PEP consume client
audience: approval-engine
allowedScopes: [approval:read, approval:consume]
grantTypes: [client_credentials]
clientType: confidential
secretRef: env:KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET
serviceSubject: service:secrets-engine
principal_type: service
tenant: tenant:coulomb
roles: [secrets-engine]
tokenLifetime: 15m
- clientId: approval-engine-operator
displayName: approval-engine lifecycle operator
audience: approval-engine
allowedScopes:
- approval:create
- approval:read
- approval:approve
- approval:revoke
- approval:supersede
- approval:observe
- approval:emit
grantTypes: [client_credentials]
clientType: confidential
secretRef: env:KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET
serviceSubject: service:approval-engine-operator
principal_type: service
tenant: tenant:coulomb
roles: [approval-operator]
tokenLifetime: 15m
```
Human approvers use the existing KeyCape human flow with `approval:approve`
only, still with `aud=approval-engine`. They must not receive
`approval:consume`.

View file

@ -14,7 +14,15 @@ and consume conflicts all prevent the callback. A same-digest retry receives
the engine's idempotent success. If the callback fails after consume, the
approval stays spent; there is no unconsume.
The module rereads the mounted bearer-token file on each HTTP request. Its unit
harness uses a dry-run callback and demonstrates the ordering, but live closure
requires the secrets-engine-owned handler to prove that no OpenBao request is
made in every failure case.
The module rereads the mounted bearer-token file on each HTTP request. Conflict,
unavailability, unauthorized, missing, DENY, and digest-mismatch paths all
prevent the callback. Same-digest consume retries are idempotent; a later full
sequence against a spent approval fails at the claim. The consume response is
rejected if it carries decision-shaped keys (`effect` / `allow` / `deny`).
The live HTTP harness in `tests/test_pep.py` drives this sequence against the
real WSGI surface without performing a protected action. secrets-engine owns
the production OpenBao PEP (`src/secrets_engine/approval_consume.py`); that
handler is implemented and proven in-repo for 409 / unreachable / missing
binding. Live closure still needs this service deployed and a durable consume
binding served (`SECRETS-WP-0007-T04` / `SECRETS-WP-0008-T02`).