225 lines
14 KiB
Markdown
225 lines
14 KiB
Markdown
|
|
# Plane A — Authorization: prior knowledge and best practices
|
|||
|
|
|
|||
|
|
> Research artefact — 2026-08-17. Supports `canon/architecture/adr-008-multi-tenancy-model.md`
|
|||
|
|
> (draft-2), plane A of four. Surveys external practice for binding a request
|
|||
|
|
> to the tenants it may act for, and holds our `A0–A3` ladder against it.
|
|||
|
|
> Companions: plane I (identity), plane E (enforcement), plane P (placement).
|
|||
|
|
|
|||
|
|
## 1. What the plane covers
|
|||
|
|
|
|||
|
|
How a request is bound to the tenants its principal may act for, where that
|
|||
|
|
decision is made, and how the decision point is kept honest. Our ladder:
|
|||
|
|
|
|||
|
|
| Level | State |
|
|||
|
|
|---|---|
|
|||
|
|
| A0 | None, or tenant context not carried |
|
|||
|
|
| A1 | Ad-hoc checks scattered through handlers |
|
|||
|
|
| A2 | Single local authorization boundary; context bound once |
|
|||
|
|
| A3 | Delegated to `flex-auth` as PDP, live re-query where the IAM Profile requires |
|
|||
|
|
|
|||
|
|
## 2. This is the industry's #1 vulnerability class, by name
|
|||
|
|
|
|||
|
|
The single most important external finding: what our framework calls a tenant
|
|||
|
|
boundary failure, the security industry calls **Broken Object Level
|
|||
|
|
Authorization**, and it is not a marginal concern.
|
|||
|
|
|
|||
|
|
- BOLA is **API1** in the OWASP API Security Top 10 and "has held the top spot
|
|||
|
|
since the list launched"
|
|||
|
|
([Decryption Digest — BOLA and IDOR testing guide](https://www.decryptiondigest.com/blog/bola-idor-api-authorization-test-remediation)).
|
|||
|
|
- It is "consistently the most common, the most exploitable, and the most
|
|||
|
|
damaging API vulnerability across bug bounty submissions, public disclosures,
|
|||
|
|
and production penetration tests" (ibid.).
|
|||
|
|
- The multi-tenant framing is explicit: "a single missing authorization check
|
|||
|
|
can let one tenant access another's storage buckets, database records, or
|
|||
|
|
configuration data"
|
|||
|
|
([Wiz — OWASP API security top 10](https://www.wiz.io/academy/api-security/owasp-api-security)).
|
|||
|
|
|
|||
|
|
**Against our model:** ADR-008 §13.2 identifies the missing cross-tenant
|
|||
|
|
evidence artifact as the framework's largest gap. This research upgrades that
|
|||
|
|
from an internal judgement to an industry consensus: we have no test coverage
|
|||
|
|
for the vulnerability class that ranks first globally. The ADR should cite
|
|||
|
|
BOLA/API1 by name so the gap is legible to anyone who arrives with a security
|
|||
|
|
background rather than an architecture one.
|
|||
|
|
|
|||
|
|
## 3. OWASP has a Multi-Tenant Security Cheat Sheet, and we should adopt it wholesale
|
|||
|
|
|
|||
|
|
This is the most directly applicable artefact found in the whole research
|
|||
|
|
effort, and neither ADR-008 nor any repo document references it
|
|||
|
|
([OWASP Multi-Tenant Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Multi_Tenant_Security_Cheat_Sheet.html)).
|
|||
|
|
|
|||
|
|
Its normative requirements, mapped to our ladders:
|
|||
|
|
|
|||
|
|
| OWASP requirement | Our plane | Status |
|
|||
|
|
|---|---|---|
|
|||
|
|
| "Never trust client-supplied tenant IDs without validation" — derive from verified JWT claims | I2 | Implied, not stated. Tighten. |
|
|||
|
|
| "Establish tenant context early in the request lifecycle (middleware/interceptor)"; bind to the authenticated session | A2 | **Matches A2 exactly.** |
|
|||
|
|
| "Always validate that requested resources belong to the current tenant" | E1–E2 | Matches. |
|
|||
|
|
| "Include tenant context in all log entries" | — | **Not covered by any plane.** |
|
|||
|
|
| Tenant-isolated audit trails | — | `audit-core`'s concern; not in the framework. |
|
|||
|
|
| "Ensure complete data deletion on tenant offboarding" | — | **Not covered. See §6.** |
|
|||
|
|
| Prefix all cache keys with tenant identifiers | — | **Not covered** (see plane I §3). |
|
|||
|
|
| Per-tenant rate limiting with tier-based quotas | — | Adjacent to `tenant-engine` guardrails. |
|
|||
|
|
| Separate API keys per tenant | E4-adjacent | Our E4 is credential-based; consistent. |
|
|||
|
|
|
|||
|
|
Explicitly rejected anti-patterns, two of which we should check ourselves
|
|||
|
|
against:
|
|||
|
|
|
|||
|
|
- **"Skipping tenant validation for internal services."** Our estate is mostly
|
|||
|
|
internal service-to-service calls — `flex-auth` calls `tenant-engine`
|
|||
|
|
synchronously on the authorization path, `user-engine` calls it for tenant
|
|||
|
|
creation. Our A ladder describes a request arriving with a subject; it does
|
|||
|
|
not say whether a *service* identity calling on behalf of a tenant carries
|
|||
|
|
and revalidates tenant context. This is a real question for A3.
|
|||
|
|
- **"Exposing sequential or guessable resource IDs."** Not assessed anywhere.
|
|||
|
|
|
|||
|
|
**Proposed action:** reference the cheat sheet from ADR-008 as the external
|
|||
|
|
baseline, and fold its uncovered requirements (logging, cache keys, offboarding
|
|||
|
|
deletion) into either the ladders or an explicit scope exclusion.
|
|||
|
|
|
|||
|
|
## 4. The PDP/PEP split is right, and there is now a standard for the wire
|
|||
|
|
|
|||
|
|
Our A3 delegates decisions to `flex-auth` as PDP, which is the mainstream
|
|||
|
|
architecture — a Policy Enforcement Point intercepts an action and queries a
|
|||
|
|
Policy Decision Point
|
|||
|
|
([DEV — RBAC vs ABAC vs ReBAC](https://dev.to/kanywst/rbac-vs-abac-vs-rebac-how-to-choose-and-implement-access-control-models-3i2d)).
|
|||
|
|
|
|||
|
|
What is new since our contracts were written:
|
|||
|
|
|
|||
|
|
- **OpenID AuthZEN Authorization API 1.0** was approved as an OpenID Final
|
|||
|
|
Specification in January 2026 and published Standards Track in March 2026
|
|||
|
|
([OpenID Foundation — Authorization API 1.0 final specification approved](https://openid.net/authorization-api-1-0-final-specification-approved/),
|
|||
|
|
[spec](https://openid.github.io/authzen/)).
|
|||
|
|
- Its purpose is exactly our seam: letting PDPs and PEPs "communicate
|
|||
|
|
authorization requests and decisions to each other without requiring
|
|||
|
|
knowledge of each other's inner workings"
|
|||
|
|
([Curity — introduction to AuthZEN](https://curity.io/resources/learn/authzen/)).
|
|||
|
|
- Interoperability is demonstrated, not theoretical: one PEP implementation
|
|||
|
|
driving 5+ different PDPs (Topaz, Axiomatics, OpenFGA) by switching endpoint
|
|||
|
|
URLs alone
|
|||
|
|
([DEV — AuthZEN deep dive](https://dev.to/kanywst/authzen-authorization-api-10-deep-dive-the-standard-api-that-separates-authorization-decisions-1m2a)).
|
|||
|
|
- Keycloak shipped experimental AuthZEN support in May 2026
|
|||
|
|
([Keycloak — AuthZEN as experimental feature](https://www.keycloak.org/2026/05/authzen-as-experimental-feature)).
|
|||
|
|
|
|||
|
|
**Against our model:** `flex-auth` uses a bespoke `CheckRequest` shape and a
|
|||
|
|
bespoke action vocabulary (`tenant.update`, `tenant.guardrail.set`, …), with
|
|||
|
|
action strings copied verbatim between repos to avoid re-derivation. That
|
|||
|
|
coupling is exactly what AuthZEN standardises away. We are not wrong — we are
|
|||
|
|
pre-standard, and the standard landed seven months ago.
|
|||
|
|
|
|||
|
|
**Proposed amendment:** A3 currently says "delegated to `flex-auth` as PDP".
|
|||
|
|
Consider distinguishing:
|
|||
|
|
|
|||
|
|
- **A3** — externalised to a PDP (any protocol).
|
|||
|
|
- **A4** — externalised over a standard interface (AuthZEN), so the PDP is
|
|||
|
|
swappable and the PEP is not coupled to one engine's request shape.
|
|||
|
|
|
|||
|
|
That gives the ladder somewhere to go and makes the current coupling visible
|
|||
|
|
rather than invisible. It also reframes `key-cape`/Keycloak's AuthZEN support
|
|||
|
|
as a fleet opportunity rather than an unrelated upstream feature.
|
|||
|
|
|
|||
|
|
## 5. Model choice: our vocabulary is RBAC-shaped, our problem may be ReBAC-shaped
|
|||
|
|
|
|||
|
|
- ReBAC originates in Google's **Zanzibar**, modelling authorization as a graph
|
|||
|
|
of typed relationships, with authorization data stored as explicit tuples in
|
|||
|
|
a dedicated datastore
|
|||
|
|
([AuthZed — introduction to Google Zanzibar](https://authzed.com/learn/google-zanzibar)).
|
|||
|
|
- It is described as "well-suited to multi-tenant enterprise environments where
|
|||
|
|
access patterns are relational rather than role-based", and specifically for
|
|||
|
|
modelling "complex customer organizational structures while maintaining
|
|||
|
|
strict data isolation" (ibid.,
|
|||
|
|
[AuthZed — ABAC vs ReBAC](https://authzed.com/learn/abac-vs-rebac-when-to-use-which)).
|
|||
|
|
- Open implementations: SpiceDB, OpenFGA, Authzed
|
|||
|
|
([IAM Day by Day — Zanzibar & ReBAC](https://iamdaybyday.com/patterns/authorization/zanzibar/)).
|
|||
|
|
|
|||
|
|
**Against our model:** our capability roles (`PLTF`/`IAM`/`VEN`/`CUS`) are
|
|||
|
|
tenant-level RBAC, and `user-engine` owns memberships separately. The moment a
|
|||
|
|
question like "may this user act for this tenant, given they are a member of an
|
|||
|
|
organisation that administers it" arises, that is a relationship query, and
|
|||
|
|
role-flattening will start to strain. Not a present problem — flagged because
|
|||
|
|
the estate's own tenant taxonomy (`family`, `community`, `association`) is
|
|||
|
|
overtly relational and will produce these questions.
|
|||
|
|
|
|||
|
|
## 6. Finding: nothing in the framework covers tenant exit
|
|||
|
|
|
|||
|
|
OWASP lists "ensure complete data deletion on tenant offboarding" as a **must**
|
|||
|
|
([OWASP Multi-Tenant Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Multi_Tenant_Security_Cheat_Sheet.html)).
|
|||
|
|
|
|||
|
|
Our position today: `tenant-engine`'s lifecycle contract provides reversible
|
|||
|
|
retirement and states plainly that **there is no hard-delete**. That is a
|
|||
|
|
defensible design for an audit-bearing estate, but it means:
|
|||
|
|
|
|||
|
|
- No plane in ADR-008 has anything to say about tenant exit, data deletion, or
|
|||
|
|
the interaction with retention.
|
|||
|
|
- Physical backups on `platform-pg` retain 30 days, so even a hypothetical
|
|||
|
|
hard-delete leaves tenant data recoverable for a month — which is a GDPR
|
|||
|
|
erasure question, not just an architecture one.
|
|||
|
|
- `rapp-postgres` SCOPE explicitly excludes tenant identity, and
|
|||
|
|
`tenant-engine` explicitly excludes storage. Deletion falls between them.
|
|||
|
|
|
|||
|
|
**Proposed action:** this is a fifth concern the framework does not model, and
|
|||
|
|
it is not obviously a fifth *plane* — it is a lifecycle stage cutting across
|
|||
|
|
all four. Recommend adding it as an explicit open question rather than
|
|||
|
|
inventing a plane for it. The estate's `gdpr_report` tooling in the hub
|
|||
|
|
suggests someone has thought about this elsewhere; worth locating before
|
|||
|
|
drafting.
|
|||
|
|
|
|||
|
|
## 7. Finding: automated conformance testing may not be sufficient for this plane
|
|||
|
|
|
|||
|
|
This challenges the recommendation I gave in the ADR review (§18.3, "build a
|
|||
|
|
fleet conformance suite").
|
|||
|
|
|
|||
|
|
- "Finding cross-tenant vulnerabilities requires semantic understanding of what
|
|||
|
|
data belongs to which context, and that understanding requires a human tester
|
|||
|
|
who can set up separate tenant contexts and compare the responses."
|
|||
|
|
- "Across 119 SaaS assessments, the highest-impact findings were concentrated
|
|||
|
|
in access control, business logic, and tenant isolation, precisely the
|
|||
|
|
categories that require context-aware human testing rather than automated
|
|||
|
|
tool execution."
|
|||
|
|
([Bugstrix — multi-tenant SaaS security testing](https://bugstrix.com/blogs/multi-tenant-saas-security-testing-how-to-prevent-cross-tenant-data-leaks/))
|
|||
|
|
|
|||
|
|
**Against our model:** this does not invalidate the conformance suite — a
|
|||
|
|
mechanical check that every tenant-owned table carries the key (E1), or that a
|
|||
|
|
session without the GUC reads nothing (E3), is precisely the kind of structural
|
|||
|
|
assertion automation does well. What it invalidates is the hope that a suite
|
|||
|
|
*replaces* adversarial review at E2, where the boundary lives in business
|
|||
|
|
logic and the failure is semantic.
|
|||
|
|
|
|||
|
|
**Proposed amendment to §13:** split the evidence artifacts by what can be
|
|||
|
|
automated. E1 and E3 are mechanical and belong in CI. E2 is semantic and needs
|
|||
|
|
a periodic adversarial review with a recorded date, closer to a pen-test
|
|||
|
|
cadence than a test suite. Claiming E2 on the strength of a green CI run would
|
|||
|
|
be exactly the overclaim §6 of the ADR prohibits.
|
|||
|
|
|
|||
|
|
## 8. Summary against our ladder
|
|||
|
|
|
|||
|
|
| Finding | Verdict |
|
|||
|
|
|---|---|
|
|||
|
|
| Tenant boundary failure = BOLA = OWASP API1, top of the list since launch | **Reframe.** Cite by name; our largest gap is the industry's largest risk. |
|
|||
|
|
| OWASP Multi-Tenant Security Cheat Sheet exists and is unreferenced | **Adopt.** Closest external baseline to our framework. |
|
|||
|
|
| A2 matches OWASP's "establish context early, bind to session" | **Aligned.** |
|
|||
|
|
| "Skipping tenant validation for internal services" is a named anti-pattern | **Check ourselves.** Our estate is mostly internal service-to-service. |
|
|||
|
|
| PDP/PEP split is mainstream | **Aligned.** |
|
|||
|
|
| AuthZEN 1.0 Final (Jan 2026) standardises the PDP wire; Keycloak experimental May 2026 | **Gap.** `flex-auth` is pre-standard and bespoke. Propose A4. |
|
|||
|
|
| ReBAC/Zanzibar suits relational tenant structures | **Watch.** Our `family`/`community`/`association` groupings are relational. |
|
|||
|
|
| Tenant offboarding deletion is a MUST we do not model | **Finding.** No plane covers exit; interacts with 30-day backup retention. |
|
|||
|
|
| Cross-tenant testing needs human semantic review, not only automation | **Amend §13.** E1/E3 mechanical, E2 adversarial-review with a date. |
|
|||
|
|
|
|||
|
|
## Sources
|
|||
|
|
|
|||
|
|
- [OWASP — Multi-Tenant Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Multi_Tenant_Security_Cheat_Sheet.html)
|
|||
|
|
- [Wiz — OWASP API Security Top 10 risks and how to mitigate them](https://www.wiz.io/academy/api-security/owasp-api-security)
|
|||
|
|
- [Decryption Digest — BOLA and IDOR testing guide](https://www.decryptiondigest.com/blog/bola-idor-api-authorization-test-remediation)
|
|||
|
|
- [SecPortal — Broken Object Level Authorization (BOLA) guide](https://secportal.io/vulnerabilities/broken-object-level-authorization)
|
|||
|
|
- [Bugstrix — Multi-tenant SaaS security testing: preventing cross-tenant data leaks](https://bugstrix.com/blogs/multi-tenant-saas-security-testing-how-to-prevent-cross-tenant-data-leaks/)
|
|||
|
|
- [OpenID Foundation — Authorization API 1.0 Final Specification approved](https://openid.net/authorization-api-1-0-final-specification-approved/)
|
|||
|
|
- [OpenID AuthZEN — Authorization API 1.0 specification](https://openid.github.io/authzen/)
|
|||
|
|
- [OpenID Foundation — AuthZEN working group specifications](https://openid.net/wg/authzen/specifications/)
|
|||
|
|
- [Curity — An introduction to Authorization Exchange (AuthZEN)](https://curity.io/resources/learn/authzen/)
|
|||
|
|
- [DEV — AuthZEN Authorization API 1.0 deep dive](https://dev.to/kanywst/authzen-authorization-api-10-deep-dive-the-standard-api-that-separates-authorization-decisions-1m2a)
|
|||
|
|
- [Keycloak — AuthZEN as an experimental feature (May 2026)](https://www.keycloak.org/2026/05/authzen-as-experimental-feature)
|
|||
|
|
- [Auth0 — A guide to OpenID AuthZEN's Authorization API 1.0](https://auth0.com/blog/implementing-authzen-guide-openid-authorization-api/)
|
|||
|
|
- [AuthZed — An introduction to Google Zanzibar and ReBAC](https://authzed.com/learn/google-zanzibar)
|
|||
|
|
- [AuthZed — ABAC vs ReBAC: when to use which](https://authzed.com/learn/abac-vs-rebac-when-to-use-which)
|
|||
|
|
- [IAM Day by Day — Zanzibar & relationship-based access control](https://iamdaybyday.com/patterns/authorization/zanzibar/)
|
|||
|
|
- [DEV — RBAC vs ABAC vs ReBAC: how to choose](https://dev.to/kanywst/rbac-vs-abac-vs-rebac-how-to-choose-and-implement-access-control-models-3i2d)
|