Multi-Tenancy & Data Isolation
In a multi-tenant system, the single most important rule is that one tenant can never see or touch another's data. There is no acceptable rate of cross-tenant leakage. A single case is a reportable breach. Make the tenant boundary something the system enforces by design, on every path, not something each query must remember to add.
Multi-tenancy is efficient because tenants share infrastructure, code, and often tables. It is dangerous for the same reason: the only thing separating customers is correct tenant scoping, applied without exception. The goal is to make isolation the default and a leak the hard thing to write, rather than relying on every developer to add a tenant filter every time.
The tenant identity must always come from the validated security context, never from the request, and it must reach every read, write, and delete of tenant-owned data. The Finperiti shared-HS256-secret finding is a multi-tenancy risk in disguise: a single shared signing key undermines the very identity that tenant isolation depends on.
Isolate by construction
- AlwaysDerive the tenant from the validated security context (token claims) on every request, and carry it as ambient, trusted context. Never take it from client input.
- DoApply the tenant predicate to every query, command, and delete on tenant-owned data, and make it structurally hard to omit (a base repository, a query filter, a guarded data-access layer).
- DoCarry and index the tenant key on every tenant-owned table, so scoping is enforceable and efficient (see Database Design).
- ConsiderA central enforcement point — a query interceptor or repository base — that adds the tenant filter automatically, so a forgotten WHERE clause cannot leak.
- ConsiderStronger isolation (separate schemas, databases, or keys per tenant) for the most sensitive data, to limit the damage of any single mistake.
- NeverQuery, update, or delete tenant-owned data without enforcing the tenant predicate server-side. No cross-tenant access, ever.
Guard the whole surface
- DoScope everything that is tenant-derived, not just the database: caches, file and blob paths, search indexes, message queues, and exports all need the tenant in their key.
- DoIsolate per-tenant secrets and keys where possible, so compromising one tenant's material does not expose others.
- DoTest isolation explicitly. Write automated tests that try cross-tenant access and assert it fails, so a regression becomes a red build.
- ConsiderLogging the tenant on every operation, and alerting if an operation ever runs without one, to catch scoping gaps early.
- Do notCache or memoise tenant data under a key that leaves out the tenant. This is a classic source of one customer seeing another's data.
- NeverUse a single shared signing key or trust boundary that lets one tenant's identity be valid for another's data.
var key = $"customer:{customerId}";
return cache.GetOrAdd(key, () => Load(customerId)); // no tenant in key or query
Two tenants with colliding ids, or a shared id space, get each other's data straight from the cache, bypassing any database scoping entirely.
var t = ctx.TenantId; // from validated context
var key = $"{t}:customer:{customerId}";
return cache.GetOrAdd(key, () =>
db.QuerySingleOrDefault("... WHERE Id=@id AND TenantId=@t", new { id = customerId, t }));
The tenant is part of both the cache key and the query predicate, so isolation holds end to end.
Self-review checklist
- AskDoes the tenant for this operation come from the validated context, never from the request?
- AskIs the tenant predicate applied to every read, write, and delete here — and enforced somewhere it cannot be forgotten?
- AskAre caches, files, indexes, and queues scoped by tenant too, not just the database?
- AskIs there a test that proves cross-tenant access fails?