# RBAC

## What it is

RBAC decides what someone may do by looking at the roles they hold rather than
at who they are. The letters stand for role-based access control, and NIST,
where the model was formalised, also calls it “role based security”. A role is a
named collection of permissions, such as “accounts clerk” or “read-only on
production”. Users are assigned roles, and every permission a user exercises
arrives through one of them.

David Ferraiolo and Rick Kuhn of NIST proposed the general model in 1992, as an
alternative to the mandatory and discretionary access control of the day. In
1996 Sandhu, Coyne, Feinstein and Youman split it into layers: RBAC0 with users,
roles and permissions, RBAC1 adding role hierarchies, RBAC2 adding constraints,
and RBAC3 with both. The two lines were unified and became the standard
ANSI/INCITS 359-2004, revised in 2012. Its reference model has four components.
Core RBAC, the only one every system must have, holds the assignments of users
to roles and permissions to roles, plus the session, in which a user activates a
subset of the roles assigned. Hierarchical RBAC lets a senior role inherit a
junior one. Static separation of duty constrains which roles one user may be
assigned together, and dynamic separation of duty constrains which roles may be
active at once in a session, so someone can hold both the role that raises a
payment and the role that approves it without using both in one sitting.

The difference from an ordinary group is narrower than it looks. The NIST FAQ
puts it this way: a group is usually a collection of users, and most group
mechanisms also let a permission be attached straight to a user, which the FAQ
calls a “loophole” that makes the user-permission relation hard to control. RBAC
closes it by attaching permissions only to roles. The reason is in another line
of the same FAQ: roles in an organisation stay fairly stable, whereas users and
permissions are numerous and change quickly, so routing all access through roles
makes it simpler to manage and to review.

Databases often implement the model directly. PostgreSQL has had one kind of
entity since version 8.1, the role, which can act as a user, a group or both. A
role meant as a group usually has no LOGIN, and membership in it is granted like
any other privilege. Each membership carries two options: INHERIT gives the
member the group's privileges automatically, and SET lets the member switch into
the role with SET ROLE, which comes close to the RBAC idea of activating a role
only in the session that needs it.

```sql
-- the role carries the permissions; nobody logs in as it
CREATE ROLE reporting NOLOGIN;
GRANT USAGE ON SCHEMA sales TO reporting;
GRANT SELECT ON ALL TABLES IN SCHEMA sales TO reporting;

-- a person receives the role, never the table
CREATE ROLE alex LOGIN;
GRANT reporting TO alex;

-- a powerful role that has to be switched on deliberately
CREATE ROLE schema_admin NOLOGIN;
GRANT schema_admin TO alex WITH INHERIT FALSE;
-- later, in the one session that needs it: SET ROLE schema_admin;
```

## Why it matters

Access control has to answer two questions that pull in different directions: is
this request allowed right now, and who can do what across the whole system.
Most schemes answer the first well. RBAC is built for the second, because its
answer is a short list of roles and the people in each, which an auditor or a
manager can actually read. NIST's one-line verdict on the alternative,
attribute-based access control, where each decision evaluates rules over
attributes of the user, the resource and the context, is that it “can be easy to
set up but complex to manage”.

The practical decision is where roles stop being enough. The 1992 model grants
access “only if” the role requirements are met, a wording chosen to leave room
for further checks at access time. A doctor role lets someone read medical
charts, but not every patient's chart: the relationship between this doctor and
that patient is a fact about the data, not a job title. That is where a second
mechanism belongs, such as row-level security in the database or an attribute
rule in the application, layered on top of the roles rather than replacing them.

Our recommendation is to model access as roles first, named after jobs people
actually do, with permissions attached only to roles and never directly to a
login. Keep the role list short enough that one person can read all of it. Add
row-level or attribute rules only where a role demonstrably cannot draw the
boundary, and let the database enforce whatever it can, because a check made by
the database applies to every client, including the code path someone forgot.

## What it looks like in practice

In a database engineering job from 2022 to 2024, access to every developer
environment, application instance, PostgreSQL and MS SQL database and Linux
server came under
[more than 1,000 RBAC rules](/portfolio/enhanced-data-security-by-implementing-1-000-rbac-14/).
The model was the simple part; the volume was the challenge.
Permissions were broken into reusable categories such as “read-only access to
production databases” and mapped to roles such as developers, application
instances and database administrators. Developers reached only their designated
servers, and application instances had limited permissions to prevent lateral
movement. Ansible applied every rule, so every environment matched, and the
rules were documented so teams could audit or change them. Unauthorised-access
risk fell by 85%, and setup took 60% less time than by hand.

Roles rarely work alone. When a
[map application moved from MSSQL to PostgreSQL with PostGIS](/portfolio/improved-geographical-map-application-performance-by-10x-through-6/),
role-based access controls went in together with row-level security to enforce
data-protection policies across several user groups. That pairing is the usual
division of labour: roles decide which tables a group may touch, and row
policies decide which rows inside them.

The strictest version takes the tables out of reach entirely. In a
[function-first PostgreSQL data layer](/portfolio/designed-a-postgresql-function-first-data-layer-across-63/)
built in 2025, every operation the application performs is one of 1,275
functions across 34 schemas. The role the API logs in as, mariner, holds EXECUTE
on the application functions and USAGE on the schemas and nothing else, which
comes to roughly 4,000 explicit grants instead of one blanket grant. The
functions run as SECURITY DEFINER, owned by a separate non-login role with a
pinned search_path, and the superuser account is kept for migrations and cron. A
compromised API would still be limited to what the functions allow. The price is
written down as well: a new field means a migration and a function change.

## Where it goes wrong

- **Roles multiply until nobody can read them.** Every exception becomes a new
  role, such as “developer except billing” or “developer plus one report”, and
  soon there are more roles than people. Role explosion defeats the reason for
  RBAC, which was a list short enough to review. Give a person two or three
  roles rather than minting one per exception, and merge roles whose permission
  sets have become identical.
- **A permission lands on a login directly.** One urgent ticket, one GRANT
  straight to a user, and that user now has access that no role review will
  show. It is the loophole NIST describes in group systems, rebuilt by hand.
  Grant to roles only, and have the review query look for privileges held by
  roles that can log in.
- **Defaults hand out access before any role does.** In PostgreSQL the name
  PUBLIC means every role, and by default it receives EXECUTE on new functions
  and CONNECT and TEMPORARY on databases. Where a database was upgraded from
  PostgreSQL 14 or an older release, it can also create objects in the schema
  called public. Revoke those defaults deliberately, ideally inside the
  transaction that creates the object.
- **Roles only ever grow.** People change jobs and keep the old role beside the
  new one, and temporary access becomes permanent because removing it is
  nobody's task. Each assignment needs a reason and a review date, and leaving a
  job should take away the roles that came with it.
- **Some accounts skip the rules.** Superusers and roles with BYPASSRLS ignore
  row-level security, and a table's owner normally does too unless the table is
  set to FORCE ROW LEVEL SECURITY. An application that connects as the owner or
  as a superuser turns every row policy into decoration, so connect it as a role
  that owns nothing.

## Further reading

- [NIST Role Based Access Control project](https://csrc.nist.gov/projects/role-based-access-control)
  — the archived project home: the model's history, the 2004 standard and its
  2012 revision.
- [NIST RBAC FAQ](https://csrc.nist.gov/projects/role-based-access-control/faqs)
  — roles versus groups, RBAC0 to RBAC3, separation of duty and the four
  components of the standard.
- [PostgreSQL: Role Membership](https://www.postgresql.org/docs/current/role-membership.html)
  — group roles, granting and revoking membership, and the INHERIT and SET
  options.
- [PostgreSQL: Row Security Policies](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
  — default deny, which roles bypass row policies, and FORCE ROW LEVEL SECURITY.
- [PostgreSQL: Privileges](https://www.postgresql.org/docs/current/ddl-priv.html)
  — what PUBLIC receives by default and how to revoke it.
- [SQL Server: Database-level roles](https://learn.microsoft.com/en-us/sql/relational-databases/security/authentication-access/database-level-roles)
  — fixed and user-defined roles inside a SQL Server database.

<https://engineer.company/glossary/rbac/>
