Skip to main content
RunBook Academy

PostgreSQLV · Authentication, Roles and TLSAuthorisation

Ownership, default privileges, and the permission disaster they cause

Intermediate⏱ ~25 minpsql

What you'll learn

  • Explain why GRANT ON ALL TABLES does not cover future tables
  • Configure ALTER DEFAULT PRIVILEGES correctly, including the FOR ROLE clause
  • Diagnose a permission failure that appeared after a deployment
  • Describe what object ownership confers beyond privileges

Prerequisites

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

The commonest permission failure in PostgreSQL is not a missing grant. It is a grant that was correct when it was made and does not cover the objects created since.

The demonstration

A schema with a read-only group, granted SELECT on all tables. Then a new table is created.

Read-only / SafeGRANT ON ALL TABLES, then a new table
$ psql -U postgres  # GRANT, CREATE TABLE, then check both
GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_ro;
CREATE TABLE app.created_later(id int);

    tbl      | app_ro_can_select
---------------+-------------------
existing      | t
created_later | f
(2 rows)

ALL TABLES is not a standing instruction. It expands, at the moment the statement runs, to the list of tables that exist, and grants on each of them. A table created a second later is not covered, and nothing reports this.

The fix, and its trap

ALTER DEFAULT PRIVILEGES records a rule applied whenever an object is created.

Configuration changea standing rule, and a table created afterwards
$ psql -U postgres  # ALTER DEFAULT PRIVILEGES, then CREATE TABLE
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
GRANT SELECT ON TABLES TO app_ro;

CREATE TABLE app.created_after_default(id int);

        tbl          | app_ro_can_select
-----------------------+-------------------
created_after_default | t
(1 row)

The trap is in the FOR ROLE clause. Default privileges are recorded per creating role, and apply only when that role creates an object.

Read-only / Safethe rule, as stored
$ psql -U postgres -c 'SELECT defaclrole::regrole AS for_role, defaclnamespace::regnamespace AS in_schema, defaclobjtype AS objtype, defaclacl FROM pg_default_acl'
 for_role  | in_schema | objtype |      defaclacl
-----------+-----------+---------+----------------------
app_owner | app       | r       | {app_ro=r/app_owner}
(1 row)

Omitting FOR ROLE records the rule for the role running the statement, which is usually a superuser or a DBA — and if migrations actually run as app_owner, the rule never fires.

Covering the ground properly

A complete setup for one schema needs both halves, and needs sequences as well as tables:

-- Retrospective: everything that exists now
GRANT USAGE ON SCHEMA app TO app_ro, app_rw;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_ro;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_rw;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO app_rw;

-- Prospective: everything app_owner creates from now on
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
  GRANT SELECT ON TABLES TO app_ro;
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_rw;
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
  GRANT USAGE, SELECT ON SEQUENCES TO app_rw;

Three things that are easy to omit:

USAGE ON SCHEMA is a prerequisite for every table privilege beneath it. Without it, table grants exist and are unusable, and the error is the same permission denied — this is the other half of the diagnosis in the previous lesson.

Sequences back every GENERATED or serial column. A role with INSERT on a table and nothing on its sequence cannot insert, and the error names the sequence rather than the table, which sends people looking in the wrong place.

Default privileges are per schema. A rule for app does nothing for a schema added later, so creating a schema is also a permissions task.

What ownership confers

Ownership is not a privilege and cannot be granted with GRANT. The owner of an object may always ALTER it, DROP it, and change its privileges, regardless of what has been granted or revoked. Revoking everything from an owner does not stop them.

This is why the previous lesson’s pattern puts ownership on a NOLOGIN role: the ability to drop a table should not be attached to a credential that an application holds.

-- Move existing objects to the intended owner
REASSIGN OWNED BY old_role TO app_owner;

-- Then the old role can be dropped, once it owns nothing
DROP OWNED BY old_role;
DROP ROLE old_role;

DROP ROLE fails while the role owns anything or holds any privilege, which is a useful safety property and a frequent surprise during decommissioning. REASSIGN OWNED and DROP OWNED are the two statements that clear the way, and they operate per database — they must be run in each database where the role owns something.

Production discipline

  1. Pair every GRANT ON ALL with an ALTER DEFAULT PRIVILEGES. The first covers the past, the second the future.
  2. Always write FOR ROLE, naming the role that actually creates objects. Omitting it records the rule for whoever ran the statement, which is usually not the migration role.
  3. Audit pg_default_acl against your deployment pipeline. A creating role missing from that list is a future incident.
  4. Grant USAGE ON SCHEMA and sequence privileges too. Both produce permission denied errors that point somewhere unhelpful.
  5. Keep ownership on a NOLOGIN role. The owner can always drop the object, whatever has been revoked.
  6. Use REASSIGN OWNED and DROP OWNED per database when decommissioning a role, since DROP ROLE refuses while anything is owned.

Cross-course references

  • Git, CI/CD & GitOps — Part CVI (Change management) covers making a migration’s permission consequences part of its review, which is where the default-privileges gap is cheapest to catch.
  • Secrets, PKI & Certificate Management — Part I (Foundations) covers blast-radius reasoning, which is the argument for separating ownership from the credentials an application holds.
  • Ansible for Production Sysadmins — Part XII (Idempotency) covers expressing these grants as a desired state rather than as one-time statements.

Quiz

Knowledge check · 6 questions

  1. Q1. A reporting role could read every table on Monday. After Tuesday's migration it cannot read three new tables. No grants were revoked. What happened?

  2. Q2. A DBA sets default privileges while connected as postgres, tests by creating a table as postgres, and confirms the reporting role can read it. Migrations run as app_owner and the problem recurs. Why?

  3. Q3. Which are commonly omitted from a schema permission setup and produce a permission denied error? Select all that apply.

  4. Q4. Revoking all privileges from an object's owner prevents the owner from dropping it.

  5. Q5. Explain the difference between a null access control list on a table and one that exists but omits a given role.

  6. Q6. Diagnose and give the durable fix.

    A nightly reporting job has failed on the first of the month for four consecutive months, always with permission denied on a table whose name contains the previous month. Each time, an engineer grants SELECT on that table to the reporting role and the job succeeds. The application creates a new partition table each month through a migration that connects as the role deploy_bot. A DBA reports having configured default privileges for this schema last year.

Passing score: 75%. Answers are checked in this browser.