key-cape/docs/tenant-claim-contract.md
tegwick 7a6666d1f9
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 33s
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

4.7 KiB

Tenant claim contract

States what KeyCape owns and emits for the tenant claim. Raised by glas-harness message 356f6977-d361-4e3b-83ab-b2c7f4759286 (GLAS-WP-0015) and resolved by operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 — "Use tenant:platform for the Glas approval dependency chain", recorded in glas-harness/docs/platform-tenant-decision.md.

tenant:platform is the platform management, administration and services tenant (the landlord zone). KeyCape emits it verbatim for the two approval clients and still creates no mapping between tenant vocabularies: no alias to platform or tenant:coulomb, and no implicit cross-tenant grant.

What KeyCape emits

The tenant claim is bound at registration or directory-resolution time and is never influenced by request parameters.

Principal Source of tenant Value in the reviewed registrations
Service (client_credentials) The client's tenant field. Required by config validation — a client_credentials client without serviceSubject and tenant fails startup validation. In config/service-clients.example.yaml: tenant:platform for secrets-engine-approval and approval-engine-operator; tenant:coulomb for codex-railiance-platform and secrets-engine-openbao.
Human (authorization_code) The directory user's tenant, falling back to the platform default when unset. Directory value, else tenant:coulomb.

tenant, tenant_hint, audience and resource request parameters cannot change the claim. tenant_hint on /authorize reaches registration/enrollment handoffs only; it is not a claim input.

The approval chain, after the decision

All three layers now use one exact spelling, tenant:platform:

Layer Value Owner
Approval store tenant tenant:platform approval-engine
Service-client JWT tenant claim tenant:platform KeyCape (this repo)
Lifecycle CheckRequest tenant tenant:platform flex-auth

KeyCape performs no normalization, prefix-stripping or aliasing, so a resource server compares tenant as an exact string. The previously observed platform and tenant:coulomb spellings are not accepted aliases for tenant:platform; a token carrying either must be denied by the resource server.

Scope of the alignment

The decision changed the tenant field on exactly two registrations, secrets-engine-approval and approval-engine-operator, plus the matching entries in docs/approval-engine-provisioning-request.yaml. Deliberately unchanged:

  • codex-railiance-platform and secrets-engine-openbao keep tenant:coulomb.
  • Human directory resolution keeps its tenant:coulomb default.
  • No cross-tenant grant is implied: a tenant:platform token conveys no reach into tenant:coulomb resources, and audiences, scopes, subjects, roles, lifetimes and MFA requirements are untouched.

This resolves the choice of tenant only. Live provisioning of the two clients remains gated on deployment-owned custody and the separate verification gates in KEY-WP-0013-T02; KeyCape changed no live registration or policy subject.

Evidence

src/internal/server/oidc/tenant_test.go:

  • TestServiceTenantIsBoundToRegistrationAndIgnoresRequestParameters — a request supplying tenant=tenant:platform and tenant_hint=platform still yields the registered tenant, signature-verified against /jwks.
  • TestServiceTenantsAreDistinctPerRegistration — two registrations with different tenants yield their own values and never each other's; this is the wrong-tenant denial basis for an exact-comparison resource server.
  • TestHumanTenantClaimUsesDirectoryValueThenPlatformDefault — human tokens carry the directory tenant, defaulting to tenant:coulomb rather than an empty claim.
  • TestApprovalClientIssuesExactPlatformTenantAndRejectsAliases — the approval-client shape issues tenant:platform exactly, with aud=approval-engine, even when the caller asks for platform, tenant:coulomb or TENANT:PLATFORM.
  • TestUnrelatedServiceClientKeepsCoulombTenant — the OpenBao login client keeps tenant:coulomb even when the request asks for tenant:platform, so the alignment grants no cross-tenant reach.

src/cmd/keycape/clients_test.go:

  • TestServiceRegistrationTenantsAreExactPerDecision — loads the real registration fixture and pins the exact tenant of every reviewed client, so drift or an alias reintroduced into config/service-clients.example.yaml fails the build.

These are local issuance proofs. They are not live-rollout evidence; see docs/approval-engine-auth-contract.md and KEY-WP-0013-T02 for the deployment boundary. No token or secret values appear in this document or in test output.