123 lines
9.1 KiB
Markdown
123 lines
9.1 KiB
Markdown
|
|
# ADR-008 multi-tenancy — external research index
|
|||
|
|
|
|||
|
|
> Research artefact — 2026-08-17. Four deep-research digests, one per plane of
|
|||
|
|
> `canon/architecture/adr-008-multi-tenancy-model.md` (draft-2), surveying prior
|
|||
|
|
> knowledge and best practice and holding our ladders against it.
|
|||
|
|
>
|
|||
|
|
> - `2026-08-17-adr008-plane-i-identity.md`
|
|||
|
|
> - `2026-08-17-adr008-plane-a-authorization.md`
|
|||
|
|
> - `2026-08-17-adr008-plane-e-enforcement.md`
|
|||
|
|
> - `2026-08-17-adr008-plane-p-placement.md`
|
|||
|
|
>
|
|||
|
|
> Purpose: feed draft-3. Nothing here amends the ADR by itself.
|
|||
|
|
|
|||
|
|
## 1. Headline: the reframe is externally corroborated
|
|||
|
|
|
|||
|
|
The operator's draft-1 → draft-2 instruction — stop specifying one model, build
|
|||
|
|
a framework of graduated levels — matches the most rigorous public guidance
|
|||
|
|
almost verbatim:
|
|||
|
|
|
|||
|
|
> "Instead of viewing isolation as a discrete property, consider it a spectrum.
|
|||
|
|
> You can deploy components of your architecture that are more isolated or less
|
|||
|
|
> isolated than other components in the same architecture."
|
|||
|
|
> — [Azure Architecture Center, Tenancy models](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/considerations/tenancy-models)
|
|||
|
|
|
|||
|
|
Microsoft also derives our E↔P coupling independently: shared deployment ⇒
|
|||
|
|
enforcement lives in application code; dedicated deployment ⇒ enforcement is
|
|||
|
|
structural. Draft-2's shape is sound. What follows is about its details.
|
|||
|
|
|
|||
|
|
## 2. Corrections to draft-2 — things it currently states wrongly
|
|||
|
|
|
|||
|
|
Ranked by how badly they mislead.
|
|||
|
|
|
|||
|
|
| # | Correction | Plane | Where |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| C1 | **E3 does not prevent an application from routing around it.** Any session can re-`SET` the GUC; SQL injection or a compromised process defeats it entirely. E3 is strong against *accident*, weak against *compromise*. The ADR's phrase "the application cannot trivially route around" overclaims — the exact fault §6 prohibits. | E | §4.3 |
|
|||
|
|
| C2 | **The pooling claim in §16 is backwards.** Transaction-scoped context (`SET LOCAL`) is what makes E3 *safe* under pooling; **statement**-level pooling is what breaks it — returning other tenants' rows, only under production concurrency. The ADR says E3 "forecloses aggressive transaction-level pooling". | E | §16 |
|
|||
|
|
| C3 | **"Set at pool checkout" is the wrong scope.** Should be set transaction-locally inside an explicit transaction. | E | §4.3 |
|
|||
|
|
| C4 | **E3 does not supersede E2 — it layers on it.** Practice treats them as coexisting; a service dropping E2 on reaching E3 is worse off, because E3 fails open under injection. Claiming E3 should require the E2 artifact too. | E | §4.3, §13 |
|
|||
|
|
| C5 | **P cannot express per-tenant placement, which §11 requires.** A tier requiring `E3 P2` bought by only some tenants puts the service at two P levels at once. The posture vector cannot represent it, forcing an over- or under-claim. | P | §5, §11 |
|
|||
|
|
|
|||
|
|
## 3. Gaps — real concerns no plane covers
|
|||
|
|
|
|||
|
|
| # | Gap | Suggested home |
|
|||
|
|
|---|---|---|
|
|||
|
|
| G1 | **Tenant offboarding and deletion.** OWASP lists complete data deletion on offboarding as a MUST. `tenant-engine` has reversible retirement and explicitly no hard-delete; `platform-pg` retains backups 30 days. Falls between two repos' declared scopes. | New open question; a lifecycle stage across all four planes, not a fifth plane. |
|
|||
|
|
| G2 | **Non-database stores.** Caches, search indices and background jobs are named leak surfaces. Our ladders are datastore-shaped throughout. | Scope note on the E ladder, or widened evidence artifacts. |
|
|||
|
|
| G3 | **Identity-provider placement.** Realm-per-tenant vs Keycloak Organizations is the same silo/pool decision, live in our estate, undecided. Realm-per-tenant has a stated ceiling of 5–20 tenants. | Note in §3 that P scopes to data substrate; identity placement is parallel and unowned. |
|
|||
|
|
| G4 | **Noisy-neighbour evidence.** Azure requires testing both that data does not leak *and* that noisy-neighbour outcomes are acceptable. §13 covers only leakage. | Add a P1/P2 evidence artifact. |
|
|||
|
|
| G5 | **Tenant→deployment mapping record.** A first-class component in the reference guidance. Our placement facts in §1 were assembled by inspection. | Follows from G3/C5. |
|
|||
|
|
| G6 | **Cell sizing.** `platform-pg` is an uncapped cell with a computed ~6-consumer ceiling and no overflow target. | Reframes §18.8: ask what its declared maximum size is. |
|
|||
|
|
| G7 | **Tenant identifier encodes mutable headcount bands.** `tenant:<grouping>:<name>` embeds `small`/`medium`/`large`, which change. Contradicts identifier-design consensus. | Not ADR-008's; raise to `tenant-engine` / NetKingdom canon re ADR-0013. |
|
|||
|
|
| G8 | **Logging.** OWASP MUST: include tenant context in all log entries. Unmodelled. | Minor; fold into evidence artifacts. |
|
|||
|
|
|
|||
|
|
## 4. Opportunities — cheaper or better than we assumed
|
|||
|
|
|
|||
|
|
| # | Finding | Effect |
|
|||
|
|
|---|---|---|
|
|||
|
|
| O1 | **pgTAP + pgrls** (67 RLS lint rules, semantic policy-diff for CI gating, pytest isolation plugin) exist off the shelf. `rapp-postgres` already runs a 15-probe harness of the same shape. | The E3 half of open question §18.3 is far cheaper than assumed. Splits the question into a tractable mechanical half and a human half. |
|
|||
|
|
| O2 | **OWASP Multi-Tenant Security Cheat Sheet** is the closest external baseline to our framework and is referenced nowhere in the estate. | Adopt as the named external baseline in ADR §15. |
|
|||
|
|
| O3 | **OpenID AuthZEN Authorization API 1.0** reached Final Specification January 2026; Keycloak shipped experimental support May 2026. Demonstrated PDP interoperability across 5+ engines. | `flex-auth`'s bespoke `CheckRequest` is pre-standard. Propose an `A4` rung for standard-interface PDP delegation. |
|
|||
|
|
| O4 | **Token versioning** as a middle rung between cached claim and live re-query. | Cheap global invalidation without a hot-path lookup. |
|
|||
|
|
| O5 | **BOLA / OWASP API1.** Our tenant-boundary gap is the industry's #1 API vulnerability, top of the list since it launched. | Cite by name. Converts an internal judgement into an external consensus for reviewers with a security background. |
|
|||
|
|
|
|||
|
|
## 5. Specific technical risks for the E3 implementation contract (§18.6)
|
|||
|
|
|
|||
|
|
Concrete requirements the contract must carry, each sourced in the enforcement
|
|||
|
|
digest:
|
|||
|
|
|
|||
|
|
- `FORCE ROW LEVEL SECURITY` on every tenant-owned table. Without it the table
|
|||
|
|
owner bypasses policies **silently** — and ADR-0001 already established that
|
|||
|
|
objects created by `<consumer>_migrate` are owned by `_migrate`, so our
|
|||
|
|
migration role would bypass every policy on tables it created.
|
|||
|
|
- No `BYPASSRLS` attribute on any leased role.
|
|||
|
|
- `SECURITY INVOKER` for ordinary logic; `SECURITY DEFINER` reserved for
|
|||
|
|
deliberate, reviewed cross-tenant access.
|
|||
|
|
- Context set with `SET LOCAL` inside an explicit transaction, never
|
|||
|
|
session-scoped.
|
|||
|
|
- An `EXPLAIN` comparison before and after enabling RLS: functional indexes
|
|||
|
|
using non-leakproof functions (`lower`, `enum_eq`) stop being used under RLS.
|
|||
|
|
Relevant to `tenant-engine`, which sits on the synchronous authorization path
|
|||
|
|
and just requested a 5s statement timeout.
|
|||
|
|
|
|||
|
|
## 6. Recommended shape of draft-3
|
|||
|
|
|
|||
|
|
1. Apply corrections C1–C4 to §4.3, §13 and §16. These are the ones a reviewer
|
|||
|
|
will otherwise catch, and C1 is the ADR overclaiming in exactly the way it
|
|||
|
|
forbids.
|
|||
|
|
2. Resolve C5 by making placement expressible per tenant class — a default plus
|
|||
|
|
exceptions in the posture vector — or by declaring per-tenant placement out
|
|||
|
|
of scope and accepting that isolation tiers become unmodellable.
|
|||
|
|
3. Split open question §18.3 into a mechanical half (E1/E3, tooling exists) and
|
|||
|
|
a semantic half (E2, adversarial review on a recorded cadence). Cross-tenant
|
|||
|
|
findings "require a human tester who can set up separate tenant contexts and
|
|||
|
|
compare the responses" — a green CI run is not E2 evidence.
|
|||
|
|
4. Add G1 (offboarding/deletion) and G3 (identity placement) as open questions;
|
|||
|
|
add G2 as a scope note.
|
|||
|
|
5. Cite the external baselines — OWASP Multi-Tenant Security Cheat Sheet, Azure
|
|||
|
|
tenancy models, AWS SaaS Lens — in §15, and BOLA/API1 in §13.2.
|
|||
|
|
6. Reframe §18.8 from "evaluate cells" to "declare `platform-pg`'s maximum
|
|||
|
|
size and its overflow target".
|
|||
|
|
7. Route G7 to `tenant-engine` separately. It is a critique of ratified canon,
|
|||
|
|
not of this ADR, and folding it in would overreach.
|
|||
|
|
|
|||
|
|
## 7. Method and limits
|
|||
|
|
|
|||
|
|
Four search-and-read passes, one per plane, against vendor architecture
|
|||
|
|
guidance (AWS Well-Architected SaaS Lens, Azure Architecture Center), standards
|
|||
|
|
bodies (OpenID Foundation), security baselines (OWASP), PostgreSQL practitioner
|
|||
|
|
literature, and IdP vendor documentation. Full source lists are in each digest.
|
|||
|
|
|
|||
|
|
Limits worth stating:
|
|||
|
|
|
|||
|
|
- No primary academic sources beyond the Zanzibar lineage; this is an
|
|||
|
|
engineering-practice survey, not a literature review.
|
|||
|
|
- Vendor guidance is not neutral — AWS and Azure both describe patterns their
|
|||
|
|
platforms sell. The Azure tenancy-models page is the most rigorous taxonomy
|
|||
|
|
found and is used heavily in the P digest; readers should discount its
|
|||
|
|
Azure-service-specific recommendations accordingly.
|
|||
|
|
- Nothing here was tested against our own systems. Every claim about our estate
|
|||
|
|
is drawn from repo documents, not from probing running infrastructure.
|
|||
|
|
- The E digest's corrections are the highest-confidence findings, being
|
|||
|
|
PostgreSQL mechanics rather than architectural opinion.
|