Row-level security

Last checked · 3 min read

Row-level security (RLS) is a Postgres feature that filters which rows of a table a role can read or change, using policies written as SQL expressions. Once it is enabled on a table, a row is visible or writable only if a policy allows it. It narrows an agent's reach from whole tables to the rows its task needs.

How it works

Privileges decide which tables a role can touch. Row-level security decides which rows. You enable it per table with ALTER TABLE … ENABLE ROW LEVEL SECURITY, then add policies. Each policy has an expression that must be true for a row: USING decides which existing rows a command can see, update or delete, and WITH CHECK decides which new or changed rows it may write.1

Two defaults matter:

  • No policy means no rows. Once row security is enabled on a table, access with no matching policy is denied.1
  • Policies add up with OR. Several permissive policies, which is the default kind, are combined with OR; restrictive policies are combined with AND.1 One broad permissive policy can undo a careful narrow one.

Who skips it

  • Superusers and roles with BYPASSRLS always bypass row security.1
  • The table’s owner, normally. Owners bypass row security unless the table has FORCE ROW LEVEL SECURITY set.1 An agent that connects as the role that owns the tables, as many setups do, isn’t filtered at all.
  • Whole-table operations. TRUNCATE and REFERENCES aren’t subject to row security,1 so a role allowed to truncate a table empties all of it.
  • A disabled table. Policies can exist while row security is disabled on the table, and then they’re ignored.2

The catch with agents: who the session says it is

Most multi-tenant policies compare a row with a session setting, such as current_setting('app.customer_id', true). The true makes it return NULL instead of an error when the setting doesn’t exist,3 so a session that hasn’t said who it’s working for sees nothing, as in the example’s first query.

The setting belongs to the session, though. A setting attached to a role with ALTER ROLE … SET is only that role’s default at login: in our test, a session of that role set its own app.customer_id, and the role’s row-level security policy then trusted it.4 The example’s second query is the problem: the session chose customer 7 itself. An agent that can send raw SQL can send SET app.customer_id = '8' just as easily and read another customer’s tickets. Nothing fails; the policy does what it says.

Three ways to close that:

  • Don’t give the agent raw SQL. Give it tools with fixed queries, and have the tool server set the customer from the conversation it serves, not from the model.
  • Key policies to something the session can’t change. SET ROLE changes current_user but leaves session_user alone,5 so a login role per tenant with a policy on session_user holds against raw SQL.
  • Put a rule in front of the SQL that refuses SET, set_config and multi-statement calls before they reach the database.

The safe Postgres access post runs each of these on PostgreSQL 18.6 with the output.

An example

PostgreSQL 18.6: one customer's tickets, and who chooses the customertext
-- as the table owner
ALTER TABLE public.tickets ENABLE ROW LEVEL SECURITY;
CREATE POLICY agent_one_customer ON public.tickets
  FOR ALL TO support_agent
  USING (customer_id = current_setting('app.customer_id', true)::int)
  WITH CHECK (customer_id = current_setting('app.customer_id', true)::int);

-- as support_agent
shop=> SELECT count(*) FROM public.tickets;
 0
shop=> SET app.customer_id = '7';
shop=> SELECT count(*) FROM public.tickets;
 2

How Deedglass uses it

deedglass scan flags a database role that bypasses row-level security as a high finding, and its grade takes 35 points off for a superuser or BYPASSRLS role, once.6 Deedglass's Postgres connector runs agent SQL as a least-privilege role in a transaction Deedglass controls, one statement per action,7 and refuses SET, RESET and set_config before they reach the database, so the agent can't name a different customer.

Related on this site

Sources

  1. 1PostgreSQL 18 documentation: Row security policies · postgresql.org · checked 18 October 2026
  2. 2PostgreSQL 18 documentation: ALTER TABLE · postgresql.org · checked 18 October 2026
  3. 3PostgreSQL 18 documentation: System administration functions · postgresql.org · checked 14 October 2026
  4. 4PostgreSQL 18 documentation: ALTER ROLE · postgresql.org · checked 14 October 2026
  5. 5PostgreSQL 18 documentation: SET ROLE · postgresql.org · checked 14 October 2026
  6. 6Deedglass docs: Scan in 60 seconds · docs.deedglass.com · checked 18 October 2026
  7. 7Deedglass docs: The Postgres connector · docs.deedglass.com · checked 18 October 2026
  8. 8Deedglass source: the Postgres connector's statement refusals · github.com · checked 18 October 2026

Last checked 18 October 2026. We re-check the glossary every month. If something here is wrong, write to hello@deedglass.com.