the-custodian/research/2026-08-17-adr008-multi-tenancy-research-index.md
codex 04f888399f research: external survey of multi-tenancy practice, one digest per ADR-008 plane
Four search-and-read passes against AWS Well-Architected SaaS Lens, Azure
Architecture Center, OWASP, the OpenID Foundation, and PostgreSQL practitioner
literature. Each digest holds one of our ladders against outside practice and
lists its sources.

Headline: the draft-1 -> draft-2 reframe is corroborated. Microsoft states
"isolation is a spectrum, not a discrete property" and derives our E/P coupling
independently. The shape is sound; the details are not.

Five corrections draft-2 needs:

- E3 overclaims. Any session can re-SET the GUC, so RLS is strong against a
  forgotten predicate and useless against SQL injection or a compromised
  process. The ADR says "the application cannot trivially route around" - the
  exact overclaim it prohibits elsewhere.
- The pooling claim in section 16 is backwards. Transaction-scoped SET LOCAL is
  what makes E3 safe under pooling; statement-level pooling is what serves
  other tenants' rows, only under production concurrency.
- E3 layers on E2 rather than superseding it.
- Placement cannot be expressed per tenant, but the commercial section requires
  exactly that - a tier bought by some tenants puts a service at two P levels
  at once, which the posture vector cannot represent.
- FORCE ROW LEVEL SECURITY is mandatory: ADR-0001 already established that our
  migration role owns the tables it creates, so it would silently bypass every
  policy.

Also: pgTAP and pgrls make the E3 evidence artifact far cheaper than assumed;
OWASP publishes a Multi-Tenant Security Cheat Sheet we reference nowhere; our
tenant-boundary gap is BOLA, OWASP API1, top of that list since it launched;
AuthZEN 1.0 went Final in January and flex-auth is pre-standard; and no plane
covers tenant offboarding, which OWASP makes a MUST.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:00:02 +02:00

122 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.