2026-09-05 01:08:58 +02:00
|
|
|
# Proposed non-secret admission packet. This is not executable authorization.
|
Align approval registrations to the tenant:platform decision
Operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 accepts tenant:platform
as the platform management tenant for the Glas approval chain, requiring exact
spelling across the approval store, the service-client JWT claim and the
lifecycle CheckRequest.
Changes the tenant field on secrets-engine-approval and approval-engine-operator
only, in the registration fixture and the provisioning packet. Unrelated clients
and the human directory default keep tenant:coulomb, and no audience, scope,
subject, role, lifetime or MFA grant changes.
Adds issuance evidence that the approval shape emits tenant:platform exactly and
never an alias the caller requests, that the OpenBao client gains no
cross-tenant reach, and a fixture guard pinning every reviewed client's tenant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-06 22:30:32 +02:00
|
|
|
# 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.
|
2026-09-05 01:08:58 +02:00
|
|
|
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
|
Align approval registrations to the tenant:platform decision
Operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 accepts tenant:platform
as the platform management tenant for the Glas approval chain, requiring exact
spelling across the approval store, the service-client JWT claim and the
lifecycle CheckRequest.
Changes the tenant field on secrets-engine-approval and approval-engine-operator
only, in the registration fixture and the provisioning packet. Unrelated clients
and the human directory default keep tenant:coulomb, and no audience, scope,
subject, role, lifetime or MFA grant changes.
Adds issuance evidence that the approval shape emits tenant:platform exactly and
never an alias the caller requests, that the OpenBao client gains no
cross-tenant reach, and a fixture guard pinning every reviewed client's tenant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-06 22:30:32 +02:00
|
|
|
tenant: tenant:platform
|
2026-09-05 01:08:58 +02:00
|
|
|
scopes: [approval:read, approval:consume]
|
|
|
|
|
lifetime: 15m
|
|
|
|
|
proposed_openbao_path: platform/workloads/secrets-engine/approval-client
|
2026-09-08 16:46:02 +02:00
|
|
|
field: CLIENT_SECRET
|
2026-09-05 01:08:58 +02:00
|
|
|
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
|
Align approval registrations to the tenant:platform decision
Operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 accepts tenant:platform
as the platform management tenant for the Glas approval chain, requiring exact
spelling across the approval store, the service-client JWT claim and the
lifecycle CheckRequest.
Changes the tenant field on secrets-engine-approval and approval-engine-operator
only, in the registration fixture and the provisioning packet. Unrelated clients
and the human directory default keep tenant:coulomb, and no audience, scope,
subject, role, lifetime or MFA grant changes.
Adds issuance evidence that the approval shape emits tenant:platform exactly and
never an alias the caller requests, that the OpenBao client gains no
cross-tenant reach, and a fixture guard pinning every reviewed client's tenant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-06 22:30:32 +02:00
|
|
|
tenant: tenant:platform
|
2026-09-05 01:08:58 +02:00
|
|
|
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
|
2026-09-08 16:46:02 +02:00
|
|
|
field: CLIENT_SECRET
|
2026-09-05 01:08:58 +02:00
|
|
|
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
|
2026-09-08 16:46:02 +02:00
|
|
|
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]
|
2026-09-05 01:08:58 +02:00
|
|
|
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]
|
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.
|
2026-09-05 01:08:58 +02:00
|
|
|
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"
|
2026-09-05 01:08:58 +02:00
|
|
|
- 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.
|
2026-09-05 01:08:58 +02:00
|
|
|
blockers:
|
|
|
|
|
- Admit exact custody paths, field delivery, consumer identities and lifecycle authority.
|
|
|
|
|
- Resolve attended first-provision authority through the custody owner.
|
2026-09-08 16:46:02 +02:00
|
|
|
- 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.
|