Database audit fix

How to Audit and Restrict PostgreSQL Superusers

A PostgreSQL superuser bypasses every permission check and can run shell commands as the postgres OS user through COPY TO PROGRAM. Only the bootstrap postgres role should carry rolsuper: ALTER ROLE app NOSUPERUSER on the rest.

Hiren KalariyaLast reviewed: Sep 25, 2026Check postgresql-superusers
Medium
severity
Yes
needs root or sudo
3
results it can return
5 of 6
checks in this audit

Every threshold on this page is transcribed from the database-security-least-privilege audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 6 Least-Privilege Permissions checks, or how the audit runs.

Medium severityNeeds sudo

What this check reads

The check runs SELECT rolname FROM pg_roles WHERE rolsuper AND rolname <> 'postgres' as the postgres OS user through psql -X -tAc. It reads the single rolsuper attribute and hard-excludes the one literal role name postgres, so neighbouring attributes such as CREATEROLE, REPLICATION and BYPASSRLS never form part of the answer. The threshold is one row: any role name returned is a WARN that lists it, and an empty result is the PASS in which postgres is the only superuser.

When it applies

Runs only when PostgreSQL is detected (HAS_PG=1) and reachable as the postgres OS user through root/sudo (PGOK=1); otherwise SKIP. It tests one attribute - rolsuper - and hard-excludes the role literally named postgres. Roles that are nearly as powerful but not flagged superuser are not examined: CREATEROLE, CREATEDB, REPLICATION, BYPASSRLS, and membership of pg_read_server_files, pg_write_server_files or pg_execute_server_program. A cluster whose bootstrap superuser is not called postgres (some cloud images, POSTGRES_USER in Docker) will report that role as a finding.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPNot installed / not accessibleNothing was verified. No PostgreSQL was found, or the cluster could not be queried, so its superuser list is unknown.
WARNSuperuser roles other than `postgres`A role beyond the bootstrap account can bypass every permission check and reach the filesystem as the postgres OS user.
PASSOnly `postgres`The bootstrap role is the sole superuser, though near-superuser attributes and predefined-role membership are not covered here.

Why it matters

A PostgreSQL superuser bypasses all permission checks and can read/write files as the OS user (COPY ... TO/FROM PROGRAM, pg_read_file, untrusted languages), effectively shell access as postgres. CVE-2019-9193 formalised that this is by design, not a bug. Application roles should own their schema and nothing more. CIS PostgreSQL "Ensure excessive administrative privileges are revoked"; OWASP least privilege.

Why it fails, and when it is wrong

  • Cloud images and Docker POSTGRES_USER create an extra superuser. Migration tools run as superuser to install extensions, then the role is reused by the app.
  • Roles with CREATEROLE, CREATEDB, BYPASSRLS, REPLICATION, or membership in pg_read_server_files/pg_execute_server_program/pg_write_server_files are not checked but are nearly as powerful.
  • Some extensions require superuser to install but not to use; install once as postgres.

How to fix it

List the superuser roles

sudo -u postgres psql -X -tAc "SELECT rolname FROM pg_roles WHERE rolsuper AND rolname <> 'postgres'"

Every name this returns is a role that bypasses every permission check in the cluster.

Work out what each one actually does

Superuser is almost always standing in for one narrower capability. Read the attributes side by side before you remove anything, because a role that turns out to lean on CREATEROLE, CREATEDB or BYPASSRLS needs that attribute granting back explicitly.

-- review near-superuser attributes:
SELECT rolname, rolsuper, rolcreaterole, rolcreatedb, rolreplication, rolbypassrls FROM pg_roles;

Grant the specific capability

-- give only what is needed:
GRANT CONNECT ON DATABASE appdb TO app;
GRANT USAGE, CREATE ON SCHEMA app TO app;
ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT SELECT,INSERT,UPDATE,DELETE ON TABLES TO app;

ALTER DEFAULT PRIVILEGES is the line most people leave out. Without it the role keeps working today and loses access to every table created after the grant.

Remove superuser

ALTER ROLE app NOSUPERUSER;

Reconnect as the role afterwards and run its normal workload. If something breaks now, it was relying on superuser to bypass a permission check, and the privilege it actually wants belongs in the previous step.

Verify the fix

sudo -u postgres psql -X -c "SELECT rolname FROM pg_roles WHERE rolsuper"
# expected: postgres only

# The attributes this check does not look at:
sudo -u postgres psql -X -c \
  "SELECT rolname, rolsuper, rolcreaterole, rolcreatedb, rolreplication, rolbypassrls FROM pg_roles WHERE NOT rolcanlogin = false"

# And the powerful predefined-role memberships:
sudo -u postgres psql -X -c \
  "SELECT r.rolname, g.rolname AS member_of FROM pg_auth_members m
     JOIN pg_roles r ON r.oid=m.member JOIN pg_roles g ON g.oid=m.roleid
    WHERE g.rolname LIKE 'pg_%'"

# Prove the app role lost its powers:
sudo -u postgres psql -X -c "SET ROLE app; COPY (SELECT 1) TO PROGRAM 'id'"
# expected: ERROR: must be superuser or a member of pg_execute_server_program

Debugging

HAS_PG=0 or PGOK=0; see the PostgreSQL Auth Methods debugging notes for how to test the probe.

If your superuser is named something other than postgres (common with Docker's POSTGRES_USER or a cloud image), the check reports it because it excludes that one literal name. This is a false positive; record it.

Migration and extension installation often need superuser, application traffic does not. Use a separate role for migrations, or install extensions once as postgres, then ALTER ROLE app NOSUPERUSER.

The known blind spot. Run the attribute and membership queries above; pg_read_server_files or pg_execute_server_program membership is close to superuser in practice.

The role was relying on superuser to bypass permission checks. Grant the concrete privileges instead (CONNECT, schema USAGE/CREATE, table DML, and ALTER DEFAULT PRIVILEGES for future objects) as shown in How to fix.

Sources

How the script reads this

Next

Re-run the Least-Privilege Permissions audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 6 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 6 Least-Privilege Permissions fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals