Access Control Models
Imagine a hospital database holding patient records, billing files, and staff schedules. A nurse should read charts on her ward. But she shouldn't edit billing codes. A doctor should update prescriptions but not delete audit logs. An administrator should manage accounts. But she shouldn't peek at diagnoses without cause. Somebody, somewhere, must encode all of these rules precisely. That encoding must be exact enough for a machine to enforce it. It must work on every single request, thousands of times per second. All this without human judgment in the loop. This is the problem access control solves. Authentication answers "who are you?" Access control answers the harder question: "now that I know who you are, what are you allowed to do?" Every operating system, database, cloud platform, and web application answers this question. Each uses one of a small number of well-studied models. Get the model wrong and you either lock out legitimate users constantly, or you leave a door open. An attacker will eventually find that open door. The big picture is that access control is fundamentally about relationships between three things: subjects (users, processes, services) who want to act, objects (files, records, devices, APIs) that are acted upon, and permissions (read, write, execute, delete) that constrain the action. Different organizations need different philosophies for assigning these permissions. Sometimes owners should decide. Sometimes a central policy must override everyone. Sometimes permissions should track job roles rather than individual identities. This article builds the conceptual toolkit from the ground up. It starts with the access control matrix as the unifying abstraction. Then it covers the three dominant models built on top of it — Discretionary Access Control (DAC), Mandatory Access Control (MAC), and Role-Based Access Control (RBAC). It includes worked traces, a state-machine-style diagram, and an honest look at where each model breaks down in practice.
- Access control decides what an authenticated subject may do to an object — the step after login.
- The access control matrix (subjects × objects → permissions) is the universal reference model behind every scheme.
- DAC lets object owners grant/revoke permissions at will (Unix file permissions, Windows ACLs) — flexible but leak-prone.
- MAC enforces a system-wide policy (like security clearances) that no user, not even the owner, can override.
- RBAC assigns permissions to roles, and users inherit permissions by holding roles — the dominant model in enterprise IT.
- These models set up The Principle of Least Privilege and Sandboxing and Isolation, which restrict how much access even a granted subject gets.
Access control is the enforcement of a policy — matrix, lattice, or role graph — that decides, for every (subject, object, action) triple, whether the action is permitted.
The idea
An access control model is a formal scheme for deciding whether a subject is authorized to perform an action on an object. A subject is an active entity — a user, a process running on that user's behalf, or a service account. An object is a passive resource — a file, a database row, a socket, a printer. A permission (or right) is an operation such as `read`, `write`, `execute`, `append`, or `delete`. Access control is distinct from Authentication and Password Security: authentication establishes identity, while access control decides authorization — what that identity may do once it is trusted. The unifying mathematical abstraction underlying every access control scheme is the access control matrix, introduced formally by Butler Lampson in 1971 and refined by Harrison, Ruzzo, and Ullman (the HRU model) in 1976. Conceptually it is a table with one row per subject and one column per object; the cell at `(subject, object)` lists the permissions that subject holds on that object. This matrix is complete in principle but useless in practice at scale — a system with a million users and a billion files would need a matrix with 10^15 cells, almost all empty. Real systems store the matrix compressed, either by row (each subject carries a list of what it can access — a capability list) or by column (each object carries a list of who can access it — an access control list, or ACL). Understanding this compression is key to understanding why DAC, MAC, and RBAC look so different in code even though they are all, underneath, filling in the same matrix. The three models differ in who decides the matrix entries and how rigidly they are enforced:
- Discretionary Access Control (DAC): the owner of an object decides who else gets access, and can change their mind. Permissions propagate at the owner's discretion. This is the model behind Unix `chmod`/`chown` and Windows NTFS permissions.
- Mandatory Access Control (MAC): a central authority defines an inviolable policy — typically based on security labels (e.g., Confidential, Secret, Top Secret) attached to both subjects and objects — and no individual user can override it, not even the object's creator. This is the model behind military multi-level security and SELinux.
- Role-Based Access Control (RBAC): permissions are assigned to abstract roles (Nurse, Auditor, Sysadmin) rather than to individual users; users are assigned to roles, and gain the union of their roles' permissions. This decouples "who a user is" from "what a user can do," which is what makes it scale in large organizations.
How it works
The access control matrix, formally
Let `S` be the set of subjects, `O` the set of objects, and `R` the set of rights. The matrix `M` is a function: ``` M : S × O -> 2^R ``` `M[s][o]` is the subset of rights subject `s` holds on object `o`. A reference monitor — the component that actually intercepts every access attempt — evaluates a simple predicate on every request: ``` function CHECK_ACCESS(subject s, object o, right r): if r in M[s][o]: return ALLOW else: return DENY ``` This predicate is trivial; the entire engineering difficulty is in (a) building `M` correctly, (b) storing it efficiently, and (c) updating it safely as permissions change (which is exactly what HRU studies: whether a sequence of grant/revoke operations can ever leak a right that the policy intended to forbid).
DAC: owner-driven ACLs
In DAC, each object stores its own column of the matrix as an ACL: a list of `(subject, permissions)` pairs, plus commonly an owner field. The owner can grant or revoke entries. ``` structure ACL_Entry: subject permissions # subset of {read, write, execute, ...} structure Object: owner acl : list of ACL_Entry function GRANT(object o, requester u, target t, perms): if u != o.owner and "grant" not in permissions_of(u, o): return DENY # only the owner (or a delegate) may grant find_or_create_entry(o.acl, t).permissions |= perms return OK function CHECK(object o, subject s, right r): for entry in o.acl: if entry.subject == s and r in entry.permissions: return ALLOW return DENY ``` Unix permission bits (`rwxr-xr--`) are a compact special case of an ACL restricted to exactly three subjects per object: owner, group, and everyone else. Modern filesystems (NTFS, POSIX ACLs, ext4 with `acl` mount option) generalize this to arbitrary per-user entries.
MAC: labels and a lattice
In MAC, every subject carries a clearance label and every object carries a classification label, both drawn from a partially ordered set (a lattice), e.g. ``` Unclassified < Confidential < Secret < Top Secret ``` The classic policy is the Bell–LaPadula model, designed to preserve confidentiality:
- Simple Security Property (no read up): a subject may read an object only if `label(subject) >= label(object)`.
- Star Property (no write down): a subject may write an object only if `label(subject) <= label(object)`.
``` function CHECK_MAC(subject s, object o, right r): if r == READ: return ALLOW if clearance(s) >= classification(o) else DENY if r == WRITE: return ALLOW if clearance(s) <= classification(o) else DENY return DENY ``` No owner discretion enters this check at all — the labels are assigned by a security officer and the reference monitor enforces them unconditionally, even against the object's own creator. A dual model, Biba, flips the inequalities to protect integrity instead of confidentiality (no read-down, no write-up), which is the logic behind things like Windows Mandatory Integrity Control and SELinux's integrity policies.
RBAC: roles as an indirection layer
RBAC inserts a layer of indirection between users and permissions. Instead of a matrix `S × O`, you build two smaller relations: user-role assignment and role-permission assignment, plus optionally a role hierarchy allowing senior roles to inherit junior roles' permissions. ``` structure RBAC_System: UA : set of (user, role) pairs # user assignment PA : set of (role, permission) pairs # permission assignment RH : partial order over roles # role hierarchy (optional) function PERMISSIONS_OF(user u): roles = { r : (u, r) in UA } roles |= { ancestor : role in roles, ancestor senior-to role in RH } return { p : role in roles, (role, p) in PA } function CHECK_RBAC(user u, object o, right r): perms = PERMISSIONS_OF(u) return ALLOW if (o, r) in perms else DENY ``` RBAC systems are typically layered with constraints such as separation of duty (a user cannot simultaneously hold two conflicting roles, e.g., "Payment Initiator" and "Payment Approver") enforced as an additional check at role-assignment time rather than at access-check time.
Complexity & correctness
Space. The full access control matrix requires `O(|S| · |O|)` space if stored densely, which is intractable for realistic systems (millions of users, billions of objects, almost all cells empty). Compressing by column (ACLs) costs `O(Σ_o |nonzero entries in column o|)`; compressing by row (capabilities) costs `O(Σ_s |nonzero entries in row s|)`. Both are proportional to the actual number of granted rights, not to `|S| · |O|`, which is why every real system uses one of these sparse representations rather than the dense matrix.
Time. Checking `CHECK_ACCESS` against an ACL of length `k` for a given object takes `O(k)` time naively, or `O(1)` amortized with a hash map keyed by subject — this is what filesystems and databases actually do. RBAC's `PERMISSIONS_OF` costs `O(|roles held| + |role hierarchy edges reachable|)` to resolve the transitive closure of inherited roles, which is typically precomputed and cached since role hierarchies change far less often than access checks occur; with caching, checks are `O(1)` amortized per request. MAC's label comparison is `O(1)` per check since it is just comparing two totally (or partially) ordered labels.
Correctness of DAC. DAC's "correctness" is intentionally weak: it correctly enforces whatever the current matrix says, but says nothing about whether the matrix itself is safe over time. This is precisely the subject of the HRU safety problem: given an initial matrix and a set of allowed commands (grant, revoke, create, destroy), can a specific right ever leak to a subject that should never have it? Harrison, Ruzzo, and Ullman proved that in the general case (unrestricted commands), this safety question is undecidable — there is no algorithm that, for arbitrary grant/revoke programs, always correctly answers whether a leak is possible. This is a foundational negative result: it tells us DAC systems can never be proven leak-free in general; they can only be tested, audited, and constrained to decidable special cases (e.g., mono-operational commands, for which safety is decidable but NP-hard).
Correctness of MAC (Bell–LaPadula). MAC's correctness argument is a genuine invariant proof. Define the security property `P`: for every allowed (subject, object, read) or (subject, object, write) action, the simple security and star properties hold. The reference monitor checks both properties before allowing any state transition (read or write). By induction on the sequence of operations: if the system starts in a state satisfying `P`, and every transition is only permitted when it preserves `P`, then `P` holds in every reachable state — this is the Basic Security Theorem of Bell–LaPadula. The key insight for why this stops leaks: information can only flow from lower-labeled objects to higher-labeled subjects (via read) and from higher-labeled subjects into higher-or-equal-labeled objects (via write) — so confidential information can never be written down to a less-trusted object, blocking the classic "high process leaks secret into a low file" attack, even if that process is malicious or buggy. Notice, though, that MAC's rigidity is also its cost: it cannot express the fact that a Secret-cleared user might sometimes need to write a genuinely unclassified summary — real deployments need carefully audited trusted subjects who are allowed limited exceptions, which reintroduces exactly the risk MAC was designed to eliminate.
Correctness of RBAC. RBAC's guarantee is different in kind: it does not by itself prevent leaks (a role can still be over-permissioned), but it correctly reduces the administrative problem from `O(|users| · |objects|)` assignments to `O(|users| · |roles| + |roles| · |objects|)`, which is a genuine asymptotic win when `|roles| ≪ |users|`. The correctness property administrators actually care about — separation of duty — is enforced as a static constraint (a user is never assigned two conflicting roles) or a dynamic constraint (a user may hold both roles but cannot activate both in the same session), each checked as an explicit invariant at assignment or activation time, not derived automatically from the model.
Worked example
Consider a small hospital system with subjects `Alice` (nurse), `Bob` (billing clerk), and `Carol` (chief physician, also a security auditor). Objects: `chart-101` (patient record), `invoice-77` (billing record), `audit-log`.
Step 1 — DAC as it might actually be configured (Unix-style ACLs):
| Object | Owner | ACL |
|---|---|---|
| `chart-101` | Carol | Alice:read, Carol:read+write |
| `invoice-77` | Bob | Bob:read+write |
| `audit-log` | root | Carol:read |
Trace: `Alice` requests `write` on `chart-101`. `CHECK` scans the ACL, finds `Alice:read` only — no `write` bit — returns `DENY`. This is DAC working as intended: the owner (Carol) chose not to grant Alice write access. But now suppose Carol, in a rush, runs `GRANT(chart-101, Carol, Alice, {write})` to let Alice update vitals temporarily, and forgets to revoke it next week. Six months later Alice — now demoted to a different ward — still holds stale write access. DAC has no built-in mechanism to catch this; it is a pure permission decay problem, and it is why DAC systems require periodic manual audits.
Step 2 — MAC with Bell–LaPadula, same scenario reframed: suppose `chart-101` is labeled `Secret` (sensitive diagnosis) and `Alice` is cleared to `Confidential`. Even if Carol (the owner-equivalent) wants Alice to read it, the check fails: ``` CHECK_MAC(Alice, chart-101, READ) clearance(Alice) = Confidential classification(chart-101) = Secret Confidential >= Secret ? -> false (Confidential < Secret in the lattice) return DENY ``` No amount of owner discretion overrides this — Carol cannot grant Alice access without a security officer re-labeling either the object or Alice's clearance. This is precisely why MAC is used in settings where even legitimate insiders must not bypass policy.
Step 3 — RBAC, the practical middle ground: define roles `Nurse`, `BillingClerk`, `Auditor`, `ChiefPhysician`, with `ChiefPhysician` senior to `Nurse` in the hierarchy (so a chief physician inherits nurse-level chart access automatically).
| Role | Permissions |
|---|---|
| `Nurse` | `chart-*: read` |
| `BillingClerk` | `invoice-*: read, write` |
| `Auditor` | `audit-log: read` |
| `ChiefPhysician` | (inherits `Nurse`) + `chart-*: read, write` |
`Carol` is assigned `ChiefPhysician` and `Auditor`. Trace: `Carol` requests `read` on `audit-log`. ``` PERMISSIONS_OF(Carol) roles = {ChiefPhysician, Auditor} roles |= ancestors -> {ChiefPhysician, Auditor, Nurse} (via hierarchy) perms = chart-:{read,write} ∪ audit-log:{read} CHECK_RBAC(Carol, audit-log, READ) (audit-log, READ) in perms -> true return ALLOW ``` If Alice is later reassigned from Ward A to Ward B, an administrator changes her role assignment* once — `remove(Alice, NurseWardA); add(Alice, NurseWardB)` — and every permission she holds updates atomically, with no stale per-file entries left behind. This single trace shows exactly why RBAC displaced pure DAC in most enterprise systems: it turns "audit every file" into "audit every role."
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Permission decay in DAC | Owners grant access for a temporary need and forget to revoke it | Stale rights accumulate; violates least privilege silently |
| Confusing "no read up" with "no write up" | Bell–LaPadula's two rules are easy to swap when reasoning quickly | Confidentiality is actually broken while it looks enforced |
| Role explosion in RBAC | Creating a new role for every fine-grained variation instead of using hierarchy/constraints | Hundreds of near-duplicate roles, defeating RBAC's manageability goal |
| Trusting the object owner absolutely (pure DAC) | DAC assumes owners always make correct security decisions | A compromised or careless owner account propagates access uncontrollably |
| Treating role assignment as authorization itself | Assuming holding a role always implies correct current authorization | Ignores need for separation-of-duty checks and periodic role review |
| Forgetting the reference monitor must mediate every access | Caching or bypassing checks for performance | TOCTOU-style gaps where access is checked once but resources change after |
Pioneers & history
Origins · 1 minButler W. Lampson and how this was discovered
Butler Lampson's 1971 paper "Protection" gave the field its first clean formalism: the subject-object-rights matrix that every later model — DAC, MAC, RBAC, capability systems — is ultimately a compressed or constrained version of. He later contributed foundational work on distributed systems, personal computing, and trusted computing, and received the Turing Award in 1992.
Where this matters
Every major operating system implements at least DAC natively: POSIX permission bits and ACLs on Linux/macOS, NTFS ACLs and Windows security descriptors, and access tokens carrying group SIDs. Enterprise identity systems — Active Directory, Okta, AWS IAM, Kubernetes RBAC — are built almost entirely on the RBAC model, assigning permissions to roles like `Developer`, `Billing-Admin`, or `EKS-ReadOnly` rather than to individuals. MAC survives in high-assurance contexts: SELinux and AppArmor enforce mandatory labels on Linux processes regardless of file ownership, and government/military classified systems still run formal multi-level security. Database engines layer row-level and column-level security atop these same primitives (e.g., PostgreSQL row-security policies are effectively DAC-plus-labels). Cloud platforms increasingly blend all three: AWS IAM roles are RBAC, S3 bucket policies are DAC-like ACLs, and government cloud regions add MAC-style classification enforcement. Understanding these models is the direct prerequisite for reasoning about The Principle of Least Privilege (how much access a granted subject should actually receive) and Sandboxing and Isolation (containing what a subject can do even after access is granted).
Ask the tutor: Can you walk through how Bell–LaPadula's Basic Security Theorem proof by induction actually works, step by step?
Is access control the same thing as authentication?
No. Authentication answers "who are you?" — typically via passwords, tokens, or biometrics, as covered under Authentication and Password Security. Access control answers a separate question that comes after identity is established: "given that you are who you claim to be, what are you permitted to do?" A system can have flawless authentication and still be insecure if its access control policy is misconfigured, and vice versa — strong access control cannot compensate for weak authentication, since an attacker who steals credentials inherits whatever access those credentials legitimately hold.
Why doesn't every system just use Mandatory Access Control, since it seems the most rigorous?
MAC is rigorous precisely because it is inflexible, and that inflexibility has real operational costs. Every object needs an accurate label, every subject needs an accurate clearance, and legitimate workflows that cross classification levels (e.g., declassifying a summary) require special trusted exceptions that reintroduce risk. Most organizations do not have adversaries sophisticated enough, or data sensitive enough, to justify this overhead — a hospital or a startup gains far more from RBAC's manageable role structure than from a rigid lattice that would block ordinary daily work.
What exactly is a "role" in RBAC, and how is it different from a group?
A role is a named bundle of permissions tied to a job function (Nurse, Auditor, Billing-Admin), while a group in older systems is often just a bundle of users with no formal permission semantics attached. The key RBAC-specific ideas are the user-role and role-permission relations kept separate, a role hierarchy allowing senior roles to inherit junior permissions, and formal separation-of-duty constraints on which roles may combine. A "group" in Unix, by contrast, is usually just a set of users referenced directly in file permission bits, with no standardized inheritance or constraint model.
How do capability-based systems relate to the access control matrix?
A capability list is the row-compressed representation of the same matrix that ACLs compress by column: instead of an object listing who can access it, a subject holds unforgeable tokens ("capabilities") naming exactly which objects and rights it possesses. Capability systems (like seL4 or historical systems such as Hydra) make it structurally easy to answer "what can this subject do?" but harder to answer "who can access this object?" — the reverse tradeoff from ACL-based DAC, which is easy to audit per-object but requires scanning many objects to answer "what can Alice do everywhere?"
Can DAC, MAC, and RBAC be combined in one real system?
Yes, and most production systems do combine them. A Linux server might use standard DAC (Unix permission bits and ACLs) for ordinary file access, layer SELinux MAC policies on top to contain what compromised processes can do regardless of file ownership, and expose an RBAC layer at the application level (e.g., a web app's admin/editor/viewer roles) governing what actions authenticated users can trigger through the UI. These layers are independent reference monitors; a request must typically pass all of them, so the most restrictive applicable rule wins.
What is the "reference monitor" and why does it matter for correctness?
The reference monitor is the component that intercepts every single access attempt and evaluates the policy — it must be always invoked (no bypass path), tamper-proof (attackers cannot modify its logic or the policy data), and small enough to verify (a large, complex monitor is hard to audit for correctness). Any access control model — DAC, MAC, RBAC — is only as strong as the reference monitor enforcing it; a theoretically perfect Bell–LaPadula policy is worthless if a bug lets a process write directly to disk, bypassing the check entirely.
How does access control relate to least privilege and sandboxing?
Access control models decide the ceiling of what a subject is permitted to do across the whole system; The Principle of Least Privilege is the design discipline of granting the smallest subset of that ceiling actually needed for a task. Sandboxing and Isolation then goes further, physically or logically containing a subject so that even permitted actions cannot escape a boundary (e.g., a browser tab's renderer process). Access control answers "is this allowed at all," while least privilege and sandboxing answer "how do we make sure allowed access still can't be abused."