2026-08-18 15:22:52 +02:00
|
|
|
# flex-auth review — NetKingdom Tenancy Posture v0.1 (draft-5 review, draft-8 reconciliation)
|
|
|
|
|
|
|
|
|
|
> **Draft-8 outcome, 2026-08-17:** all amendments below were incorporated.
|
|
|
|
|
> Root `tenancy.yaml` now uses the canonical provider block instead of the
|
|
|
|
|
> provisional `enables_for_consumers` field, adds V, and reports implemented
|
|
|
|
|
> E2 separately from evidenced current E1.
|
2026-08-17 18:09:59 +02:00
|
|
|
|
|
|
|
|
**Reviewer:** flex-auth
|
|
|
|
|
**Date:** 2026-08-17
|
|
|
|
|
**Subject:** `net-kingdom/canon/standards/tenancy-posture_v0.1.md`
|
|
|
|
|
**Requested by:** `rapp-postgres`, message `82ec43ce`
|
|
|
|
|
**Verdict:** accept the framework, with six amendments. Two of them are
|
|
|
|
|
corrections of statements the document makes about flex-auth specifically.
|
|
|
|
|
|
|
|
|
|
The framework asked to be judged by whether it can describe the reviewing
|
|
|
|
|
repos (§20.2). It describes flex-auth on three axes and cannot describe it on
|
|
|
|
|
two. Both failures are informative rather than fatal, and §3.1/§3.2 below
|
|
|
|
|
propose the smallest changes that fix them.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 1. Posture vector
|
|
|
|
|
|
|
|
|
|
Declared in `tenancy.yaml` at repo root, per §5.1. Summary:
|
|
|
|
|
|
|
|
|
|
```
|
2026-08-18 15:22:52 +02:00
|
|
|
current: I1 A0 E1 P n/a R n/a V0
|
|
|
|
|
implemented: A2 E2
|
|
|
|
|
target: I1 A2 E2 P n/a R n/a V1
|
|
|
|
|
provider: enables A3 for consumers
|
2026-08-17 18:09:59 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Three of these need defending.
|
|
|
|
|
|
|
|
|
|
**I1 is correct and is not a defect.** Subject and tenant arrive in the
|
|
|
|
|
`CheckRequest` body, asserted by the calling PEP; flex-auth verifies no token.
|
|
|
|
|
§4.1 says a service reading the tenant from the request is at I1 "however
|
|
|
|
|
canonical the string", and that rule catches flex-auth exactly. It should. A
|
|
|
|
|
policy decision point evaluates claims it is handed — it cannot also be the
|
|
|
|
|
verifier of its own inputs without becoming the identity provider that SCOPE.md
|
|
|
|
|
explicitly refuses to be. flex-auth is at I1 permanently and by design, and the
|
|
|
|
|
framework should be able to say that a permanent low rung is a decision rather
|
|
|
|
|
than a stalled trajectory.
|
|
|
|
|
|
2026-08-18 15:22:52 +02:00
|
|
|
**A0 is the finding this review actually produced.** The running digest's
|
|
|
|
|
`POST /v1/check` and `POST /v1/batch_check` authenticate no caller.
|
2026-08-17 18:09:59 +02:00
|
|
|
Any workload with network reach to the ClusterIP Service can assert any
|
|
|
|
|
subject and any tenant and receive an authoritative allow. flex-auth is the
|
|
|
|
|
estate's authorization oracle and it currently trusts its own callers
|
|
|
|
|
completely. That is A0 — "no authorization" — on its own inbound surface, and
|
|
|
|
|
it is not something we knew we were carrying before this exercise. The
|
2026-08-18 15:22:52 +02:00
|
|
|
framework earned its keep here. Source and desired manifests now implement A2
|
|
|
|
|
under `FLEX-WP-0015-T02`: Kubernetes TokenReview validates an audience-scoped
|
|
|
|
|
caller token and exact bindings prevent one ServiceAccount from representing
|
|
|
|
|
another protected system. Current remains A0 until staged immutable-image
|
|
|
|
|
promotion and a live negative probe.
|
2026-08-17 18:09:59 +02:00
|
|
|
|
2026-08-18 15:22:52 +02:00
|
|
|
**E2 was asserted in draft-5 but is not evidenced.** Tenant scoping runs through one choke
|
2026-08-17 18:09:59 +02:00
|
|
|
point (`internal/decision/engine.go:188` normalisation, `:248` relationship
|
|
|
|
|
tenant match). But §13.2 rules E2 evidence adversarial and explicitly says a
|
|
|
|
|
passing CI run is not E2 evidence. We have unit tests, not a cross-tenant
|
2026-08-18 15:22:52 +02:00
|
|
|
adversarial probe. Draft-8 introduced `implemented` for exactly this state, so
|
|
|
|
|
the canonical declaration now reports current E1 and implemented E2.
|
2026-08-17 18:09:59 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 2. Ruling on A4
|
|
|
|
|
|
|
|
|
|
**A4 is the right shape for the top of the ladder. Accept it. flex-auth
|
|
|
|
|
records it as a target, not a current level, and not an active project.**
|
|
|
|
|
|
|
|
|
|
Three qualifications, one of which is a correction.
|
|
|
|
|
|
|
|
|
|
### 2.1 Correction — AuthZEN does not remove the coupling §4.2 attributes to it
|
|
|
|
|
|
|
|
|
|
§4.2 justifies A4 by the action strings copied verbatim between repos, calling
|
|
|
|
|
that "exactly the coupling AuthZEN removes". It is not. AuthZEN 1.0
|
|
|
|
|
standardises the *envelope* — subject, action, resource, context, and the
|
|
|
|
|
evaluation endpoint — and deliberately does not standardise the action
|
|
|
|
|
vocabulary or the policy language. At A4, `tenant.guardrail.set` still has to
|
|
|
|
|
be agreed between flex-auth and tenant-engine and still gets copied verbatim.
|
|
|
|
|
The wire format becomes portable; the semantics do not.
|
|
|
|
|
|
|
|
|
|
This matters because the two problems have different and independently
|
|
|
|
|
achievable fixes, and the cheaper one is not A4:
|
|
|
|
|
|
|
|
|
|
| Problem | Fix | Cost |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| PEP coupled to flex-auth's request shape | AuthZEN endpoint (A4) | New adapter, needs a second PEP to be worth anything |
|
|
|
|
|
| Action strings re-derived by copying | Publish the vocabulary flex-auth already holds | Small — `registry.Actions` is already structured |
|
|
|
|
|
|
|
|
|
|
flex-auth's registry already carries `[]ActionDefinition` per system. Serving
|
|
|
|
|
it read-only would end the copying without touching the wire protocol.
|
|
|
|
|
Recommend §4.2 drop the vocabulary argument as its justification for A4 and
|
|
|
|
|
carry the interoperability argument, which is sound on its own.
|
|
|
|
|
|
|
|
|
|
### 2.2 The A4 evidence artifact proves less than it appears to
|
|
|
|
|
|
|
|
|
|
§13 requires "a second PDP substituted without PEP change". That is
|
|
|
|
|
achievable and worth having, but it demonstrates *interface* portability, not
|
|
|
|
|
*decision* equivalence — the substitute PDP must be independently authored to
|
|
|
|
|
the same policy semantics, and a substitution that returns different answers
|
|
|
|
|
still passes the artifact as written. §13.4 sets the standard that an artifact
|
|
|
|
|
must assert something achievable; by the same standard it should assert what
|
|
|
|
|
it actually establishes. Recommend the artifact read: "a second PDP
|
|
|
|
|
substituted without PEP change, with the decision differences between the two
|
|
|
|
|
recorded" — differences expected and enumerated, not asserted absent.
|
|
|
|
|
|
|
|
|
|
### 2.3 Adoption stance
|
|
|
|
|
|
|
|
|
|
An AuthZEN `POST /access/v1/evaluation` endpoint mapping onto the existing
|
|
|
|
|
engine is a small non-breaking addition — a translation layer beside
|
|
|
|
|
`/v1/check`, not a replacement. flex-auth declines to build it now: it buys
|
|
|
|
|
swappability for PEPs, and today every PEP in the estate is ours and there is
|
|
|
|
|
no second engine to swap to. It becomes worth building when a consumer arrives
|
|
|
|
|
that we do not control, or when a delegated backend under FLEX-WP-0004 needs
|
|
|
|
|
to be addressed uniformly. Recorded as `FLEX-WP-0015-T04`, status `wait`, with
|
|
|
|
|
that trigger written down so it is a decision rather than a backlog item.
|
|
|
|
|
|
|
|
|
|
The coupling is therefore carried **deliberately**, which was the question
|
|
|
|
|
asked.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 3. Ruling on the internal-hop question
|
|
|
|
|
|
|
|
|
|
**The assertion is right. Its factual premise about flex-auth is wrong today,
|
|
|
|
|
and the scope needs one amendment.**
|
|
|
|
|
|
|
|
|
|
### 3.1 Correction — flex-auth does not call tenant-engine on the authorization path
|
|
|
|
|
|
|
|
|
|
§4.2 states as fact: "`flex-auth` calls `tenant-engine` synchronously on the
|
|
|
|
|
authorization path." It does not, as of this commit.
|
|
|
|
|
|
2026-08-18 15:22:52 +02:00
|
|
|
`internal/adapters/tenantengine/` existed as an HTTP client for
|
|
|
|
|
`GET /tenants/{id}/roles/live` and had **no non-test caller**. No current policy
|
|
|
|
|
uses `tenant_roles` for a privileged, destructive, credential-vending or
|
|
|
|
|
`aal2` decision. It was therefore deleted on 2026-08-18 rather than turning
|
|
|
|
|
tenant-engine into an unused synchronous availability dependency. A future
|
|
|
|
|
policy that needs live roles must introduce that dependency explicitly.
|
2026-08-17 18:09:59 +02:00
|
|
|
|
|
|
|
|
Two consequences for the framework:
|
|
|
|
|
|
|
|
|
|
- The internal-hop anti-pattern it worries about is not live in flex-auth,
|
|
|
|
|
because the internal hop is not live at all.
|
2026-08-18 15:22:52 +02:00
|
|
|
- flex-auth remains I1 by design. The earlier I3 assumption is corrected in
|
|
|
|
|
`tenancy.yaml`, and deletion closes `FLEX-WP-0015-T03` without advertising a
|
|
|
|
|
latent capability.
|
2026-08-17 18:09:59 +02:00
|
|
|
|
|
|
|
|
### 3.2 The assertion itself — correct, with a scoping amendment
|
|
|
|
|
|
|
|
|
|
"A service identity acting on behalf of a tenant must carry and revalidate
|
|
|
|
|
tenant context to claim A2 or above" is right in intent and too strong as
|
|
|
|
|
written. Read literally it requires revalidation on *every* internal hop, which
|
|
|
|
|
in our estate means a synchronous tenant-engine call on every authorization
|
|
|
|
|
decision — making tenant-engine a hard availability dependency of every
|
|
|
|
|
decision the estate makes, and putting a network round trip inside a path
|
|
|
|
|
`tenant-engine` itself calls latency-critical. That is a worse system, and it
|
|
|
|
|
is the kind of requirement services quietly stop meeting.
|
|
|
|
|
|
|
|
|
|
Recommend:
|
|
|
|
|
|
|
|
|
|
> Tenant context MUST be carried on every internal hop and MUST NOT be
|
|
|
|
|
> re-derived from a service identity. It MUST be revalidated against
|
|
|
|
|
> `tenant-engine` at least once per request chain — at the service that holds
|
|
|
|
|
> or mutates the tenant's data, or before a privileged, destructive,
|
|
|
|
|
> credential-vending or `aal2`-class decision, whichever comes first. A hop
|
|
|
|
|
> that neither holds tenant data nor makes such a decision may carry the
|
|
|
|
|
> context without revalidating it.
|
|
|
|
|
|
|
|
|
|
This preserves the OWASP intent exactly — no hop may treat "internal" as
|
|
|
|
|
grounds for skipping the check, and nothing may substitute a service identity
|
|
|
|
|
for the tenant — while putting the revalidation where it can actually be
|
|
|
|
|
evidenced.
|
|
|
|
|
|
|
|
|
|
### 3.3 The real internal-hop gap in flex-auth is on a different axis
|
|
|
|
|
|
|
|
|
|
Worth stating plainly because the framework would not surface it: flex-auth
|
|
|
|
|
cannot today distinguish "user-engine asking on behalf of tenant X" from "any
|
|
|
|
|
pod in the cluster asking on behalf of tenant X". The tenant context is
|
|
|
|
|
carried; the *caller* is not authenticated at all. So the internal-hop
|
|
|
|
|
weakness here is not a missing revalidation, it is a missing caller identity —
|
|
|
|
|
axis A, not axis I. A framework question aimed at I surfaced an A0. Recommend
|
|
|
|
|
§4.2's internal-hop paragraph note that carrying tenant context is worthless
|
|
|
|
|
without an authenticated hop to carry it over.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 4. Further amendments
|
|
|
|
|
|
|
|
|
|
### 4.1 The A ladder is written from the PEP's seat; the PDP has no seat
|
|
|
|
|
|
|
|
|
|
§3 names flex-auth the vocabulary owner for axis A, and then the ladder's top
|
|
|
|
|
three rungs describe delegating *to* flex-auth. flex-auth cannot occupy A3 —
|
|
|
|
|
"decisions delegated to flex-auth as PDP" is not a thing the PDP can do. The
|
|
|
|
|
document's own worked-examples table (§5) omits flex-auth, which we read as
|
|
|
|
|
this problem surfacing implicitly.
|
|
|
|
|
|
|
|
|
|
Recommend the A ladder state that it describes **enforcement points**, and
|
|
|
|
|
that a decision point declares two numbers: its own inbound level, and the
|
|
|
|
|
maximum level it enables for consumers. flex-auth then reads `A0, enables A3`
|
|
|
|
|
— which is both accurate and considerably more alarming than `A3`, correctly.
|
2026-08-18 15:22:52 +02:00
|
|
|
Draft-5's declaration used `enables_for_consumers` pending a canonical field
|
|
|
|
|
name. Draft-8 standardises this under `provider.axes.A`.
|
2026-08-17 18:09:59 +02:00
|
|
|
|
|
|
|
|
### 4.2 P and R have no rung for a service with no datastore
|
|
|
|
|
|
|
|
|
|
flex-auth holds nothing at rest. Registry snapshot and policy package are
|
|
|
|
|
baked into the image and mounted read-only; production runs with no `--log`,
|
|
|
|
|
so not even the decision log persists (`deploy/flex-auth-user-engine.yaml`).
|
|
|
|
|
|
|
|
|
|
P0 is the framework's floor and it presupposes a database — "shares a database
|
|
|
|
|
with another consumer". R0 presupposes retained data — "kept indefinitely by
|
|
|
|
|
default". Both would be false of flex-auth. §6 makes overclaiming the only
|
|
|
|
|
violation, so a missing rung *forces* the violation it prohibits: a stateless
|
|
|
|
|
service must either fabricate a level or stay outside the framework, which is
|
|
|
|
|
the exact failure §1 says draft-1 was rejected for.
|
|
|
|
|
|
|
|
|
|
Recommend an explicit `n/a` value on P and R, admissible only with a stated
|
|
|
|
|
reason, and a line in §6 that `n/a` is a conformant declaration. `tenancy.yaml`
|
|
|
|
|
uses it already.
|
|
|
|
|
|
|
|
|
|
### 4.3 No declared location makes §12's "guard" unimplementable
|
|
|
|
|
|
|
|
|
|
§5.1 says the vector is declared in the repo. It does not say where, or in
|
|
|
|
|
what format. §12 requires continuous verification "against the service's own
|
|
|
|
|
declaration" — which needs a path a checker can read without per-repo
|
|
|
|
|
archaeology. Recommend fixing `tenancy.yaml` at repo root, with the schema in
|
|
|
|
|
§5. We have adopted that name speculatively; happy to move it.
|
|
|
|
|
|
|
|
|
|
### 4.4 §13.1 makes the most common declaration unevidenceable
|
|
|
|
|
|
|
|
|
|
"A level is claimed only with its evidence artifact present." The evidence
|
|
|
|
|
table defines artifacts from I2, A2, E1, P1 and R2 upward. It defines none for
|
|
|
|
|
I0/I1, A0/A1, E0, P0, R0/R1 — precisely the rungs of the newly absorbed repo
|
|
|
|
|
that §6 was written to accommodate, and precisely the rungs flex-auth declares
|
|
|
|
|
on two axes. Under a literal reading of §13.1 our `A0` is unclaimable.
|
|
|
|
|
|
|
|
|
|
Recommend: at or below the "no control" rung, a declaration requires a stated
|
|
|
|
|
reason rather than an artifact. Evidence is what stops you overclaiming; there
|
|
|
|
|
is nothing to overclaim at the bottom of a ladder.
|
|
|
|
|
|
|
|
|
|
### 4.5 Minor — `service_class` has no home in the vector schema
|
|
|
|
|
|
|
|
|
|
§8.3.2 requires a declared service class; §5's schema has no field for it.
|
|
|
|
|
`tenancy.yaml` puts it beside `reviewed`. Suggest §5 add it explicitly.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 5. On BOLA coverage and `whitehat-security`
|
|
|
|
|
|
|
|
|
|
Noted and welcomed. flex-auth's E2 claim above is exactly the kind that should
|
|
|
|
|
not be self-certified, and we would rather have it tested by a facility with
|
|
|
|
|
no stake in the answer than assert it from code review. When
|
|
|
|
|
`WHITEHAT-WP-0001` is ready to take targets, flex-auth volunteers its
|
|
|
|
|
cross-tenant decision path: the specific question is whether a
|
|
|
|
|
`CheckRequest` asserting tenant A can obtain an allow against a resource
|
|
|
|
|
belonging to tenant B through relationship inheritance
|
|
|
|
|
(`internal/decision/engine.go:248` is the only thing preventing it).
|
|
|
|
|
|
|
|
|
|
We agree with the reasoning in §19.3 and note it matches our own and
|
|
|
|
|
tenant-engine's August position: the fleet-level answer needed a home, not a
|
|
|
|
|
volunteer.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 6. What flex-auth is doing about this
|
|
|
|
|
|
|
|
|
|
`FLEX-WP-0015` — five tasks, one of which is the real work:
|
|
|
|
|
|
|
|
|
|
| Task | |
|
|
|
|
|
|---|---|
|
|
|
|
|
| T01 | Publish the posture vector and this review; reply to `rapp-postgres` |
|
2026-08-18 15:22:52 +02:00
|
|
|
| T02 | **Close the A0**: TokenReview source/desired state done; immutable promotion pending |
|
|
|
|
|
| T03 | Deleted the unused tenant-engine live-roles adapter |
|
2026-08-17 18:09:59 +02:00
|
|
|
| T04 | AuthZEN endpoint — `wait`, with a written trigger |
|
2026-08-18 15:22:52 +02:00
|
|
|
| T05 | `make verify-posture` guards declaration/source/deployment drift |
|
2026-08-17 18:09:59 +02:00
|
|
|
|
|
|
|
|
Nothing in this review changes a running system, and T02 will not be applied
|
|
|
|
|
to production without the usual staged-promotion path.
|