Tenancy Posture draft-6: the first review changed the document
tenant-engine assessed itself against the ladders and came back with corrections. All five are adopted, because the ratification test says that if a repo cannot express itself the ladders are wrong - and it could not, in three places. I2 conflated authority with verification. Its text named tenant-engine as the source of existence, which made the level describing canonical identity unclaimable by the service that provides it. An axis is assessed on a service's own inbound surface, never on its authority over the concept. tenant-engine is the source of tenant records and is at I1, because the acting identity arrives in the request body rather than in a verified token. A level now reports the weakest surface. tenant-engine has PDP-authorized mutations and three unauthorized read routes - including the one flex-auth calls for aal2-class decisions - and reported A2 rather than A3. Publishing the stronger surface would be accurate about that surface and misleading about the service. A per-surface vector was considered and rejected as premature. The E ladder assumed all data is tenant-keyed. A registry whose rows ARE the tenants has no predicate to scope a policy by, and enforcing one would break the service rather than secure it. Mixed-shape services now declare an E level plus a named registry exception; an unnamed exception is an overclaim. Without this they overclaim or sit at E2 forever, which is what tenant-engine was facing. Retention and erasure are two dimensions and one level cannot carry both. tenant-engine is R1 on backup and R0 on erasure - its lifecycle contract deliberately defines no hard-delete, so a tenant record cannot be deleted ever, by design, while carrying display_name and contact_email. Declared R1/R0 now. And the compounding - personal data, no erasure path, a backup window set by the longest-retaining co-resident - is the substrate owner's to surface, because each part looks locally reasonable alone. My own error, corrected: I listed tenant-engine as a live P1 occupant in both the ladder and the E/P matrix. They are on SQLite. P1 is TEN-WP-0009's target and the provisioning is my own unapplied intake. Asserting a placement that a workplan exists to create is exactly the kind of claim this document forbids. Worth recording: they found an unfiltered cross-tenant read in their own event accessor while assessing against the ladder, before publishing anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
cb32f6d68c
commit
2744ce7d36
1 changed files with 50 additions and 8 deletions
|
|
@ -8,7 +8,7 @@ version: "0.1"
|
|||
created: "2026-08-17"
|
||||
updated: "2026-08-17"
|
||||
scope: multi-tenancy-security-framework
|
||||
revision: "draft-5"
|
||||
revision: "draft-6"
|
||||
adr:
|
||||
- docs/adr/ADR-0006-recursive-multi-tenant-identity-authorization.md
|
||||
- docs/adr/ADR-0013-tenant-onboarding-grouping-taxonomy.md
|
||||
|
|
@ -24,7 +24,7 @@ related:
|
|||
|
||||
## Status
|
||||
|
||||
**Proposed, draft-5.** Relocated from `the-custodian/canon/architecture` on
|
||||
**Proposed, draft-6.** Relocated from `the-custodian/canon/architecture` on
|
||||
2026-08-17: multi-tenancy is part of the IT-security framework NetKingdom
|
||||
provides, so this framework belongs in NetKingdom canon beside the IAM Profile
|
||||
and the tenant-engine boundary contract, not in the work-factory canon.
|
||||
|
|
@ -39,8 +39,14 @@ and the tenant-engine boundary contract, not in the work-factory canon.
|
|||
- **draft-4** closed the two gaps draft-3 left open: `R4` had no mechanism
|
||||
beyond waiting, and the noisy-neighbour evidence artifact asserted something
|
||||
shared infrastructure cannot provide.
|
||||
- **draft-5** relocates to NetKingdom and renames the dimensions from *planes*
|
||||
- **draft-5** relocated to NetKingdom and renamed the dimensions from *planes*
|
||||
to *axes*, because the word was already taken (§0).
|
||||
- **draft-6** applies the first review. `tenant-engine` assessed itself,
|
||||
corrected a guess downward on two axes, found an axis that did not fit its
|
||||
data shape, and found a real cross-tenant defect in its own code while
|
||||
reading the ladder. Five changes followed (§4.1, §4.4, §4.5, §5.2, and the E
|
||||
registry exception). **The ratification test worked: the document changed,
|
||||
not the repo.**
|
||||
|
||||
Every correction so far was found by research or by relocation, not by review.
|
||||
|
||||
|
|
@ -183,13 +189,22 @@ level is the one a service can evidence and its risk warrants.
|
|||
|---|---|
|
||||
| **I0** | No tenant concept. Data not attributable to a tenant. |
|
||||
| **I1** | A local tenant notion exists but is not canonical, **or** the tenant is taken from the request rather than from a verified token. |
|
||||
| **I2** | Canonical identifiers, bound at the identity provider and carried as a verified claim; `tenant-engine` is the source of existence. |
|
||||
| **I2** | Canonical identifiers, bound at the identity provider and carried as a verified claim, **and verified by this service on its own inbound calls**. |
|
||||
| **I3** | I2 plus capability roles honoured, with live `tenant-engine` re-query for privileged, destructive, credential-vending or `aal2`-class decisions. |
|
||||
|
||||
I1 now explicitly absorbs request-supplied tenant identifiers. "Never trust
|
||||
client-supplied tenant IDs without validation" is a named anti-pattern; a
|
||||
service reading the tenant from a header is at I1 however canonical the string.
|
||||
|
||||
**An axis is assessed on a service's own inbound surface, never on its
|
||||
authority over the concept.** `tenant-engine` is the source of existence for
|
||||
tenant records and is nonetheless at I1, because it takes the acting identity
|
||||
from the request body rather than from a verified token. Draft-5 conflated
|
||||
these by naming the authority inside the I2 definition, which made the level
|
||||
describing canonical identity unclaimable by the service that provides it.
|
||||
Corrected on tenant-engine's review — a reader would otherwise assume the
|
||||
authority must be at I2 by definition.
|
||||
|
||||
`business-app-service-contract` §2.1 sets app-local accounts as the v1 baseline
|
||||
for business apps — a sanctioned low level with recorded triggers for moving
|
||||
up. That is the pattern this framework generalises.
|
||||
|
|
@ -267,6 +282,20 @@ already established that our migration role owns the tables it creates), no
|
|||
`EXPLAIN` comparison because RLS disables functional indexes built on
|
||||
non-leakproof functions.
|
||||
|
||||
**Not all data is tenant-keyed, and the ladder must not pretend otherwise.** A
|
||||
registry whose rows *are* the tenants has no per-tenant predicate to scope a
|
||||
policy by; enforcing one would break the service's function rather than secure
|
||||
it. `tenant-engine`'s `tenants` table is the worked example — `key-cape`
|
||||
enumerates it at token issuance and `flex-auth` queries it live, both of which
|
||||
are cross-tenant reads by design.
|
||||
|
||||
A service with mixed data shapes declares **E-level plus a registry
|
||||
exception**: the level its tenant-keyed tables hold, and a named list of tables
|
||||
excluded because they are registries rather than tenant data. The exception is
|
||||
part of the claim and is reviewable; an unnamed exception is an overclaim.
|
||||
Without this, mixed-shape services either overclaim or stay at E2 permanently,
|
||||
and `tenant-engine` declined to claim E3 on precisely that reasoning.
|
||||
|
||||
**Default expectation** for a new platform service: E2 at first serve, E3
|
||||
recorded as target. Services whose cross-tenant exposure would be a reportable
|
||||
breach SHOULD target E3 or above.
|
||||
|
|
@ -276,7 +305,7 @@ breach SHOULD target E3 or above.
|
|||
| Level | Shape | Live occupants |
|
||||
|---|---|---|
|
||||
| **P0** | Shares a database with another consumer. | None sanctioned; the state absorbed repos arrive in. |
|
||||
| **P1** | Database per consumer, shared cluster. | `audit-core`, `tenant-engine` on `platform-pg` |
|
||||
| **P1** | Database per consumer, shared cluster. | `audit-core` on `platform-pg`; `tenant-engine` (target, TEN-WP-0009 — still on SQLite) |
|
||||
| **P2** | Dedicated cluster per consumer. | `user-engine-pg`, `target-revenue-pg` |
|
||||
| **P3** | Dedicated cluster per tenant. | Business apps per `business-app-service-contract` §1.2 |
|
||||
| **P4** | P3 plus separate region or jurisdiction. | None |
|
||||
|
|
@ -290,7 +319,7 @@ unreachable:
|
|||
|---|---|---|---|---|---|
|
||||
| **E4** | — | — | — | (business app) | |
|
||||
| **E3** | | (target) | | | |
|
||||
| **E2** | | tenant-engine<br>audit-core | | | |
|
||||
| **E2** | (tenant-engine) | audit-core | | | |
|
||||
| **E1** | (absorbed repo) | | | | |
|
||||
| **E0** | | | | | |
|
||||
|
||||
|
|
@ -403,13 +432,26 @@ Worked examples, best-effort and subject to owner correction:
|
|||
|
||||
| Service | Current | Notes |
|
||||
|---|---|---|
|
||||
| `tenant-engine` | `I2 A3 E2 P1 R1` | Moving to P1 under TEN-WP-0009; retention declared, horizon not yet published. |
|
||||
| `audit-core` | `I2 A3 E2 P1 R1` | Holds audit evidence, so both E3 and R2 are urgent targets. |
|
||||
| `tenant-engine` | `I1 A2 E2 P— R0/R1` | **Self-reported on review**, correcting a more generous guess. I1: acting identity comes from the request body, not a verified token. A2: three read routes unauthorized. P—: still on SQLite, P1 is TEN-WP-0009's target. R: see §4.5 on the erasure/retention split. |
|
||||
| `audit-core` | `I2 A3 E2 P1 R1` | Guessed, not yet self-reported. Holds audit evidence, so E3 is urgent. |
|
||||
| A newly absorbed repo | `I1 A1 E1 P0 R0` | Conformant **if declared**, with a recorded path. |
|
||||
|
||||
**Decision 5.1:** the posture vector is declared in the repo, not in the hub,
|
||||
consistent with local-files-are-source-of-truth.
|
||||
|
||||
**Decision 5.2 — a level reports the weakest surface, not the best one.** A
|
||||
service whose mutations are authorized by a PDP and whose read routes are
|
||||
unauthenticated is at the read routes' level, not the mutations'. Publishing
|
||||
the stronger surface would be accurate about that surface and misleading about
|
||||
the service, which §6 forbids.
|
||||
|
||||
Raised by `tenant-engine`, which found exactly this shape in itself during
|
||||
review — A3 on writes, no authorization on three read routes including the one
|
||||
`flex-auth` calls for `aal2`-class decisions — and reported A2. A per-surface
|
||||
vector was considered and rejected as premature: it multiplies the declaration
|
||||
before anyone has shown the single weakest number is insufficient. Services
|
||||
with a materially split surface should record the split in the `gap` field.
|
||||
|
||||
## 6. Conformance is accuracy, not altitude
|
||||
|
||||
> **A service is conformant when its declared posture is accurate, its target
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue