Use when designing or adding tenant isolation tests to a multi-tenant app, adding a CI leak gate, or investigating a leak. Inventories tenant-bearing surfaces, then tests read-by-ID, list leaks, spoofed tenant IDs, JWT replay, storage IDOR, search, jobs and caches, plus Postgres row-level security.
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Canonical tenant-isolation policy shapes, plus the ways RLS can be bypassed that a tenant deployment must lock down.
CREATE POLICY tenant_isolation ON documents
AS PERMISSIVE
FOR ALL
TO app_user
USING (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents FORCE ROW LEVEL SECURITY;Both USING and WITH CHECK use the same expression - a tenant can't read OR
insert rows for another tenant.
CREATE POLICY tenant_isolation ON documents
AS PERMISSIVE
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY owner_or_shared ON documents
AS RESTRICTIVE
USING (
owner_id = current_setting('app.user_id')::uuid
OR id IN (SELECT document_id FROM document_shares
WHERE shared_with = current_setting('app.user_id')::uuid)
);Permissive policy enforces tenant boundary; restrictive policy layers on per-row ACL.
CREATE POLICY tenant_isolation ON documents
USING (
tenant_id = current_setting('app.tenant_id')::uuid
OR current_setting('app.is_global_admin', true)::boolean = true
);Global admin claim bypasses the tenant filter. Audit usage - admin connections must be logged and short-lived.
Three categories of bypass per Postgres docs:
| Bypass | When |
|---|---|
| Superusers | Always (cannot be disabled) |
Roles with BYPASSRLS attribute | Operational tasks; create explicit roles, audit usage |
| Table owners | Bypass by default unless FORCE ROW LEVEL SECURITY set |
For production tenant tables, the application connection role must NOT be a
superuser, NOT have BYPASSRLS, and must NOT own the table (or, if it owns the
table, FORCE ROW LEVEL SECURITY must be set).
To prevent accidentally-bypassed query patterns from masquerading as policy-respecting:
SET row_security = off;
-- Subsequent queries error if a policy would have filteredUseful in audit / backup contexts to catch unintended row exclusion.
Per Postgres docs, these operations are not subject to RLS:
TRUNCATEREFERENCES privilege checksThis creates side-channel leaks: an attacker can probe for tenant-other rows via
INSERT collisions on unique constraints, or via foreign-key reference patterns.
Compensating controls:
cross-tenant-data-leak-tests