Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
5.4 KiB
Caller authentication
Production accepts only RS256 JWTs verified against KeyCape JWKS with the exact
configured issuer and approval-engine audience. exp, iat, sub,
principal_type, tenant, roles, scope, and assurance are mandatory.
Missing or unverifiable credentials fail closed. The development static-token
mode is explicit, file-backed, and refused with --production.
The verified tenant must exactly match the service's configured store tenant;
cross-tenant reads and mutations are rejected before object lookup.
This check is store isolation, and it is not a statement about the person.
It answers "does this caller belong to the store this engine serves", not "is
this human a member of the platform zone". GH-DEC-2026-013 rules that a
key-cape human tenant claim may be supplied by the client registration rather
than asserted by the directory, and forbids a consumer treating the two as
equivalent for a decision turning on a fact about the person. Exact string
equality cannot see that difference — a string that matches exactly matches
whoever asserted it — so the difference is stated here instead.
Consequences this engine accepts deliberately:
- A registration-supplied tenant is admissible for admission to this store. Admission means "arrived through a channel the platform registered", the approver client is static and deployment-owned, and dynamic client registration is excluded from key-cape by design.
- A registration-supplied tenant is not admissible for any doctrine that
turns on the approver's own membership — "an approver must be a member of the
platform zone" is a fact about the person, and this claim cannot carry it.
No such doctrine exists today; if
gate-houseissues one, it needs a directory-sourced claim and this check does not become that claim by matching. - When key-cape emits provenance alongside the tenant, this engine records it on
the approver entry the way schema v4 records
principal_type— an evidence reader should not have to infer provenance from a string that cannot carry it.
| Route | Required scope |
|---|---|
| create approval | approval:create |
| get approval or claim | approval:read |
| add approval entry | approval:approve |
| revoke | approval:revoke |
| supersede | approval:supersede |
| consume | approval:consume and service/agent principal |
| cadence, outbox, storage | approval:observe |
| explicit heartbeat | approval:emit |
Create additionally requires binding.actor == sub. Approval-entry subject,
assurance, evidence reference, and principal type are derived from the
verified JWT, never the request body.
principal_type is recorded from verified identity (since schema v4). Schema v5
implements GH-DEC-2026-016: when an approval declares human_control: true,
/entries refuses a service, agent or unknown principal with 403 before inserting
an entry or emitting issuance. The request body cannot supply the approver type
or downgrade the declaration. Undeclared approvals retain service-to-service use;
a human entry does not retroactively declare a human control.
Requesting and binding are separate operations. POST /v1/approvals creates
an unapproved request and records the strict boolean declaration. A service or
agent with approval:create may draft that request. The principal contributing
judgment is known only at POST /entries; this is the bind/issue boundary where
GH-DEC-2026-016's non-human refusal applies. A draft is never a valid approval.
No statement about the approver is inferred from binding.principal or the
requesting actor's type. Dual control also needs its declared required_count;
human control alone does not imply two approvers or decide which acts need it.
Identity provenance: reviewed KeyCape source
f9812ab3b2bfe8f0817185f44071e612264ec3ee:src/internal/server/oidc/token.go
sets principal_type=human only after consuming a client/redirect-bound PKCE
session and looking up the current user. Its separate client-credentials path
sets service; no registration field supplies a human principal type. This is
different from registration-supplied tenant routing. Production acceptance must
pin and prove that issuer behavior with a real human flow; signed fixtures here
prove engine enforcement, not native identity admission. If an issuer introduces
a registration-supplied route to human, GH-DEC-2026-016 §5 requires independent
provenance and refusal of that route before admitting it. A consumer must not
relax the identity contract simply because the JWT verifies.
Entries written before v4 stay null; pre-v5 objects have human_control: false.
KeyCape owns client registration and scope grants; approval-engine verifies and
enforces them. Requested registrations are:
- audience/resource server
approval-enginewith the scopes above; - the secrets-engine PEP service client with
approval:readandapproval:consume; - separately governed lifecycle/operator clients with only their needed mutation or observation scopes.
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.