Skip to main content

From LinkedIn · · 1 min

The boundary is what all of them say at once

Nobody opens a pull request called Widen tenant access. They open three reasonable ones instead, each gets its own policy, and PostgreSQL combines permissive policies with OR.

By Olha Shevchenko. Audits production systems on AWS and Node.js.

The boundary is what all of them say at once

Nobody opens a pull request called "Widen tenant access."

They open three perfectly reasonable ones:

"Let support investigate customer tickets."

"Let the nightly export run with no tenant set."

"Let the admin screen fetch the record it needs."

Each exception gets its own policy. Each policy passes review. Nobody has done anything reckless.

Then PostgreSQL does exactly what it was told.

Row-Level Security policies are permissive by default. Policies that apply to the same role and command combine with OR. Each one is another way to pass, not another condition to meet.

The first check is which role the policy names.

When support_read applies only to a reporting role the application cannot assume, the tenant boundary stays where it was. When the same policy applies to the role the application already uses, every query against that table gets another route to a row.

There is a more awkward version. If production connects as the table owner, none of those policies are enforced for that connection unless somebody set FORCE ROW LEVEL SECURITY. The team can have a beautiful policy review process for rules the application skips entirely.

So I do not count policies. I trace roles: which policies apply to each one, who can assume them in production, and whether any of them bypass RLS.

A customer, or their lawyer, eventually asks how tenant data is kept separate. The answer will still say RLS. Every policy may still be correct on its own.

The diff shows one reasonable exception. The widening is in the set.

That is what the next owner inherits.

A tenant boundary that every reviewer signed off on, one policy at a time.