approval-engine/docs/caller-authentication.md
tegwick c9ad5c2dd7 Retain verified tenant provenance and reconcile production blockers
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1046e-fdbf-7b93-a267-5f72b9b24526
2026-10-04 11:26:32 +02:00

97 lines
5.6 KiB
Markdown

# 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-house` issues one, it needs a
directory-sourced claim and this check does not become that claim by matching.
- Schema v6 records verified `tenant_source` on approver entries and issuance/use
threshold evidence. KeyCape publishes `directory`, `registration`, or `default`.
An absent claim remains null; an explicit malformed or unknown value is refused.
Legacy entries remain null, never inferred from their subject or tenant.
Provenance does not change store isolation or establish zone membership.
The deployed schema-v5 image does not yet record this field.
| 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, **principal type**, and **tenant source** derive
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-engine` with the scopes above;
- the secrets-engine PEP service client with `approval:read` and
`approval: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`.