StormaticsStormatics

Your app does not need PostgreSQL superuser

A PostgreSQL superuser can read any file on the server, write to it, and run shell commands on the host. That is more than just a powerful database account. It is shell access.

This is also why the role your application logs in with should almost never be a superuser. Most tools ask for it anyway, because it is the easy way to make setup “just work.” The problem is what else comes attached. 

Here is what superuser actually grants, why your application and your tooling do not need it, and how to give any of them exactly the reach the job requires.

What a superuser can actually do

I will start with the part people typically underestimate. A superuser skips every permission check in the database, so it can read, change, or drop anything, and it can turn off row-level security while it is at it. That alone is a lot. The bigger issue is that superuser reaches past the database and onto the machine.

A superuser can runCOPY ... PROGRAM , which executes a shell command on the server as the operating system user that runs Postgres. It can create functions in untrusted languages like plpython3u or plperlu and run arbitrary code the same way. It can read and write files directly on the server through functions like pg_read_file and pg_write_file. And it can change server configuration with ALTER SYSTEM.

Add those up and a leaked superuser password goes from being a database incident to a compromised server, running as the user that owns all your data. People picture superuser as “the admin account in the app.” It is much closer to root on the database host.

Why tools ask for it, and why they do not need it

The honest reason is convenience. A tool wants to create its tables, run its migrations, maybe read a few system statistics, and asking for superuser makes all of that work on the first try with zero back and forth. It is the path of least resistance, not a real requirement.

But “I need to create some tables” is a very different request from “I need to own the server,” and the whole art of database security lives in the gap between those two sentences.

There is a quick test, and the cloud runs it for you. Managed Postgres on RDS, Cloud SQL, and Azure never hands out real superuser. So if a tool only works as superuser, it will not run on managed Postgres at all, which is usually a sign it is reaching for far more than the work needs. Plenty of well-behaved tools run happily without it. The ones that demand it are worth a second look.

Give the tool its own role and its own schema

Most tools reach for superuser because they want to create objects. You can grant exactly that, and nothing else, by giving the tool its own schema to own.

CREATE ROLE app LOGIN PASSWORD '...';
CREATE SCHEMA app AUTHORIZATION app;

Now the app role owns the app schema. It can create, alter, and drop its own tables, indexes, and functions in there as freely as it likes, with no rights at all over the rest of the database. Set the role’s search_path to its schema and the tool behaves exactly as it expects to, inside its own walls.

This single step covers the common case. Since Postgres 15, the public schema is no longer open for every user to create in, which already helps, but handing a tool its own owned schema is cleaner and clearer than leaning on public at all.

Grant exactly the reads and writes the work needs

For access to data the tool did not create, grant it on purpose rather than by default. Usage on the schema, then the specific privileges on the specific tables:

GRANT USAGE ON SCHEMA sales TO app;
GRANT SELECT, INSERT, UPDATE ON sales.orders TO app;
ALTER DEFAULT PRIVILEGES IN SCHEMA sales
GRANT SELECT ON TABLES TO app;

That last line covers tables that do not exist yet, so you are not re-granting every time someone ships a new table.

When a tool genuinely needs to read across the whole database, Postgres 14 added a built-in role for exactly that. pg_read_all_data grants read on every table, view, and sequence, with no write and no admin power attached. Its counterpart pg_write_all_data does the same for writes. They are a clean way to say “read everything” without going anywhere near superuser. Use them deliberately, though, because pg_read_all_data does include your most sensitive tables.

Keep the sharp edges off

A few capabilities are worth withholding from any application role by name, because they are the ones that turn a database account back into host access:

  • SUPERUSER, for all the reasons above.
  • CREATEROLE and CREATEDB, which are broader than they look and let a role reshape access around itself.
  • The server-file and program roles: pg_read_server_files, pg_write_server_files, and pg_execute_server_program. These hand back the exact file and shell abilities you were trying to keep off.
  • Untrusted procedural languages. An application role has no business creating functions in plpython3u or plperlu.

When an app does need one genuinely privileged action, the safe pattern is a SECURITY DEFINER function. You write a small, tightly scoped function, owned by a role that has the privilege, that performs that one action and nothing else. The app is allowed to call the function, not to hold the privilege. It is the difference between letting someone press one specific button and handing them the whole panel.

A safe role for an agent or a reporting tool

The read-only case comes up constantly: a dashboard, an analytics job, or an AI agent that should look but never touch. This is where least privilege pays off most, because you can make “cannot change anything” a property of the role itself rather than a promise you are trusting the tool to keep.

CREATE ROLE analyst NOLOGIN;
GRANT pg_read_all_data TO analyst;
ALTER ROLE analyst SET default_transaction_read_only = on;

Point that role at a read replica and it cannot affect the primary at all, no matter what it runs. That is how you let an agent or an analyst explore your data freely while removing the ability to change it. The safety comes from the grant, not from good behavior.

What this protects

Credentials leak. They end up in a log, a config file, a screenshot, a repo. The question is never only whether a credential leaks, but how much it can do once it does. Least privilege is what decides the size of that blast radius, and it is the difference between “someone read a few tables they should not have” and “someone owned the server.”

For a regulated team, this is also the first thing an auditor looks for. “The application connects as superuser” is an audit objection. “The application has a scoped role that owns one schema and reads what it needs” is good practice. Pair scoped roles with regular credential rotation, and you have closed the most common and most damaging path at once.

The takeaway

Superuser is the master key to the whole building. Almost nothing needs it, and a package drop-off certainly does not. Give each tool its own role and its own schema, grant the exact reach the task requires, keep the file and program rights off, and use a read-only role for anything that only needs to look.

The next layer, once your roles are scoped, is isolating data within a table so different tenants or users see only their own rows. That is row-level security, and it is a good thing to read next. For AI agents specifically, least privilege is one pillar of a fuller guardrail model that also covers validation and audit. 

If you want a second set of eyes on what your application roles can actually do, that review is the kind of thing Stormatics does quickly. Talk to us about your PostgreSQL security.

FAQ

What can a PostgreSQL superuser do?

A superuser bypasses every permission check and can read, change, or drop anything in the database. It can also run shell commands on the server through COPY ... PROGRAM and untrusted procedural languages, read and write files on the host, and change server configuration. In practice, a superuser is closer to root on the database machine than to an admin account inside the app.

Does my application need superuser?

Almost never. Applications ask for it out of convenience, not need. Give the application its own role and its own schema to own, then grant the specific reads and writes its work requires. If a tool only functions as superuser, note that it will not run on managed Postgres like RDS or Cloud SQL either, which usually means it is over-asking.

What is pg_read_all_data?

A built-in role, added in Postgres 14, that grants read access on every table, view, and sequence in the database, with no write or administrative power. It is a clean way to give a reporting tool or an agent broad read access without granting superuser. Its counterpart pg_write_all_data covers writes.

How do I give a tool read-only access to all tables?

Grant it pg_read_all_data, and set the role to read only with default_transaction_read_only = on so it cannot write even by accident. For maximum safety, point it at a read replica so it cannot affect the primary at all. If you only need a subset, grant SELECT on the specific schemas and use ALTER DEFAULT PRIVILEGES so future tables are covered.

Can I run database migrations without superuser?

Yes. A migration role needs to own the objects it creates and changes, which you get by giving it its own schema with AUTHORIZATION. For the occasional privileged step, wrap it in a SECURITY DEFINER function owned by a privileged role, so the migration tool can call that one action without holding the privilege itself.

Leave A Comment