118 lines
5.6 KiB
Markdown
118 lines
5.6 KiB
Markdown
|
|
# Tenant alignment
|
||
|
|
|
||
|
|
Answer to the `GLAS-WP-0015` production-dependency handoff question, which asked
|
||
|
|
this engine to state its exact tenant values and produce wrong-tenant denial
|
||
|
|
evidence rather than assume a mapping. Related: `KEY-WP-0013-T02`,
|
||
|
|
`APPROVAL-WP-0002-T01`, `SECRETS-WP-0009-T03`, `FLEX-WP-0021-T05`.
|
||
|
|
|
||
|
|
The handoff's framing is adopted here without reservation: **spelling similarity
|
||
|
|
is not a mapping**, and JWT/store comparison is exact. A tenant "mapping"
|
||
|
|
invented by a consumer is the same fail-open shape gate-house rejected for
|
||
|
|
action vocabularies in `GH-DEC-2026-008` — a translation can be confidently
|
||
|
|
wrong — arriving by a different road.
|
||
|
|
|
||
|
|
## The three values, as this repo actually holds them
|
||
|
|
|
||
|
|
| Value | Where it lives | Owner |
|
||
|
|
| --- | --- | --- |
|
||
|
|
| CheckRequest tenant | `authorization.REQUEST_TENANT` = `tenant:platform` | flex-auth package |
|
||
|
|
| KeyCape JWT tenant | `service_auth.TENANT` = `tenant:coulomb` | key-cape |
|
||
|
|
| Approval store tenant | `platform` (not held here) | approval-engine |
|
||
|
|
|
||
|
|
### CheckRequest tenant: `tenant:platform`, and it was missing entirely
|
||
|
|
|
||
|
|
`secrets-engine.catalog-lane.lifecycle` **v2** reads
|
||
|
|
|
||
|
|
```rego
|
||
|
|
known_tenant := "tenant:platform"
|
||
|
|
request_tenant := object.get(input, "tenant", "")
|
||
|
|
```
|
||
|
|
|
||
|
|
and its `wrong_tenant` first-denial branch fires on anything else. `object.get`
|
||
|
|
with a `""` default is deliberate: **an absent tenant is a denial, not an
|
||
|
|
ignored field.**
|
||
|
|
|
||
|
|
`build_action_request` emitted no `tenant` field at all. Every gated action
|
||
|
|
this engine sent would have been denied `wrong_tenant` by the deployed v2
|
||
|
|
package — and, worse, the omission also produced a `request_digest` that could
|
||
|
|
match no correctly issued decision, because `tenant` is hashed material. Fixed:
|
||
|
|
the request now carries `REQUEST_TENANT`, an empty tenant is refused at build
|
||
|
|
time, and the constant is pinned against the vendored allow envelopes so a
|
||
|
|
package retenanting surfaces as a test failure rather than a production denial.
|
||
|
|
|
||
|
|
### KeyCape JWT tenant: `tenant:coulomb` — and that is the value the package denies
|
||
|
|
|
||
|
|
This is the uncomfortable part and it is stated plainly rather than smoothed
|
||
|
|
over. `service_auth.TENANT` is `tenant:coulomb`, preflighted on the KeyCape
|
||
|
|
`client_credentials` token for the accepted `secrets-engine-openbao` service
|
||
|
|
identity. `tenant:coulomb` is **exactly** the value flex-auth used to
|
||
|
|
demonstrate a wrong-tenant denial (`decision_wrong_tenant_deny.json`,
|
||
|
|
`matched_rule: wrong_tenant`).
|
||
|
|
|
||
|
|
Two readings are possible and this engine does not choose between them:
|
||
|
|
|
||
|
|
1. They name **two different layers** — an identity/tenancy tenant for the
|
||
|
|
KeyCape client, and a resource-scoping tenant for the policy package — which
|
||
|
|
happen to use one namespace format.
|
||
|
|
2. One of the two constants is **wrong**.
|
||
|
|
|
||
|
|
Reading 1 is plausible and is probably right, but "probably right" is not a
|
||
|
|
contract. Both constants stay as they are, deliberately not unified behind one
|
||
|
|
symbol, until an owner-reviewed mapping exists with a decision reference. This
|
||
|
|
engine will consume that mapping; it will not author it.
|
||
|
|
|
||
|
|
### Approval store tenant: `platform`
|
||
|
|
|
||
|
|
Not held in this repo. Noted only because the handoff asked for all three: the
|
||
|
|
comparison between `platform` and `tenant:platform` is a prefix difference, and
|
||
|
|
a prefix difference is precisely the kind of similarity that must not be treated
|
||
|
|
as identity without an owner saying so.
|
||
|
|
|
||
|
|
## Wrong-tenant denial evidence
|
||
|
|
|
||
|
|
From flex-auth's real v2 deny envelope, vendored at
|
||
|
|
`tests/fixtures/flex-auth-replay/decision_wrong_tenant_deny.json`:
|
||
|
|
|
||
|
|
```text
|
||
|
|
decision:7d56b7fc274ddfd6 effect: deny reason: wrong_tenant
|
||
|
|
matched_rule: wrong_tenant policy_version: v2
|
||
|
|
binding.tenant: tenant:coulomb
|
||
|
|
binding.request_digest: sha256:c9c6e6f8...0d20
|
||
|
|
```
|
||
|
|
|
||
|
|
flex-auth's fixture varies the tenant on an otherwise-valid `rotate`, so a deny
|
||
|
|
proves the tenant alone carried it. `tests/test_decision_replay.py` asserts our
|
||
|
|
consumer refuses it on `effect` before anything else, and separately pins that a
|
||
|
|
deny carries **no** `lifetime` — a consumer checking lifetime before effect would
|
||
|
|
raise a confusing missing-field error on a well-formed denial.
|
||
|
|
|
||
|
|
## Why v1 must not be pinned
|
||
|
|
|
||
|
|
`v1` shipped and was deployed with no reference to `input.tenant` at all. Every
|
||
|
|
fixture carried `tenant:platform`, so the package's own coverage could not
|
||
|
|
notice, and `FLEX-WP-0021-T02`'s "wrong-tenant deny" gate was recorded as met
|
||
|
|
when it was not. A `rotate` under `tenant:coulomb` returned **allow** against the
|
||
|
|
deployed v1 package (`decision:066e629bbf0c0924`).
|
||
|
|
|
||
|
|
flex-auth superseded v1 rather than amending it, on the principle that **a
|
||
|
|
fail-open correction has to be visible as a version change; a fail-closed one
|
||
|
|
does not.** A consumer still pinned to `v1` would keep receiving allows it should
|
||
|
|
never have had, unable to tell from the version string that the rule moved
|
||
|
|
underneath it. `test_the_superseded_v1_package_is_not_accepted` pins that this
|
||
|
|
engine refuses a v1 decision.
|
||
|
|
|
||
|
|
Note the ownership point flex-auth recorded with it: the evaluator hashes
|
||
|
|
`tenant` and carries it in the decision record, but nothing in the evaluation
|
||
|
|
path compares it. **Tenant scoping is the policy package's job, and a package
|
||
|
|
that omits it is not scoped to a tenant at all.** Our own omission was the
|
||
|
|
mirror-image defect on the consumer side, and neither side's tests could see it
|
||
|
|
alone.
|
||
|
|
|
||
|
|
## Still open
|
||
|
|
|
||
|
|
- The owner-reviewed JWT/store/CheckRequest mapping, with a decision reference.
|
||
|
|
Until it exists, no live client or policy subject changes here.
|
||
|
|
- A supported owner access path to `flex-auth-secrets-engine` for a workstation
|
||
|
|
CLI. Service DNS is not workstation connectivity, so the tenant fix above is
|
||
|
|
necessary but not sufficient for activation (`FLEX-WP-0021-T05`).
|