The clearest sign that role-based access control has failed is not a breach. It is the moment your directory contains more roles than people, and nobody can say what half of them grant.
Every regulated firm I have looked at reaches the same place eventually. Access starts clean: a handful of roles, each mapping to a job. Then a genuine requirement arrives that the roles do not cover — a contractor who needs read access to one team’s records but not another’s, an analyst who can see live data but not the archived cases, a supervisor who can act only during business hours. Rather than change the model, someone mints a new role to encode the exception. Do that for three years and you have the role explosion: hundreds or thousands of roles, most differing from another by a single permission, none safe to delete because nobody remembers why they exist.
Why roles multiply and never contract
The mechanism is structural, not sloppy. A role is a fixed bundle of permissions. Real access requirements are combinatorial — they depend on who the user is, what the resource is, and the context of the request. When the only tool you have is a static bundle, the only way to express a new combination is a new bundle. Roles therefore grow with the product of every attribute you care about, not the sum. Add a second department, a second data classification and a second working pattern, and the number of roles needed to cover the cases multiplies rather than adds.
Free · 4 minutes
Would you survive contact with a determined attacker — or an auditor?
Fourteen questions on access, patching, detection, and recovery — the basics that prevent most real incidents, and the ones most often assumed rather than verified. Banded finding on screen, full sheet by email.
This is why access reviews become theatre. A reviewer signing off a user’s forty roles cannot reason about what those roles collectively permit, so they approve on trust. The entitlement review that was supposed to be a control degrades into a rubber stamp, and the auditor eventually notices.
What attributes change
Attribute-based access control does not bundle permissions. It computes a decision, per request, from three inputs: attributes of the user (department, clearance, role, region), attributes of the resource (owner, classification, status, the department it belongs to), and attributes of the request context (the action, the time, whether the device is managed). A written policy takes those inputs and returns allow or deny. A new requirement becomes a change to one policy, not another hundred roles.
The point that matters for a regulated firm is that the policy is legible. “A user may read a record when it belongs to their department and is not classified restricted” is a sentence a compliance officer can read, challenge and sign off. “Role R-4471” is not. ABAC moves the access model out of an opaque directory and into text you can review, version and test.
The policy, and where it runs
Open Policy Agent is the engine most teams reach for. It is a general-purpose decision engine: your service sends it a JSON document describing the request, OPA evaluates a policy written in its language, Rego, and returns a decision. The service enforces that decision — OPA decides, the application acts. The example below is a working policy under OPA 1.0, in which the if and contains keywords are the default. Keep it as data your service already holds; the attributes should come from a validated identity token and your own resource metadata, not from anything the caller can spoof — which is why the token those user attributes ride on must be validated correctly before you trust a single field in it.
package authz
import rego.v1
# Deny by default. Every grant below is explicit; a hard denial wins.
default allow := false
allow if {
not denied
owner_access
}
allow if {
not denied
same_department_read
}
allow if {
not denied
privileged_role_access
}
# The owner of a record may always act on it.
owner_access if {
input.resource.owner_id == input.user.id
}
# A user may read a record tagged for their own department,
# provided it is not classified restricted.
same_department_read if {
input.action == "read"
input.user.department == input.resource.department
input.resource.classification != "restricted"
}
# Named privileged roles may act, but only from a managed device
# and only while the record is in its active retention window.
privileged_role_access if {
input.user.role in {"case_handler", "supervisor"}
input.context.device_trust == "managed"
input.resource.status == "active"
}
# Hard denial: restricted data never leaves its home region,
# whatever else the grants say.
denied if {
input.resource.classification == "restricted"
input.user.region != input.resource.region
}
Three things are worth noticing. The default is deny, so a request that matches no rule is refused rather than allowed by omission. The grants are independent — owner, department, privileged — and any one is sufficient, which is how you compose access instead of bundling it. And denied overrides every grant, so a data-residency rule cannot be accidentally undone by a permissive role. Encoding a residency constraint as an attribute check, rather than as yet another region-specific role, is the same discipline that turns geofencing into an access-control layer you can actually govern.
Placing the decision point
ABAC is only as good as the point at which the decision is enforced. Run OPA as a sidecar or a library next to each service, and call it in the request path before the service touches data. The decision must sit close enough to the resource that it can see the resource’s attributes — you cannot decide on classification and owner if the enforcement point only knows the URL. Firms that bolt authorisation onto an API gateway alone, far from the data, end up back where they started: coarse decisions that cannot express the fine-grained rules the business actually needs.
Migrating without a big bang
You do not rip out RBAC on a Friday. The workable path is to run OPA in shadow mode first: call it on every request, log what it would have decided, but keep enforcing the existing roles. Compare the two decisions. Where they agree, you have coverage. Where they diverge, you have either a bug in the new policy or an undocumented grant in the old model — both worth knowing before you cut over. Only when the divergence is understood and resolved do you flip enforcement to OPA, service by service.
Keep a small number of coarse roles where they genuinely map to a job; ABAC and RBAC are not mutually exclusive, and input.user.role in the policy above shows how a role becomes just one attribute among several. The goal is not to abolish roles. It is to stop using roles to encode things that are really attributes — department, classification, device trust, region, time.
The part a supervisor will ask about
Because the policy is text, it belongs in version control, with review on change and a test suite that asserts the decisions that matter: restricted data does not cross regions, an unmanaged device cannot exercise a privileged role, a request matching nothing is denied. That is the same policy-as-code discipline that firms already apply to conditional access, and it produces the evidence a supervisor actually wants: not a screenshot of a role list, but a history of who changed the rule, who approved it, and a test proving the control holds. The same legibility that makes the policy reviewable is what makes contextual isolation of privileged data enforceable rather than aspirational.
The role explosion is not a tidiness problem. It is the point at which your access model stops being something anyone can reason about — and an access model no one can reason about is one you cannot defend when someone asks you to.
Free interactive tool
Website compliance checklist
What your site has to do, based on what it actually does
Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.
Everything that applies
Ordered by what to do first: legal requirements you can close quickly, then larger pieces of work, then what is expected rather than required. Not exhaustive, and not a legal audit.
Dated PDF, yours to keep or circulate.
Can you trust the architecture you have?
Architecture diagrams rarely show the reality of how systems actually operate. An independent review establishes what is really there.