Databases / PostgreSQL
Tutorial

PostgreSQL Role-Based Access Control Setup and Management Tutorial

Learn how to design, implement, validate, and operate PostgreSQL role-based access control using roles, grants, group roles, and safe privilege management.

PostgreSQL Role-Based Access Control Setup and Management Tutorial

Why PostgreSQL RBAC matters operationally

PostgreSQL role-based access control, or RBAC, is the practical way to decide who can connect, who can read or write specific data, and who can administer the database without relying on shared superuser credentials. In real environments, the failure mode is usually not a missing feature; it is an overprivileged role, an inherited grant that was never removed, or a production account that can do far more than its owner intended.

In this tutorial, you will build a small but production-shaped PostgreSQL RBAC model with login roles, group roles, object privileges, default privilege controls, and validation checks. By the end, you should be able to decide whether a role design fits your environment, implement it safely, test that access is actually enforced, and verify what to check before allowing it into production.

Prerequisites and stop-here warnings

Before you start, make sure you can connect as a PostgreSQL superuser or a role with equivalent privilege management rights. You also need a working database, a schema to protect, and enough access to create roles and grant privileges.

Stop here if any of these are true

  • You do not know which role currently owns the database objects you need to protect.
  • Your application still connects with a shared superuser or database owner account.
  • You cannot test access from a separate session using a non-privileged account.
  • You do not know whether your application creates objects at runtime, such as tables or sequences.

Those gaps matter because RBAC changes can fail silently if the wrong role owns objects, if privileges are inherited unexpectedly, or if runtime object creation is not covered by default privileges.

Plan the role model before changing permissions

A good PostgreSQL RBAC setup starts with clear role separation. You want login roles for people or applications, group roles for privileges, and narrowly scoped ownership where possible.

Goal

Define who authenticates, who receives permissions, and who owns the objects.

Action

Use a structure like this:

  • app_reader as a group role with read access.
  • app_writer as a group role with read/write access.
  • app_service as a login role for the application.
  • db_admins as a group role for controlled administrative access.

A login role should authenticate directly, while group roles should usually not log in. That keeps access easier to audit and reduces the chance of credential sprawl.

Expected output

A role map that answers three questions:

  1. Who logs in?
  2. Which role grants permissions?
  3. Which role owns the objects?

Validation

Confirm that you can explain each role in one sentence. If a role cannot be described clearly, it is probably doing too much.

Common failure

The most common mistake is granting object privileges directly to individual login roles. That works at first, but it becomes unmanageable when users change teams or when you need to revoke access quickly.

Create roles with least privilege in mind

The next step is to create the roles themselves. Use login roles only where authentication is required, and use NOINHERIT when you want membership to require an explicit privilege context instead of automatic access.

Goal

Create roles that separate authentication from authorization.

Action

Example setup:

CREATE ROLE app_reader NOLOGIN;
CREATE ROLE app_writer NOLOGIN;
CREATE ROLE db_admins NOLOGIN;

CREATE ROLE app_service LOGIN PASSWORD 'change-me-now';
GRANT app_reader TO app_service;

If your operational model requires the login role to inherit privileges automatically, you can omit NOINHERIT on the login role. If you want tighter control, keep NOINHERIT and use SET ROLE after connection.

For environments that manage many database systems, this role separation is similar in spirit to the way you would carefully stage access controls in How to Secure MongoDB with TLS Authentication and RBAC or How to Enable MongoDB Role-Based Access Control Securely: create administrative pathways first, then enable restricted access only after validation.

Expected output

A set of roles that can authenticate, receive permissions, and be audited independently.

Validation

Run:

\du

You should see the roles listed with the expected login status and memberships.

Common failure

A login role without the right membership will connect successfully but still fail on every protected object. That is good from a security perspective, but it can look like a broken application if you do not test permissions before deployment.

Protect schemas before granting table access

Database-wide privileges are usually too broad. In PostgreSQL, schema access is a gatekeeper for object access, so protect the schema first.

Goal

Ensure roles can reach only the schemas they are supposed to use.

Action

A typical sequence is to revoke broad default access and then grant only what is needed:

REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA app_data TO app_reader;
GRANT USAGE ON SCHEMA app_data TO app_writer;

If your application should not create objects in the schema, avoid granting CREATE there. If it does need to create objects, grant CREATE only to the role that actually requires it.

Expected output

Roles can resolve object names in the permitted schema, but they cannot create or use unrelated schema objects.

Validation

From a non-privileged session, confirm that:

  • USAGE works only for the intended schema.
  • Unauthorized schemas remain inaccessible.
  • Object lookup fails outside the granted scope.

Common failure

A frequent mistake is leaving CREATE on public, which allows unexpected objects to appear in a widely visible schema. That can create both security and operational confusion.

Grant object privileges by function, not by convenience

Once schema access is correct, grant privileges on tables, sequences, and views according to what the role must do.

Goal

Align privileges with the exact workload.

Action

Example grants for a read-only role:

GRANT SELECT ON ALL TABLES IN SCHEMA app_data TO app_reader;

Example grants for a write role:

GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app_data TO app_writer;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app_data TO app_writer;

If your application uses sequences for identity columns or serial-style inserts, do not forget sequence privileges. A role can have table insert rights and still fail because it cannot advance the sequence.

Expected output

The read role can query data, the write role can modify data, and neither has unnecessary access outside the intended schema.

Validation

Use a separate session with the target role and test the exact operations the application will perform:

  • SELECT from a protected table.
  • INSERT into a writable table.
  • UPDATE and DELETE only where intended.
  • Sequence-backed inserts if applicable.

Common failure

The most common omission is sequence privileges. Another is granting access on one table manually and forgetting a new table created later.

Set default privileges for future objects

If your application or deployment pipeline creates tables later, you must also control default privileges. Without this, new objects may inherit only the ownership defaults, not the access pattern you intended.

Goal

Make future objects inherit the right access model automatically.

Action

Default privileges apply to objects created by a specific owner in a specific schema. Example:

ALTER DEFAULT PRIVILEGES IN SCHEMA app_data
GRANT SELECT ON TABLES TO app_reader;

ALTER DEFAULT PRIVILEGES IN SCHEMA app_data
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_writer;

If tables are created by one role but accessed by another, run default privilege changes under the role that actually creates the objects. That detail is easy to miss and is a common source of “it worked for the first table” incidents.

Expected output

New tables created by the object owner automatically receive the expected access grants.

Validation

Create a test table using the same owner that production migrations use, then inspect its privileges with \dp.

Common failure

Default privileges do not retroactively fix old objects. They only affect future objects created by the relevant owner.

Use role membership carefully

Membership is one of the most powerful parts of PostgreSQL RBAC. It is also one of the easiest ways to overgrant access if you do not review inheritance.

Goal

Allow controlled privilege reuse without accidental escalation.

Action

Grant group roles to login roles only when the relationship is deliberate:

GRANT app_reader TO app_service;

If an operator needs temporary elevated access, prefer a controlled SET ROLE workflow instead of permanent broad membership. That keeps audit trails cleaner and reduces standing privilege.

Expected output

Login roles can assume only the access level they actually need.

Validation

Check role membership and test whether privileges are active automatically or only after SET ROLE.

Common failure

If you expect NOINHERIT but the login role inherits privileges anyway, the role model is not doing what you think. Verify the login role attributes before trusting the setup.

Validate access from the perspective of the role

Granting privileges is not enough. You need to verify the enforcement path from a real session that uses the target role.

Goal

Confirm that allowed operations succeed and denied operations fail.

Action

Connect as the role or assume it explicitly, then test a few representative operations:

SELECT current_user, session_user;
SELECT * FROM app_data.sensitive_table LIMIT 1;

If you use SET ROLE, confirm the active role is the expected one:

SET ROLE app_reader;
SELECT current_user, session_user;

For broader validation, inspect effective privileges with catalog views and meta-commands such as \du and \dp.

Expected output

  • Allowed statements succeed.
  • Denied statements fail with permission errors.
  • The active role shown in the session matches your design.

Common failure

Testing only as a superuser tells you almost nothing about real authorization behavior. Always validate from the role’s own perspective.

Revoke and tighten access safely

As applications change, RBAC needs cleanup. Removing access is often more important than adding it because over time privilege creep accumulates.

Goal

Remove unused or risky access without breaking legitimate workloads.

Action

Audit current grants, then revoke what is no longer necessary:

REVOKE INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app_data FROM app_reader;
REVOKE CREATE ON SCHEMA app_data FROM app_writer;

If a login role no longer needs a group role, remove the membership instead of leaving dormant access in place.

Expected output

Privileges match current operational need rather than historical convenience.

Validation

After revocation, rerun the same access tests that previously succeeded. The intended failures should now occur.

Common failure

Revoke operations can be blocked by object ownership or by grants inherited through another role. When a permission still appears active, check the full membership chain and the object owner.

Operational follow-up for production readiness

A working RBAC configuration is not finished when the commands succeed. It is finished when you can operate it safely.

Goal

Make the configuration maintainable and auditable.

Action

Keep these operational controls in place:

  • Record which roles own objects and which roles only consume them.
  • Review role memberships whenever a user changes teams or an application changes identity.
  • Recheck privileges after schema migrations, because new tables and sequences may require fresh grants.
  • Confirm that backups, maintenance jobs, and migration tools use the correct role and are not accidentally overprivileged.

If your environment depends on object growth over time, combine manual review with validation checks similar to the disciplined change control used in PostgreSQL Index Bloat Detection and Reindexing Best Practices: the point is not just to react, but to verify regularly that the state still matches the design.

Expected output

A repeatable process for keeping access tight after the initial rollout.

Validation

Before production use, confirm all of the following:

  • No shared superuser credentials are used by applications.
  • Every login role has a clear purpose.
  • Every group role maps to a documented access function.
  • Default privileges cover future objects.
  • Denied actions fail in test sessions.
  • Object ownership is intentional and understood.

Common failure

The most dangerous failure is assuming that a successful deployment means the permissions are correct forever. In practice, RBAC degrades when new objects, new owners, or new service accounts are introduced without a review.

A practical end state to aim for

A finished PostgreSQL RBAC setup should look boring in the best possible way: login roles authenticate, group roles carry privileges, schemas are restricted, object grants are minimal, and new objects inherit the right permissions automatically. The validation process should prove that authorized operations work and unauthorized operations fail.

If you can explain who can connect, what they can do, which objects they can reach, and how you verified it, your RBAC model is ready for real operational use.

Use this guidance together with SQL Server DMVs to connect the workflow with related operational context already available on the site.

Continue learning

Related content