Skip to main content

Security model

fastapi-rls is a security library, so its threat model is explicit. Read this page before you rely on it in production.

The non-superuser requirement (read this twice)

PostgreSQL superusers and roles with BYPASSRLS ignore Row Level Security entirely. If your application connects as such a role, every policy silently becomes a no-op and you leak everything — while all your tests that use the same role pass.

-- Your application role must be an ordinary role:
CREATE ROLE app_user LOGIN PASSWORD '…';
-- NOT a superuser, and NOT created with BYPASSRLS.
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO app_user;

The fastapi-rls integration test suite deliberately connects as a dedicated non-superuser role for exactly this reason — a superuser connection would make cross-tenant leak tests pass even if RLS were broken.

FORCE ROW LEVEL SECURITY is on by default

By default, RLS does not apply to the table's owner. fastapi-rls issues ALTER TABLE … FORCE ROW LEVEL SECURITY so the owner is constrained too. This matters because migrations and app connections often run as the owner. You can opt out per-operation (force=False), but the safe default is forced.

The injection trust boundary

SQL identifiers (table, column, role, operation names) are positions PostgreSQL cannot parameterize — they go into the DDL as text. fastapi-rls treats a single module (fastapi_rls.sql) as the trust boundary:

  • Table/column/policy/key identifiers are validated against a strict allowlist regex and quoted.
  • Roles must be PUBLIC or a comma-separated list of valid identifiers.
  • Operations must be one of ALL/SELECT/INSERT/UPDATE/DELETE.
  • CustomPolicy expressions are screened for statement terminators, comments, and DML/DDL keywords.

Validation happens both at policy construction and again in the DDL layer — the last line of defense before a string reaches the database.

CustomPolicy is screened, not sandboxed

The screen blocks obvious payloads; it is not a SQL parser. If you interpolate a value into a CustomPolicy expression, that is on you — construct it safely, or use TenantPolicy/UserPolicy, which never interpolate free text.

What identity values are trusted

fastapi-rls propagates the identity you resolve; it does not authenticate. The security of the boundary is only as good as your identity callable. If that function derives tenant_id from an unverified request header, an attacker sets their own tenant. Resolve identity from a verified source (a validated JWT claim, a server-side session), the same way you would for any authorization decision.

Absent context fails closed

Because policies read NULLIF(current_setting('rls.tenant_id', true), ''), a request with no identity set produces a NULL comparison — the predicate is false and no rows are returned. Missing context can never accidentally mean "see everything". Set require_context=True to turn a missing context into a loud RLSContextRequiredError instead of a silent empty result.

Threats fastapi-rls does not address

  • Authentication. You own it.
  • Authorization beyond row visibility. Endpoint-level access control is yours.
  • A compromised or misconfigured superuser connection. RLS cannot constrain a role that bypasses RLS. Guard your credentials and role grants.
  • Application bugs that leak data before it reaches the database (e.g. caching another tenant's response). RLS protects the query, not your cache.

Production checklist

See the dedicated production checklist for the full list, but the essentials:

  1. App connects as a non-superuser, non-BYPASSRLS role.
  2. RLS is enabled and forced on every protected table (fastapi-rls audit).
  3. identity derives context from a verified principal.
  4. Consider require_context=True so a missing context fails loud.