Blog

Row-Level Security Mistakes That Fail a SOC 2 Audit

2.2k 90 40 Drafted with AI, published by KollGuard

RLS is an access control, so auditors care

If you run multi-tenant Postgres or Supabase, row-level security is often your primary technical enforcement of logical access — a core SOC 2 requirement under the security and confidentiality criteria. When RLS is wrong, tenant isolation is wrong, and that is precisely the kind of finding that turns into an audit exception or, worse, a real breach. Here are the mistakes that bite teams most often.

1. RLS enabled but no policy (or vice versa)

Two halves have to line up. ALTER TABLE ... ENABLE ROW LEVEL SECURITY turns the gate on, but with no policy the default is deny-all — which sometimes surprises people when data "disappears." The more dangerous inverse: you write policies but forget to enable RLS, so the policies are inert and every row is readable. Always confirm both: RLS is enabled and the intended policies exist. FORCE ROW LEVEL SECURITY closes a further gap by applying policies even to the table owner.

2. Forgetting RLS on new tables

This is the single most common drift. You harden every table in your initial migration, then three months later a teammate ships a feature_flags or attachments table and never enables RLS on it. In Postgres, RLS is off by default for new tables. There is no global "secure by default" switch. Every new table that holds tenant data is a fresh opportunity to leak everything. This is exactly why a recurring automated check beats a one-time review — new tables appear constantly.

3. Overly broad USING clauses

A USING (true) policy technically "has a policy" while enforcing nothing. Subtler failures look reasonable at a glance:

-- Looks scoped, actually isn't if org_id can be null or spoofed
CREATE POLICY tenant_read ON documents
  FOR SELECT USING (org_id = current_setting('app.org_id')::uuid);

If app.org_id is set from an untrusted client header, an attacker sets their own org. The USING clause must derive tenant identity from something the user cannot forge — typically the authenticated JWT claim (auth.uid() / auth.jwt() in Supabase), joined against a server-controlled membership table. Also remember: USING governs which rows are visible, while WITH CHECK governs which rows can be written. Omit WITH CHECK on an INSERT/UPDATE policy and a user may be able to write rows into another tenant.

4. The service-role bypass

Supabase's service_role key (and any Postgres superuser or BYPASSRLS role) ignores RLS entirely, by design. That is fine for trusted server-side jobs — and catastrophic if it ever reaches the browser or a public edge function without its own authorization layer. Two rules: never ship the service-role key to a client, and treat every code path that uses it as fully privileged, requiring its own explicit access checks. Auditors will ask where that key lives and who can invoke it.

5. Policies that don't match your access model

RLS can be correct and still wrong for compliance if it doesn't reflect least privilege. A single "members of the org can do everything" policy fails to distinguish a read-only analyst from an admin. SOC 2's access-control criteria expect authorization aligned to role. If your app has roles, your policies (or the tables they join) should too.

How to keep it audit-clean

  • Enumerate every table and assert RLS is enabled on anything holding tenant or sensitive data.
  • Verify each such table has at least one policy, and that no policy is USING (true) unless intentional and documented.
  • Confirm write policies carry a WITH CHECK.
  • Trace tenant identity to an unforgeable source, never a client-supplied value.
  • Inventory every use of the service role and its authorization wrapper.

Run these as a repeating check, not a launch-day audit. New tables and new policies land every sprint, and RLS drift is invisible until someone — an auditor or an attacker — finds it. KollGuard automates exactly this class of Postgres and Supabase access-control checks and maps each finding to the relevant SOC 2 criteria.

Share

Get new posts by email

SOC 2, HIPAA, post-quantum readiness, and the engineering behind continuous compliance. No spam, unsubscribe anytime.

Comments

Leave a comment

Commenting as