approval-engine/docs/keycape-service-registrations.md
tegwick 6d18f62a90 Set the approval store tenant to exact tenant:platform
Operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 accepts tenant:platform
as the platform management, administration and services tenant, with no alias
to platform or tenant:coulomb and no implicit cross-tenant grant. This closes
the collision recorded in 5c87ba8, where the manifest served --tenant platform
while the requested registrations issued tenant:coulomb.

The store tenant is now exactly tenant:platform in the manifest, the CLI
default, and the Engine default, and the requested client registrations ask for
the same spelling. Exact JWT/store equality is retained: no mapping table, no
normalisation, no prefix handling.

Moving the defaults rather than only the manifest is deliberate. A default of
platform under a sanctioned value of tenant:platform is a trap, because a serve
that omits --tenant would come up healthy and then refuse every authenticated
call -- the exact failure this decision exists to prevent.

That default change broke ten tests whose identity fixtures hard-coded
platform. This is the hazard flex-auth reported as FLEX-DEC-2026-008: fixtures
that all carry one tenant prove nothing about the tenant field. Fixtures are
aligned to the exact spelling, and the field is now varied rather than merely
present. test_near_miss_tenant_spellings_are_forbidden refuses platform,
tenant:coulomb, case variants, whitespace variants and empty against a
tenant:platform store; test_exact_sanctioned_tenant_is_admitted pins the other
half so a reject-everything bug cannot pass it. 111 tests pass.

Also records the credential-independent half of the GLAS-WP-0015 image request:
the image builds non-root uid 10001 off the pinned base, carries schema v3 and
the new tenant default, migrates and verifies a fresh store to schema_version 3
with integrity ok, and refuses production without a persistent database or
authenticated audit delivery. No scan was run -- no scanner is installed here --
and no release digest exists, so T01 and T03 both stay open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM5HnEAhokxdfcPqBNpT7D

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715850@bnt-lap001
Assistant-Session: eb557e93-7cb1-45d0-9e57-7d15b3edc60e
2026-09-06 22:33:50 +02:00

5 KiB

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.

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.