Authorizationmulti-tenanttenant isolationSaaSrow-level securitycache keys

Multi-Tenant Isolation

A tenant_id column is not isolation. Isolation is that column enforced consistently across queries, caches, queues, storage, search, logs and AI context — every place a copy of the data exists.

▶ Run the labFollow the failure

Frame the problem

Security starts with a concrete asset, attacker capability and trust crossing.

Asset
Each customer's data, and the promise that no other customer can see it.
Attacker & capability
Another paying tenant — the most motivated and best-positioned attacker a SaaS product has.
Trust boundary
The tenant boundary, which exists in every subsystem and is enforced in each one separately.
AssetThreatAttack SurfaceTrust BoundaryVulnerabilityExploit PathImpactMitigationDefense in DepthResidual Risk

Every copy is a boundary

The database is where teams enforce tenancy and the other stores are where it leaks. A cache keyed by invoice:101 serves tenant B's invoice to tenant A. A search index queried without a tenant filter returns everyone's documents. A queue consumer resolves an id without the tenant. A log line includes another tenant's payload in an error. An agent's retrieval pulls the most similar chunk from the whole corpus.

The rule: wherever data is keyed, the tenant is part of the key; wherever data is queried, the tenant is part of the predicate; wherever data is rendered, the tenant is checked against the principal.

Where tenancy must be enforced
StoreLeakEnforcement
DatabaseQuery without tenant predicateRow-level security policy or principal-scoped repository
CacheKey without tenantKey prefix t:{tenant}:…
QueueMessage resolves id without tenantTenant in message; consumer scopes by it
Object storageShared bucket, guessable keysPer-tenant prefix + signed URLs scoped to the prefix
Search / vector indexQuery without tenant filterMandatory metadata filter; per-tenant index for sensitive data
LogsPayload of another tenant in an errorRedact bodies; log ids not content
AI contextRetrieval across tenantsFilter before retrieval, not after

Row-level security as depth

Databases that support row-level security let you set the tenant once per connection and have every query filtered by policy. This is defence in depth: an application bug that forgets the predicate still cannot cross tenants. It costs setting a session variable per request and forbidding the application role from bypassing the policy. For high-value multi-tenant data it is the single most effective control available.

1ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
2CREATE POLICY tenant_isolation ON invoices
3 USING (tenant_id = current_setting('app.tenant_id')::uuid);
4-- app role cannot BYPASSRLS; per request: SET LOCAL app.tenant_id = '<from session>';
5-- A query that forgets WHERE tenant_id = ... now returns only this tenant's rows anyway.

Key points

  • A tenant column is not isolation; consistent enforcement in every store is.
  • Tenant in every cache key, queue message, storage prefix and index filter.
  • Row-level security is depth against forgotten predicates.
  • Filter AI retrieval before the query, not after.

Boundary control exercise

This lesson uses the shared boundary-control exercise.

Boundary control check
Untrusted input / identity
Trust boundary
Privileged asset
Prevention may fail silently.

Follow the attack

Safe conceptual simulation: capability → missing control → crossed boundary → asset impact.

  1. 1
    Tenant A → guess or observe an id, hit a cache/search/export path lacking the tenant filter.
  2. 2
    Receive tenant B's data.
Blast radius
  • Cross-customer disclosure: contractual, regulatory and reputational, often product-ending.

Defend, detect, recover

One prevention is a single point of security failure. Layer it and make failure observable.

Prevent
  • • RLS or scoped repositories.
  • • Tenant-prefixed keys everywhere.
  • • Cross-tenant tests per store.
  • • Per-tenant encryption keys for highest-value data.
Detect
  • • Requester tenant ≠ record tenant in access logs.
  • • Cache hit for a key lacking a tenant prefix.
Respond & recover
  • • Identify affected tenants from logs; notify; add the missing enforcement in every store, not one.
Residual risk
  • • Shared infrastructure remains shared; noisy-neighbour and side channels persist.
  • • Support tooling crosses tenants by design.