flex-auth stays the only PDP. Zone membership reaches /v1/check as compiled registry data, never a synchronous lookup. Uncommitted tenant-engine read/role/grouping policy was discarded, not mixed into this pin.
15 KiB
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.yamlnow uses the canonical provider block instead of the provisionalenables_for_consumersfield, adds V, and reports implemented E2 separately from evidenced current E1.
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:
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
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.
A0 is the finding this review actually produced. The running digest's
POST /v1/check and POST /v1/batch_check authenticate no caller.
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
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.
E2 was asserted in draft-5 but is not evidenced. Tenant scoping runs through one choke
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
adversarial probe. Draft-8 introduced implemented for exactly this state, so
the canonical declaration now reports current E1 and implemented E2.
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.
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.
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.
- flex-auth remains I1 by design. The earlier I3 assumption is corrected in
tenancy.yaml, and deletion closesFLEX-WP-0015-T03without advertising a latent capability.
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-engineat least once per request chain — at the service that holds or mutates the tenant's data, or before a privileged, destructive, credential-vending oraal2-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.
Draft-5's declaration used enables_for_consumers pending a canonical field
name. Draft-8 standardises this under provider.axes.A.
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 |
| T02 | Close the A0: both pins live enforce + unbound-request 401 (2026-08-19) |
| T03 | Deleted the unused tenant-engine live-roles adapter |
| T04 | AuthZEN endpoint — wait, with a written trigger |
| T05 | make verify-posture guards declaration/source/deployment drift |
Nothing in this review changes a running system, and T02 will not be applied to production without the usual staged-promotion path.
7. Zone membership reaches flex-auth by compilation only
Recorded 2026-08-19 in reply to ops-warden (fd4b2012) after zone-engine was
seeded. Hold both sides to this:
- flex-auth remains the only policy decision point.
- zone-engine is authority over zone identity, membership, and exception
lifecycle. It is never a second PDP and never a synchronous lookup on
POST /v1/check. - Membership (and any zone fact a decision may read) is compiled into the
registry snapshot the same way ops-warden's production actor inventory is
(
build_flex_auth_registry.py).service_class: latency-critical. - Per-zone enforcement stance for controls flex-auth does not evaluate
(agent read-boundary on
risk: highlanes, warden desk escalation) does not belong in the policy package. Putting it there would split authorization again. Facts we do evaluate at check time can be compiled data that existing Rego reads.
flex-auth-ops-warden stays callerAuth.mode: enforce. Recreating that
Service would break the ops-bridge ClusterIP pin 10.43.1.165; warn
ops-warden first.