Row policies are enabled on a table, yet the application still reads every tenant's rows in production. What ownership or privilege explanations would you check, and how do you close each one?
answer
- Owner is exempt unless RLS is FORCED
- Superuser / BYPASSRLS attribute, possibly inherited
- Permissive policies OR — a stray USING (true) reopens everything
- Definer-rights views and functions run as their owner
- Test as the real app role, count foreign-tenant rows
basics
~20 sUsual causes: the app connects as the table owner (owners bypass policies unless RLS is forced), as a superuser, or as a role flagged to bypass RLS; an extra permissive policy OR-ed in a wide predicate; or reads go through a view or function that runs with the definer's privileges.
solid answer
~60 sI would check, in order: 1. **Who is the app connecting as?** If it is the table owner, policies do not apply — owners are exempt unless the table is set to *force* row-level security. Migration roles and "one role for everything" setups hit this constantly. 2. **Is the role a superuser, or does it carry a bypass-RLS attribute?** Both skip policies entirely. Easy to inherit from a role grant nobody remembers. 3. **Is there a second permissive policy?** Permissive policies OR together, so a leftover `USING (true)` policy for the same role and command re-opens the table no matter how tight the other one is. 4. **Is access going through a view or a function?** A view that runs with its definer's rights evaluates the underlying table as the *view owner*, so the caller's policies never apply. Same for a security-definer function. Fixes: run the app as a dedicated non-owning, non-bypass role; force RLS on the table so even the owner is filtered; drop or narrow the stray permissive policy; make views run with invoker rights.
go deeper
Know that owners and superusers bypass policies, so isolation must be tested using the application's own role.
List the main exemptions — owner, superuser, bypass attribute — and explain enable versus force.
Run the whole triage: effective role attributes, all policies on the table including permissive OR-ing, view and function rights, and a behavioural test as the production role in CI.
Make the exemption surface an explicit design artifact: separate DDL and DML roles, restrictive policies for mandatory conditions, deliberate bypass identities for backup and ETL, and continuous verification rather than schema review.
## Why "enabled" is not the same as "enforced" Row-level security has a surprising number of legitimate exemptions, and every one of them is a way to ship a table that looks protected in the schema and is wide open at runtime. This is one of the most common real interview scenarios because it is one of the most common production incidents. ## 1. The table owner By default the owner of a table is exempt from its own policies. The reasoning is ownership: the owner defines the policies and must be able to inspect, back up and repair the data. The consequence is brutal in the common deployment where the application connects with the same role that ran the migrations and therefore owns every table — policies exist, tests written as that role pass, and nothing is filtered. Two fixes, both worth doing: (a) split roles, so migrations run as an owner/DDL role and the application runs as a separate role with only DML privileges; (b) *force* row-level security on the table, an explicit flag that makes even the owner subject to its policies. Forcing is also what makes it safe to write policies you intend as absolute. ## 2. Superuser and bypass attributes Superusers skip all policies. So does any role carrying an explicit bypass-RLS attribute, which exists precisely so backup and replication tooling can read everything. Both are inheritable through role membership, so a role that looks ordinary can be exempt because it is a member of an admin group. Check the *effective* attributes of the connecting role, not just the row in the role catalog for its own name. Give backup/ETL identities the bypass attribute deliberately and nothing else. ## 3. A permissive policy you forgot Policies default to **permissive** and combine with OR. Adding a policy therefore never narrows access; it widens it. A very common failure: someone adds `CREATE POLICY tmp_debug ... USING (true)` to unblock an investigation, or a broad "admin can see everything" policy is defined `TO PUBLIC` instead of `TO admin_role`. From then on, the strict tenant policy is irrelevant because every row satisfies the other branch. The defence is to enumerate every policy on the table during review — not just the one you wrote — and to express mandatory conditions as **restrictive** policies, which AND in and therefore cannot be widened away by a future permissive addition. ## 4. Views and functions with definer's rights A view traditionally executes against its base tables with the privileges and identity of the *view owner*, not the caller. If a reporting view sits over a protected table and the view is owned by a privileged role, callers of the view get unfiltered rows. Modern engines offer an invoker-rights option for views; turn it on for anything sitting above an RLS-protected table, or make the view owner a role that is itself subject to the policies. The same applies to security-definer stored functions: inside them, the effective identity is the definer. That is sometimes exactly what you want (a validated context setter), but a generic "fetch report" definer function silently punches through your policies. ## 5. Related near-misses - **RLS never enabled on this table.** Policies can be defined while the table-level switch is off; they simply do nothing. Check the table's RLS flag, not just the existence of policies. - **Only some commands covered.** A policy `FOR SELECT` leaves UPDATE and DELETE governed by nothing — and with RLS enabled, the fail-closed default actually blocks them, which shows up as broken writes rather than leaks. The mirror image is a `FOR ALL` policy with an over-broad predicate. - **Partitions and inheritance.** Whether policies on a parent apply to partitions, and whether a partition carries its own, is an engine-specific detail worth verifying by test. ## How to verify rather than assume The only trustworthy check is behavioural: connect as the exact production application role, set a tenant context, and count rows for a foreign tenant. Assert zero. Do that in CI, on every deploy, with a role created the same way production's is. Reading the schema and concluding "policies exist, we are fine" is how every one of the causes above survives review.
- What is the difference between enabling and forcing row-level security on a table?Enabling turns policies on for everyone except the table owner (and other exempt roles). Forcing additionally subjects the owner to the table's own policies. Forcing matters whenever the application, or any routine job, connects as the owner, and it is the right default for tables where isolation is a security boundary rather than a convenience.
- You need an admin role that can read every tenant. What is the safest way to express that?Prefer a separate role granted the bypass attribute deliberately, or a policy scoped explicitly TO that admin role, rather than a broad policy on PUBLIC. Keeping the exemption in the role rather than in a permissive policy means it cannot accidentally widen access for the application role, and it is visible in a role audit instead of hidden among the table's policies.
saying these in an interview costs you the question
- Assuming policies apply to the table owner by default
- Reviewing only the policy you wrote instead of all policies on the table
- Adding a permissive policy expecting it to narrow access
- Forgetting that views and security-definer functions run as their owner
- Verifying isolation while connected as an admin or owner role