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.

Written and maintained by · source on GitHub