securitypostgres

Postgres RLS: Why 'Enabled, No Policy' is Deny-All for Backend Tables

· 5 min read

We at MoWave One are always refining our backend architecture for the Lima app. A recent security audit highlighted an important configuration detail in our Postgres database running on Supabase: the correct way to secure backend-only tables using Row Level Security (RLS). This led us to a clearer understanding that RLS-on with no policies is not a misconfiguration, but rather a powerful “deny-by-default” strategy for data managed exclusively by our Spring backend.

The Problem: RLS Disabled in Public Schema

Our Supabase security advisor recently flagged five tables in our public schema as rls_disabled_in_public. These tables were: ai_conversation_meta, note_attachments, finance_goal_adjustments, finance_recurring_open_series, and community_post_images. At first glance, this might seem like a straightforward security vulnerability. The underlying concern is that the anon key is embedded within our mobile application. Supabase’s PostgREST service, which exposes our database API, by default grants access to any table in the public schema that has RLS disabled, allowing the anon key, and by extension, any unauthenticated client, to query or manipulate these tables. This is a significant exposure risk, as these particular tables hold sensitive user data and operational metadata that should never be directly accessible from the client side.

The Technical Cause: Backend-Only Tables Exposed

The core of the issue wasn’t that we had sensitive data; it was that these five tables were never intended for direct client access. They are 100 percent backend-managed. Our Spring backend service is solely responsible for all read and write operations on them. The anon role, which represents unauthenticated users, and even the authenticated role, for logged-in users who haven’t been granted specific table access, should have absolutely no privileges on these tables. When RLS is disabled on a table, Postgres effectively ignores any row-level permissions for all roles, making the table entirely open to anyone with schema-level SELECT, INSERT, UPDATE, or DELETE privileges. For tables in the public schema, these schema-level privileges are often broadly granted to roles like anon and authenticated by default in a Supabase setup, creating the exact exposure the security advisor was highlighting.

The Fix: RLS Enabled, No Policies Attached

Our solution was to enable RLS on all five flagged tables, but critically, we attached no policies to them. This might sound counterintuitive to someone new to RLS, as the common usage involves defining policies to grant specific access. However, for backend-only tables, RLS on combined with no policies equates to a deny-all rule for any role that doesn’t have BYPASSRLS privileges.

Here’s how we applied the fix:

ALTER TABLE public.ai_conversation_meta ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.note_attachments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.finance_goal_adjustments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.finance_recurring_open_series ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.community_post_images ENABLE ROW LEVEL SECURITY;

This simple ALTER TABLE statement was run for each of the five tables. After enabling RLS, without any CREATE POLICY statements, the anon and authenticated roles were immediately denied any access to these tables. This effectively closed the potential security gap identified by the Supabase advisor.

Now, let’s consider the read and write paths for our backend. The Lima app’s Spring backend connects to Postgres as the postgres role. This connection is established through the Supavisor pooler, which manages our database connections efficiently. The postgres role, being a superuser role, automatically has BYPASSRLS privileges. This means that even with RLS enabled and no policies, our backend could continue to read from and write to these tables exactly as it did before, without any disruption. The BYPASSRLS attribute ensures that the postgres role is exempt from RLS checks, preserving our backend’s functionality while securing the tables from external, unauthorized access.

This change also brought these five tables into alignment with our existing standard for backend-only tables. We already had roughly 40 other tables configured with RLS-on-with-no-policy, a pattern we use for all data that should exclusively be managed by our services. The implementation was straightforward; we applied it directly in production through the Supabase Management Control Panel (MCP) and captured it as migration V103 in our version control system. We monitored the system closely, and the healthcheck for our services remained UP throughout and after the change, confirming that our backend operations were unaffected.

Takeaways: RLS-on-no-policy is Secure by Default

This experience reinforced a critical concept: RLS-on-no-policy is not a misconfiguration, but a perfectly valid and secure configuration for tables that should be exclusively managed by a trusted backend. It establishes a deny-by-default security posture for all roles that do not possess BYPASSRLS privileges. The Supabase security advisor’s note about “enabled, no policy” should be interpreted as informational, not as an indicator of a bug or an insecure state, particularly when dealing with backend-only tables. It simply tells you that RLS is active, and without explicit policies, access is restricted.

For MoWave One and the Lima app, this approach simplifies our security model. It gives us confidence that sensitive data, even if residing in the public schema, is protected from unintended client access while our backend maintains full operational control. This is an important lesson in building robust and secure applications, like the one you can experience at https://getlima.app. Always consider the roles and their privileges when configuring RLS, and remember that sometimes, the most secure policy is no policy at all.