key-cape/docs/approval-engine-provisioning-request.yaml

101 lines
5.5 KiB
YAML
Raw Normal View History

# Proposed non-secret admission packet. This is not executable authorization.
# Tenant for both requests is tenant:platform per decision
# 5ed3fb35-eca9-413a-82b9-95171ba85bf6 (landlord zone). Exact spelling required
# across the approval store, these JWT claims and the lifecycle CheckRequest;
# no alias to platform or tenant:coulomb.
status: awaiting-custody-admission
owner: key-cape
resource_audience: approval-engine
issuer: https://kc.coulomb.social
registration_source: config/service-clients.example.yaml
requests:
- client_id: secrets-engine-approval
subject: service:secrets-engine
tenant: tenant:platform
scopes: [approval:read, approval:consume]
lifetime: 15m
proposed_openbao_path: platform/workloads/secrets-engine/approval-client
field: CLIENT_SECRET
proposed_kubernetes_secret: sso/keycape-secrets-engine-approval-client
kubernetes_key: client-secret
keycape_environment: KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET
custody_owner: railiance-platform
consumer: secrets-engine
- client_id: approval-engine-operator
subject: service:approval-engine-operator
tenant: tenant:platform
scopes: [approval:create, approval:read, approval:approve, approval:revoke, approval:supersede, approval:observe, approval:emit]
lifetime: 15m
proposed_openbao_path: platform/workloads/approval-engine/operator-client
field: CLIENT_SECRET
proposed_kubernetes_secret: sso/keycape-approval-engine-operator-client
kubernetes_key: client-secret
keycape_environment: KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET
custody_owner: railiance-platform
consumer: approval-engine-operator
human_registration:
status: awaiting-exact-callback
blocks_service_client_rollout: false
Answer the approver-client questions, and fix what checking them turned up informed-decision and approval-engine both asked to hear problems with the human approver registration now rather than at handover. Checking their requested shape against the source rather than agreeing it on paper turned up three things. Scope gap, accepted: [openid, approval:approve] cannot render a decision, since GET /v1/approvals/{id} and /claim both need approval:read -- the surface could submit an entry it was never able to display. Published [openid, approval:read, approval:approve]. Reading through a service identity was the alternative and is worse: it weakens the evidence-of-what-this-person-saw claim the component exists to make. approval:consume stays excluded. Assurance shape, published and a defect fixed. Both asked for a documented shape and KeyCape already emitted one, so it is written down rather than renegotiated. Writing it down surfaced that `at` was the token mint time rather than the authentication time. Those differ by hours whenever a browser session is reused, and approval-engine persists this object verbatim as the only downstream record that MFA happened -- so a stored approval could have evidenced MFA at a moment the person proved nothing. PKCESession.AuthTime now carries the original login instant through session reuse, with mint time as the fallback. Blocker found before anyone built on it: a human token cannot carry tenant:platform. effectiveTenant resolves the human tenant from the directory user, no adapter populates User.Tenant, and the per-client tenant field is read only on the client_credentials path -- so every human token defaults to tenant:coulomb, which approval-engine refuses by exact string equality. It would have presented as a failed approval rather than a registration defect. Two resolutions sent to the owners and neither implemented here: the choice decides whether a human's tenant is a property of the person or of the registration, and that is not KeyCape's alone to make. Also recorded ops-warden's answers to KEY-WP-0014-T04, including their finding that `warden plan` returns `autonomous` for a need containing generate and CAS-write, because it has no read-versus-mutate intent. Their standing instruction -- treat a warden plan verdict on any write, rotate or provision need as unreliable until WARDEN-WP-0038 lands -- is recorded in the workplan rather than left in an inbox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-09 14:25:38 +02:00
# Owner identified 2026-09-09: informed-decision (hub repo cf4c7da8). It
# supplies client_id and callback URI from INFD-WP-0001-T07 once it has a
# deployed origin. approval-engine is a bearer-only resource server and never
# owned those strings.
owner: informed-decision
# approval:read added 2026-09-09 on approval-engine's finding: the surface
# cannot render a decision without GET /v1/approvals/{id} and /claim, so the
# earlier scope set let an approver submit what they could not display.
scopes: [openid, approval:read, approval:approve]
mfa_required: true
client_type: public
Answer the approver-client questions, and fix what checking them turned up informed-decision and approval-engine both asked to hear problems with the human approver registration now rather than at handover. Checking their requested shape against the source rather than agreeing it on paper turned up three things. Scope gap, accepted: [openid, approval:approve] cannot render a decision, since GET /v1/approvals/{id} and /claim both need approval:read -- the surface could submit an entry it was never able to display. Published [openid, approval:read, approval:approve]. Reading through a service identity was the alternative and is worse: it weakens the evidence-of-what-this-person-saw claim the component exists to make. approval:consume stays excluded. Assurance shape, published and a defect fixed. Both asked for a documented shape and KeyCape already emitted one, so it is written down rather than renegotiated. Writing it down surfaced that `at` was the token mint time rather than the authentication time. Those differ by hours whenever a browser session is reused, and approval-engine persists this object verbatim as the only downstream record that MFA happened -- so a stored approval could have evidenced MFA at a moment the person proved nothing. PKCESession.AuthTime now carries the original login instant through session reuse, with mint time as the fallback. Blocker found before anyone built on it: a human token cannot carry tenant:platform. effectiveTenant resolves the human tenant from the directory user, no adapter populates User.Tenant, and the per-client tenant field is read only on the client_credentials path -- so every human token defaults to tenant:coulomb, which approval-engine refuses by exact string equality. It would have presented as a failed approval rather than a registration defect. Two resolutions sent to the owners and neither implemented here: the choice decides whether a human's tenant is a property of the person or of the registration, and that is not KeyCape's alone to make. Also recorded ops-warden's answers to KEY-WP-0014-T04, including their finding that `warden plan` returns `autonomous` for a need containing generate and CAS-write, because it has no read-versus-mutate intent. Their standing instruction -- treat a warden plan verdict on any write, rotate or provision need as unreliable until WARDEN-WP-0038 lands -- is recorded in the workplan rather than left in an inbox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-09 14:25:38 +02:00
grant: authorization_code + S256 PKCE
never: [approval:consume]
Let a human token carry the zone it is issued into, without relabelling anyone KEY-WP-0013-T05's tenant blocker did not need the decision it was waiting on. The two proposed resolutions differ in where a human's tenant comes from -- the directory record, or the client registration -- and an implementation exists that is correct under either, so the choice can be made later without another migration. A client registration may now declare a tenant. humanTenant() resolves it by four rules: no declaration keeps the directory answer unchanged; a declared zone applies where the directory has placed the user nowhere; agreement passes; and a declared zone conflicting with a directory assignment refuses issuance rather than relabelling the user. The refusal is the design, not an edge case. A registration can bind a zone for unplaced users and can never move a placed one, so this gets the approval chain its tenant:platform without writing a general cross-tenant override into the issuer. It fails closed rather than picking a winner, because either answer would be a silent cross-tenant assertion, and it reports 403 with error_type: tenant_binding so an operator can tell a misconfigured registration from a rejected login. If the owners later populate directory tenants, the same code stops supplying the zone and starts enforcing agreement with it. Safe only because client registrations are static and deployment-owned. The tenant contract records that this rule must be revisited if dynamic client registration is ever admitted. Tests cover all four rules; neutering the conflict check fails the relabel test rather than passing silently. T05 now waits on one thing only: the client_id and callback URI from informed-decision once it has a deployed origin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-09 14:40:36 +02:00
# Required. Without it the token carries tenant:coulomb from the directory
# default and approval-engine refuses it. See docs/tenant-claim-contract.md.
tenant: tenant:platform
keycape_side_resolved: |
2026-09-09: a human token can now carry tenant:platform. A client
registration may declare a tenant; humanTenant() supplies it when the
directory has not placed the user, requires agreement when it has, and
refuses issuance on conflict rather than relabelling. Safe only because
registrations are static and deployment-owned. Still outstanding from
elsewhere: the exact client_id and callback URI from informed-decision
(INFD-WP-0001-T07), and whether the owners prefer the directory-sourced
resolution instead, which this implementation degrades into cleanly.
verification:
Ship the live-registration check both blocked tasks depend on KEY-WP-0013-T02 and KEY-WP-0014-T04 stay blocked on custody and on ops-warden, but each contains a KeyCape-owned piece that had been left as prose. T02 requires proving "live JWKS verification and denied excess scopes without logging values"; T04 step 4 requires verifying a rotated secret, refusing its predecessor and refusing excess scope. Both were describable and neither was runnable, so the proof would have been improvised by hand against production at the moment custody lands -- the worst possible time for it. keycape verify-client does it in one command. Per registration it checks discovery origin, the client_credentials exchange and its RS256 signature against the deployed JWKS, exact sub/tenant/roles/principal_type, that every -deny-scope is refused, and that the token carries no scope that was not requested. That last check is a real gap: the caller commands prove every requested scope was granted, never that nothing extra came back. -previous-secret-env additionally requires the predecessor to be refused, and treats an unchanged secret as a rotation that did not happen. Nothing is written to disk and no value is printed. Failures name the claim, not the observed value, so running this against production cannot turn a verification into a disclosure. Every check runs before it reports, so one failure does not hide the rest. The exact invocation for each approval client is recorded in the verification block of the provisioning packet, so custody admission hands back a command rather than a description. Tests cover the passing case, an over-broad registration, a live predecessor, an identical rotation and output non-disclosure; neutering mustFail makes the suite fail, so the checks have teeth. Also recorded in KEY-WP-0014: read from ops-warden's catalog rather than waiting for a reply, key-cape-oidc-login is asked-and-waiting on us since 2026-08-28 and is a pointer lane with no programmatic consumers, and the rapp-qonto-keycape-client blocker citing an absent native exchange command went stale when service-token shipped on 2026-09-05. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-08 14:30:43 +02:00
# The first two lines are now one runnable command per client; see
# docs/native-authentication.md, "Verifying a live registration". It writes
# nothing and prints no value, so it is safe to run against production.
- command: |
keycape verify-client -issuer https://kc.coulomb.social
-client-id secrets-engine-approval -audience approval-engine
-scope "approval:read approval:consume"
-secret-env KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET
-expect-subject service:secrets-engine -expect-tenant tenant:platform
-expect-roles secrets-engine
-deny-scope "approval:approve approval:revoke approval:supersede"
- command: |
keycape verify-client -issuer https://kc.coulomb.social
-client-id approval-engine-operator -audience approval-engine
-scope "approval:create approval:read approval:approve approval:revoke approval:supersede approval:observe approval:emit"
-secret-env KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET
-expect-subject service:approval-engine-operator -expect-tenant tenant:platform
-expect-roles approval-operator
-deny-scope "approval:consume"
- Check KeyCape and consumer readiness without emitting secrets or tokens.
- Preserve existing registrations and signing key; record versions and image digest.
Ship the live-registration check both blocked tasks depend on KEY-WP-0013-T02 and KEY-WP-0014-T04 stay blocked on custody and on ops-warden, but each contains a KeyCape-owned piece that had been left as prose. T02 requires proving "live JWKS verification and denied excess scopes without logging values"; T04 step 4 requires verifying a rotated secret, refusing its predecessor and refusing excess scope. Both were describable and neither was runnable, so the proof would have been improvised by hand against production at the moment custody lands -- the worst possible time for it. keycape verify-client does it in one command. Per registration it checks discovery origin, the client_credentials exchange and its RS256 signature against the deployed JWKS, exact sub/tenant/roles/principal_type, that every -deny-scope is refused, and that the token carries no scope that was not requested. That last check is a real gap: the caller commands prove every requested scope was granted, never that nothing extra came back. -previous-secret-env additionally requires the predecessor to be refused, and treats an unchanged secret as a rotation that did not happen. Nothing is written to disk and no value is printed. Failures name the claim, not the observed value, so running this against production cannot turn a verification into a disclosure. Every check runs before it reports, so one failure does not hide the rest. The exact invocation for each approval client is recorded in the verification block of the provisioning packet, so custody admission hands back a command rather than a description. Tests cover the passing case, an over-broad registration, a live predecessor, an identical rotation and output non-disclosure; neutering mustFail makes the suite fail, so the checks have teeth. Also recorded in KEY-WP-0014: read from ops-warden's catalog rather than waiting for a reply, key-cape-oidc-login is asked-and-waiting on us since 2026-08-28 and is a pointer lane with no programmatic consumers, and the rapp-qonto-keycape-client blocker citing an absent native exchange command went stale when service-token shipped on 2026-09-05. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-08 14:30:43 +02:00
- Human consume denial is approval-engine's to verify at its resource; KeyCape
proves only that the human client is never issued a consume grant.
blockers:
- Admit exact custody paths, field delivery, consumer identities and lifecycle authority.
- Resolve attended first-provision authority through the custody owner.
- Verify the actual upstream ID-token issuer before production image rollout.
custody_return:
owner_record: railiance-platform/workplans/RPF-WP-0035-credential-lane-implementation.md#Admit-KeyCape-approval-engine-client-custody-and-delivery
requests: [CCR-2026-0017, CCR-2026-0018]
status: proposed-awaiting-owner-approval
field_correction: CLIENT_SECRET
scope: KeyCape verifier-side copies only; client-side retrieval is not admitted.
human_registration_gate:
- The approver UI owner must supply its real client ID and exact callback.
- This gate blocks human approval entry; it does not block the two independent client_credentials registrations or service startup.