key-cape reports the withdrawal came too late: approval-engine-operator was provisioned in the 2026-09-09 custody window and is wired into a pod. A live credential that can create and approve approvals, presented by nobody, is a worse resting state than either extreme. As requesting owner: disable, do not delete. CCR-2026-0018 stands so it is restorable under the narrowing constraint once a presenter exists. A recorded expiry was rejected — it promises to act later on a capability that is dangerous now, guarding exactly the case where nobody is watching. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyybaE7DUXrWYrhbnESCTe Assistant: claude-code Assistant-Model: opus Assistant-Process: 1275879@bnt-lap001 Assistant-Session: eb464208-f821-41b2-bc5a-a6c33d92a8ad
232 lines
12 KiB
Markdown
232 lines
12 KiB
Markdown
# 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://kc.coulomb.social`) |
|
|
| JWKS | `GET /jwks` on the KeyCape service |
|
|
| Scopes | `approval:create`, `approval:read`, `approval:approve`, `approval:revoke`, `approval:supersede`, `approval:consume`, `approval:observe`, `approval:emit` |
|
|
|
|
## Tenant — resolved: exact `tenant:platform`
|
|
|
|
The operator accepted `tenant:platform` as the platform management,
|
|
administration and services tenant (the landlord zone). Decision
|
|
`5ed3fb35-eca9-413a-82b9-95171ba85bf6`, recorded in
|
|
`glas-harness/docs/platform-tenant-decision.md` and relayed by `glas-harness`
|
|
2026-09-06.
|
|
|
|
**The spelling is the contract.** `ApiApplication.identity` compares the
|
|
verified JWT `tenant` claim to the engine's configured store tenant with exact
|
|
string equality and raises `Forbidden` before any object lookup
|
|
(`approval_engine/api.py:66`). There is no mapping table, no normalisation, and
|
|
no prefix handling anywhere in this engine — deliberately. `platform` is **not**
|
|
an accepted alias for `tenant:platform`, and neither is `tenant:coulomb`.
|
|
|
|
Rendered values, all three now exactly `tenant:platform`:
|
|
|
|
| Value | Where it is set | Content |
|
|
| --- | --- | --- |
|
|
| Store tenant | `deploy/approval-engine.yaml` `--tenant` | `tenant:platform` |
|
|
| Store tenant default | `approval_engine/cli.py` `--tenant`, `Engine(tenant=…)` | `tenant:platform` |
|
|
| JWT `tenant` claim | the client registrations below | `tenant:platform` |
|
|
| CheckRequest tenant | flex-auth policy subject | `tenant:platform` |
|
|
|
|
The CLI and `Engine` defaults were moved off `platform` in the same change. A
|
|
default that differs from the sanctioned value is a trap: a `serve` invocation
|
|
that omits `--tenant` would have come up healthy and then refused every
|
|
authenticated call, which is the failure this decision exists to prevent.
|
|
|
|
The flex-auth CheckRequest tenant now matches by spelling, but note it still
|
|
does not participate in this comparison — it is a PDP policy subject this engine
|
|
never reads. Alignment there is a property of the decision, not a mechanism
|
|
here.
|
|
|
|
Denial evidence:
|
|
|
|
- `tests/test_auth.py::test_wrong_tenant_is_forbidden` — a signature-valid token
|
|
whose tenant differs from the store tenant is refused `403` without mutation.
|
|
- `tests/test_auth.py::test_near_miss_tenant_spellings_are_forbidden` — pins the
|
|
decision's "no alias" clause directly: `platform`, `tenant:coulomb`,
|
|
`TENANT:PLATFORM`, `tenant:platform ` (trailing space) and `` (empty) are each
|
|
refused against a `tenant:platform` store, while the exact spelling is
|
|
admitted. Without a test that *varies* the tenant, a suite of fixtures all
|
|
carrying the sanctioned value proves nothing about the field.
|
|
|
|
This resolves the choice of value only. Verification and the remaining admission
|
|
gates — KeyCape actually owning these registrations, credential materialization,
|
|
and the `T03` rollout evidence — are unchanged and still open.
|
|
|
|
Tracked against `APPROVAL-WP-0002-T01`; related `KEY-WP-0013-T02`,
|
|
`SECRETS-WP-0009-T03`, `GLAS-WP-0015`.
|
|
|
|
## 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:platform
|
|
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:platform
|
|
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`. As of 2026-09-09 that flow is `informed-decision`'s
|
|
(`INFD-WP-0001`); see `docs/approver-surface-requirements.md`.
|
|
|
|
## `approval-engine-operator` — requested, unpresented, do not provision
|
|
|
|
Answering `railiance-platform` (`RPF-WP-0035-T06`, `CCR-2026-0020`) 2026-09-09.
|
|
Their question was the right one and we could not answer it from this source
|
|
either, because the answer is *nobody*.
|
|
|
|
**Nothing in this repository presents this client, and nothing is deployed that
|
|
could.** This engine only *verifies* bearer tokens; it never obtains one. The
|
|
sole outbound credential it holds is the mounted `audit-token` used for
|
|
audit-core delivery (`approval_engine/audit.py`), which is not an OAuth client.
|
|
`approval_engine/pep.py` is a consumer-side helper for PEPs such as
|
|
`secrets-engine`, which present their own tokens. There is no client-credentials
|
|
grant anywhere in the package, and `deploy/approval-engine.yaml` is unapplied.
|
|
|
|
Scope by scope, the bundle we originally requested does not describe one actor:
|
|
|
|
| Scope | Who would present it |
|
|
| --- | --- |
|
|
| `approval:create` | whoever *requests* an approval. Not named anywhere in the estate today; `gate-house` doctrine says which acts need one, but no requester identity has been settled |
|
|
| `approval:approve` | `informed-decision`'s **human** client, per its own registration. A service principal holding this is exactly the question `gate-house` has not ruled on — see the `principal_type` note below |
|
|
| `approval:read` | anything rendering or inspecting an approval |
|
|
| `approval:revoke`, `approval:supersede` | attended lifecycle operations |
|
|
| `approval:observe` | monitoring and operations |
|
|
| `approval:emit` | **redundant in normal operation** — the running server emits its own heartbeat on `--heartbeat-seconds` with no token; the route exists only to force one |
|
|
|
|
So this was a convenience bundle written when this repo assumed a single
|
|
"lifecycle operator" would exist, and it aged badly: it now carries the widest
|
|
scope set in the pair, including `approval:approve`, for an identity nobody has
|
|
named.
|
|
|
|
**Decision, option (b): cancel `CCR-2026-0020`.** No client-side reader is
|
|
wanted, because there is no presenter. This is an owner decision and not a
|
|
deferral — recorded here so the request does not rot `in_flight`. Verifier
|
|
custody (`CCR-2026-0018`) is unaffected and remains correct: KeyCape must be
|
|
able to *authenticate* this client if it is ever presented.
|
|
|
|
When a presenter is actually named, the registration should be re-requested
|
|
**narrowed to that presenter's scopes**, not reinstated as this bundle. In
|
|
particular `approval:approve` should not travel with the operational scopes:
|
|
schema v4 records `entries.principal_type` so a service bind is legible, but
|
|
legibility is not permission, and whether a non-human may supply approver
|
|
evidence at all is `gate-house`'s to rule on.
|
|
|
|
`secrets-engine-approval` above is unaffected — it has a real presenter, a
|
|
narrow scope pair, and is the client that unblocks `T05`.
|
|
|
|
### It was already live — disable the registration, keep the custody
|
|
|
|
2026-09-10: key-cape reports the withdrawal came too late. The client was
|
|
provisioned in the 2026-09-09 attended custody window and verified there
|
|
(`docs/evidence/2026-09-09-keycape-verifier-admission.json` records
|
|
`exact_claims_verified` and a working exchange for `approval-engine-operator`),
|
|
its secret is in the namespace and wired into a pod.
|
|
|
|
So the estate holds a live confidential credential that can **create and
|
|
approve** approvals, that nobody presents, and that no consumer is waiting on.
|
|
Standing capability with no counterparty is a worse resting state than either
|
|
extreme: it serves no one, and it is the shape found later by someone other
|
|
than its owner.
|
|
|
|
**Decision, as requesting owner: disable the registration, keep the custody.**
|
|
Not delete. `CCR-2026-0018` verifier custody stands, so the client remains
|
|
authenticable and restorable the moment a presenter is named; the re-request
|
|
constraint above still applies — narrowed to that presenter, and
|
|
`approval:approve` must not travel with the operational scopes. key-cape
|
|
executes the disablement with railiance-platform as the deployment change.
|
|
|
|
The middle option — leaving it standing with a recorded expiry — was rejected
|
|
deliberately. An expiry is a promise to act later on a capability that is
|
|
dangerous now, and the thing it protects against is precisely the case where
|
|
nobody is watching.
|
|
|
|
## Human approver client — not supplied (2026-09-08)
|
|
|
|
`KEY-WP-0013-T02` asked for two exact strings for the human approver
|
|
registration (public client, authorization code + S256 PKCE, exact redirect,
|
|
`aud=approval-engine`, `allowedScopes: [openid, approval:approve]`,
|
|
`mfaRequired: true`): the `client_id` and the full callback URI.
|
|
|
|
**This repository cannot supply either, and must not guess one.** Redirects are
|
|
matched exactly at `/authorize`, so a fabricated value fails closed — or, worse,
|
|
registers a redirect no deployed component owns.
|
|
|
|
Why there is nothing to give:
|
|
|
|
- approval-engine is a bearer-token **resource server** only. `approval_engine/api.py`
|
|
is a WSGI app whose every non-health route reads `Authorization: Bearer …`
|
|
(`approval_engine/auth.py`). There is no `/callback`, no redirect handling, no
|
|
authorization-code or PKCE code anywhere in the package.
|
|
- Nothing here is a browser client. The requested registrations above are both
|
|
`client_credentials` confidential service clients.
|
|
- The service is not publicly addressable: `deploy/approval-engine.yaml` has a
|
|
Namespace, ClusterIP Service and StatefulSet — no Ingress, no external
|
|
hostname — so no `https://…/callback` origin exists to register.
|
|
|
|
The human approver flow therefore belongs to whichever **browser-facing
|
|
approver UI** presents `approval:approve` tokens to this engine. That component
|
|
is not in this repo and is not named in `APPROVAL-WP-0002`. The two strings must
|
|
come from its owner once it exists and has a deployed origin; approval-engine
|
|
only needs the resulting access token to carry `aud=approval-engine`,
|
|
`principal_type: human`, `tenant: tenant:platform` and scope `approval:approve`.
|
|
|
|
### Token confirmed: access token, never the ID token
|
|
|
|
`ApiApplication.identity` verifies the **access token** presented as
|
|
`Authorization: Bearer` at this resource — RS256 against the deployed issuer's
|
|
`/jwks`, checking `iss`, `aud=approval-engine`, `exp`/`iat` (30s leeway), `sub`,
|
|
`principal_type`, `tenant` (exact equality with the store tenant), `roles`,
|
|
`scope` and `assurance`. `id_token` appears nowhere in the codebase; the ID
|
|
token belongs to the login client and is never evidence here. Human principals
|
|
are additionally refused `approval:consume`.
|