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
This commit is contained in:
tegwick 2026-09-06 22:33:50 +02:00
parent 5c87ba8610
commit 6d18f62a90
8 changed files with 182 additions and 38 deletions

View file

@ -21,44 +21,57 @@ Required claims remain those in `docs/caller-authentication.md`: `iss`, `sub`,
| 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 reconciliation — unresolved, blocks token issuance
## Tenant — resolved: exact `tenant:platform`
**This is a live collision, not a naming preference.** `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
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.
no prefix handling anywhere in this engine — deliberately. `platform` is **not**
an accepted alias for `tenant:platform`, and neither is `tenant:coulomb`.
The three values currently in play:
Rendered values, all three now exactly `tenant:platform`:
| Value | Where it is set | Current content |
| Value | Where it is set | Content |
| --- | --- | --- |
| Store tenant | `deploy/approval-engine.yaml` `--tenant` (CLI default `platform`) | `platform` |
| JWT `tenant` claim | the client registrations below | `tenant:coulomb` |
| CheckRequest tenant | flex-auth policy subject | `tenant:platform`**never read by this engine** |
| 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` |
`platform` != `tenant:coulomb`, so tokens issued under the registrations below
would be denied `403` on every non-health route. Spelling similarity between
`platform` and `tenant:platform` is not a mapping either; the flex-auth policy
subject is a PDP input this engine never inspects, so it cannot participate in
the comparison at all.
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.
Denial evidence: `tests/test_auth.py::test_wrong_tenant_is_forbidden` — a
signature-valid token whose tenant differs from the store tenant is refused
without mutation.
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.
**Resolution is an owner decision and is deliberately not taken here.** Either
KeyCape issues `tenant: platform` to match the store, or this repo's manifest
sets `--tenant tenant:coulomb` to match the registration. Which is correct
depends on whether `platform` and `tenant:coulomb` name the same layer — a
question this engine cannot answer, and answering it wrongly grants a token
cross-tenant access to the approval store. The values below are left as
requested until that mapping is stated by an owner, so the mismatch stays
visible rather than being silently resolved by whichever document was edited
last.
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`.
`SECRETS-WP-0009-T03`, `GLAS-WP-0015`.
## Clients
@ -76,7 +89,7 @@ clients:
secretRef: env:KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET
serviceSubject: service:secrets-engine
principal_type: service
tenant: tenant:coulomb
tenant: tenant:platform
roles: [secrets-engine]
tokenLifetime: 15m
@ -96,7 +109,7 @@ clients:
secretRef: env:KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET
serviceSubject: service:approval-engine-operator
principal_type: service
tenant: tenant:coulomb
tenant: tenant:platform
roles: [approval-operator]
tokenLifetime: 15m
```