Role-based access control (RBAC)
Open in the interactive app: practice, quiz, mock interview →In one minute
- Role-based access control grants permissions to roles and assigns roles to users, so access is managed by job function rather than person by person.
- A permission is an action on a resource type, such as
invoice:approve. A check asks: does any role this user holds, directly or by inheritance, grant this permission in this scope? - Scoping roles to a tenant, project, or resource is what makes RBAC work in multi-tenant products. Global roles alone cannot express "admin of project A only".
- RBAC's weak spot is role explosion: every exception becomes a new role. Attribute rules (ABAC) or relationship graphs (ReBAC) handle per-object and contextual decisions better.
- Authorisation should be enforced centrally and checked on every request, not only in the UI.
How it works
Core model
Users map to roles and roles map to permissions. A check unions the permissions of the user's roles and tests membership; the resolved set changes rarely, so cache it. The model was formalised by Ferraiolo and Kuhn at NIST in 1992 and standardised as ANSI/INCITS 359-2004, revised in 2012 (NIST RBAC project). The classic reference models build up in layers: RBAC0 is users, roles, permissions and sessions; RBAC1 adds hierarchies; RBAC2 adds constraints; RBAC3 has both (Sandhu et al. 1996). The session part is often forgotten: a user activates only the roles a task needs, which is the idea behind modern just-in-time elevation of admin roles.
Hierarchy
Roles can inherit. billing-admin inherits billing-viewer, so admins get read rights without duplicating them. Keep inheritance acyclic and shallow.
Scope
In a SaaS product a user may be owner in workspace A and viewer in workspace B. Role assignments therefore carry a scope: (user, role, scope). Scopes can nest, such as organisation, project, and environment, with assignments at a parent applying to children. The check becomes: find assignments for this user whose scope is the resource's scope or an ancestor.
Constraints
Separation of duty prevents one person from holding conflicting roles, for example payment-creator and payment-approver. Static SoD blocks the assignment; dynamic SoD allows both roles but not both actions on the same object.
Enforcement architecture
A policy decision point (PDP) answers allow or deny for enforcement points in gateways and services. It can be an embedded library, a sidecar, or a central service. The UI may hide buttons, but the server must check every request. Broken access control is the number one category in the OWASP Top 10:2025, and OWASP's data found some form of it in every application tested (OWASP A01:2025).
When RBAC is not enough
"Users can edit documents they own" or "managers can approve expenses under 5,000 USD from their own reports" are per-object, attribute-based rules. Encoding them as roles leads to one role per document or per amount band. Use ABAC conditions or a relationship-based model, as in Google's Zanzibar, and keep roles for coarse job functions. Zanzibar handles trillions of ACLs and millions of checks per second with p95 latency under 10 ms (Zanzibar paper); open-source descendants such as OpenFGA and SpiceDB bring the same tuple model to ordinary teams.
Down the stack
Layer 1The contract
An RBAC decision point answers check(subject, action, resource) with allow or deny, denying by default when nothing grants access. Given the same policy snapshot it is deterministic. The important and often unstated part is freshness: after a revocation is acknowledged, how long can old grants still be honoured? Google's Zanzibar makes this explicit with consistency tokens ("zookies") so a check is evaluated at a snapshot at least as new as the content it protects, which closes the "new enemy" problem of a check seeing an old ACL with new content (Pang et al., USENIX ATC 2019).
Layer 2The mechanism
Kubernetes is a clean reference. A Role or ClusterRole holds rules made of apiGroups, resources, verbs, and optionally resourceNames. A RoleBinding attaches subjects (users, groups, service accounts) to a role within one namespace, which is the scope. The authorizer allows a request if any binding grants it; permissions are purely additive with no deny rules, so evaluation order does not matter (Kubernetes RBAC docs). The same docs warn that wildcards in resources or verbs silently grant access to resource types and verbs added later. Not every system is union-only: in AWS IAM an explicit deny in any applicable policy overrides every allow, and service control policies and permission boundaries intersect with identity policies (AWS IAM docs). Explicit deny is useful for guardrails ("nobody outside the security team may disable logging") but makes "why was this denied?" harder to answer. It also prevents privilege escalation: you cannot create a binding that grants permissions you do not hold yourself unless you have the bind or escalate verb. In application code, a fast representation numbers each permission and stores each role as a bitset, so 120 permissions fit in two 64-bit words and a check is a bitwise AND. Relationship-based systems store tuples such as doc:7#editor@user:alice and evaluate rewrite rules over them, with special indexes to flatten deeply nested groups.
Layer 3The machine
A bitset check touches one cache line and takes a few nanoseconds; a hash set lookup on permission strings costs tens of nanoseconds; a database query costs a millisecond or more. Caching effective permissions for 320,000 users at 2 KB each would be 640 MB per process, so caches are LRU-bounded per instance or kept in a shared store. Putting roles into JWTs avoids lookups but has limits: many proxies reject request headers over 8 KB (nginx returns 400 when one header field exceeds its default 8 KB buffer, per the nginx docs), so tokens carry coarse roles, not full permission lists, and a token's lifetime becomes the worst-case revocation delay. Resolving nested groups is a graph traversal whose depth must be bounded to keep latency predictable.
Why it's built this way
Organisations reason in job functions, so permissions are grouped into roles. Every request needs a decision, so effective permissions are precomputed and cached. Union-only semantics make policies composable and order independent, at the cost of no explicit deny; systems that need organisation-wide guardrails add a separate deny layer on top, as AWS does. Caching trades freshness for speed, so revocation needs invalidation or consistency tokens. Tenants must never see each other's data, so every assignment carries a scope.
Diagram
flowchart LR U[User alice] -->|"assigned in project-A"| R1[editor] U -->|"assigned in org"| R2[viewer] R1 -->|inherits| R2 R1 --> P1["doc:write"] R2 --> P2["doc:read"] Q["Check: alice, doc:write, project-A/doc-7"] --> PDP[Decision point] PDP -->|"scope matches, editor grants"| A[Allow]
Worked example
A B2B analytics product has 8,000 customer organisations, an average of 40 users each (320,000 users), and serves 25,000 API requests per second. Every request needs an authorisation check.
The model has 6 built-in roles, enterprise custom roles, about 120 permission strings, and assignments scoped to organisation or project. About 1 million assignment rows fit easily in one indexed table.
Check path: on login the service resolves the user's effective permissions per scope and caches them in the session or a near cache, about 2 KB per user. A check is then a hash lookup, under 10 microseconds. At 25,000 RPS that is trivial CPU. Without caching, 25,000 database queries per second for authorisation alone would dominate the database.
Revocation: cached permissions outlive a removed role until refresh, so pair a 60-second TTL with invalidation events on change, and check high-risk permissions such as org:delete live.
After two years, customers have created 14,000 custom roles, many differing by one permission; usage reports let the team consolidate them.
In the wild
- First American Financial, 2019: sequential document IDs and no login exposed 885 million records. Links shared through its EaglePro app ended in a document number, and swapping in another number returned unrelated customers' documents without authentication, back to 2003. The company's own penetration test found it in December 2018 but it was not fixed until a journalist reported it in May 2019. New York's financial regulator fined it $1 million in 2023 for failing to maintain access controls (NYDFS consent order), and the SEC separately fined it $487,616 over disclosure controls (SEC).
- Microsoft, 2023: a valid signature from the wrong scope. Storm-0558 used a consumer (MSA) signing key to forge tokens that enterprise Exchange Online accepted. Microsoft's validation libraries checked the signature but not the key's scope, and the mail team assumed they did both. The fix was libraries that check scope automatically, the same "permission valid, scope unchecked" bug as an IDOR at the identity layer (MSRC).
- Google Cloud, 2025: one blank policy row took down API authorization worldwide. Service Control runs policy and quota checks on every Google Cloud API call. A policy row with blank fields hit a new code path with no feature flag and no error handling, and the binary crash-looped in every region within seconds, so external APIs returned 503 for hours. Google promised proper error handling (failing open where needed), incremental propagation of global policy data and feature flags off by default (Google Cloud status).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Flat RBAC | Simple, auditable, fast checks | Role explosion for fine-grained rules | Internal tools, small permission sets |
| Scoped, hierarchical RBAC | Fits multi-tenant SaaS, clear inheritance | Scope resolution adds complexity | Most B2B products |
| ABAC (attribute rules) | Contextual rules on time, amount, location | Harder to audit "who can do X" | Rules depend on resource or request attributes |
| ReBAC (relationship graph) | Natural for sharing, ownership, nested groups | Needs a graph store and consistency care | Document sharing, social or collaborative apps |
Pitfalls in production
- Checks only in the UI or gateway, not in the service. Enforce authorisation at the service that owns the data, on every request.
- Insecure direct object references: the role allows
doc:read, but the code never checks the document belongs to the user's tenant. Always check scope as well as permission. - Role explosion makes audits impossible. Review role counts and usage, and move per-object rules to ABAC or ReBAC.
- Stale caches keep removed users in. Invalidate on assignment change and bound TTLs.
- Wildcard grants such as
*on resources or verbs. They quietly expand when new resource types ship. List verbs and resources explicitly and lint policies in CI. - Treating the decision point as infallible. If the PDP or its policy store breaks, every request breaks with it. Decide fail-closed or fail-open per action, roll policy changes out gradually, and keep a cached last-known-good policy.
Leadership lens
- Define the permission vocabulary early as
resource:actionstrings; renaming later touches every service. - Ask for every endpoint: which permission, which scope, and where is it enforced?
- Decide where RBAC stops and attribute or relationship rules start, before role explosion decides for you.
Check yourself
A user has
doc:readbut reads another tenant's document by changing an id in the URL. Which part of the check failed?Show answer
The scope check. The permission was valid, but the system did not verify the resource belongs to a scope where the user holds that role.
Why does "users can edit their own drafts" fit poorly in pure RBAC?
Show answer
It depends on a relationship between the user and a specific object. RBAC would need a role per document; an ownership attribute or relation expresses it directly.
What is the difference between static and dynamic separation of duty?
Show answer
Static SoD forbids holding both conflicting roles at all; dynamic SoD allows both but forbids exercising both on the same transaction.
Why cache resolved permissions, and what is the risk?
Show answer
Checks happen on every request, and caching makes them hash lookups. The risk is delayed revocation, handled with short TTLs and invalidation events.
Sources and further study
Primary
- Ferraiolo and Kuhn, Role-Based Access Controls (1992): the NIST paper that defined RBAC for commercial systems.
- Sandhu et al., Role-Based Access Control Models (IEEE Computer, 1996): the RBAC0 to RBAC3 family, sessions, hierarchies and constraints.
- Pang et al., Zanzibar (USENIX ATC 2019): tuples, userset rewrites, zookies and the Leopard index for nested groups.
- OWASP Top 10:2025, A01 Broken Access Control: how often access control fails in tested applications and the standard defences.
Systems docs
- AWS IAM policy evaluation logic: explicit deny, permission boundaries and SCPs layered on top of allows.
- Azure deny assignments: how Azure RBAC adds deny on top of its role assignments.
- OpenFGA concepts: an open-source Zanzibar-style model with types, relations and tuples.
Books
- Building Secure and Reliable Systems, Adkins et al., chapter 5 "Design for Least Privilege": classifying access by risk, multi-party authorization and a policy framework for authorization decisions. Free online.
- Release It! (2nd edition), Michael T. Nygard, chapter 11 "Security": the OWASP Top 10 and the principle of least privilege from an operations angle.
Other curricula
- roadmap.sh cyber security roadmap: places RBAC as one node among access control concepts, without scope or caching.
- donnemartin/system-design-primer: mentions least privilege in a four-bullet security section and does not cover authorization models.
Watch
- Role-based access control (RBAC) vs. Attribute-based access control (ABAC) (IBM Technology): when roles suffice and when attributes are needed.
- USENIX ATC '19: Zanzibar, Google's Consistent, Global Authorization System: the relationship-based model used beyond RBAC at Google scale.
Free resources
- NIST Role-Based Access Control project: the origin and standard model of RBAC, including constraints.
- OWASP Authorization Cheat Sheet: least privilege, deny by default, and checking on every request.
- Kubernetes RBAC: a widely used, scoped RBAC implementation with roles and bindings.
- Zanzibar: Google's Consistent, Global Authorization System: the paper behind relationship-based access control.