Skip to content
KEDBYTE
How Identity Works
Chapter
46

Deciding What You May Do

Part IV · Identity Between Systems|13,958 words|about 61 min read|Volume 4

46.0 What this chapter gives you#

  1. You will be able to state the difference between authentication and authorization in one sentence, and say precisely why the second question is the harder one.
  2. You will be able to draw an access control matrix for a small system, and name the two ways real systems store it instead of storing the matrix.
  3. You will be able to explain what a capability is, describe the confused deputy problem, and say which of the two storage forms suffers from it and why.
  4. You will be able to state the simple security property and the star property of the Bell-LaPadula model, and say exactly how the Biba model inverts them.
  5. You will be able to name the levels of the NIST role-based access control model, and calculate the point at which roles multiply beyond use.
  6. You will be able to write the same authorization rule in XACML, in Rego and in Cedar, and say what each of the three languages is good at and bad at.
  7. You will be able to read a Zanzibar relation tuple aloud, explain what a zookie is for, and describe the new enemy problem it exists to prevent.
  8. You will be able to place the policy decision point, enforcement point, information point and administration point in a real architecture, and say what breaks when each one fails.
  9. You will be able to express one concrete access question in role-based, attribute-based and relationship-based form, and compare the three answers honestly.
  10. You will be able to measure whether least privilege is holding in a system you run, using numbers you can actually collect this week.

Authentication is the question “who are you”. Authorization is the question “what may you do”. The first question has an ending. There is a moment when the password is checked, the passkey signature verifies, the certificate chain builds, and the system can say: this is Priya Nair, and we are as sure as our controls allow. The second question has no such moment. It is asked again on every request, against a set of rules that changes weekly, about resources that were created after the rules were written, on behalf of people whose jobs have changed since anyone last looked.

That asymmetry is the thesis of this chapter. Authentication answers who; authorization answers what, and it is the harder question by a wide margin. It is harder because the answer depends on the resource as well as the person, because it depends on time and context, because it is asked billions of times a day and must be answered in single-digit milliseconds, and because nobody in the organization can tell you what the correct answer is. Ask an engineering team who is allowed to authenticate and they will point at the identity provider. Ask the same team who is allowed to read the Northwind deal folder and you will get a pause, then a list of caveats, then an offer to check.

We will follow one question the whole way through this chapter. Kestrel Advisory is a corporate advisory firm with about nine hundred staff in London, Frankfurt and Singapore. Its document system is called Vault. At 14:32 on Tuesday 18 August 2026, Priya Nair, an associate on the Northwind transaction, clicks on a document called contract-2026-0817, which sits in a folder called northwind, which sits inside a folder called deals. Some piece of software must decide, in the next few milliseconds, whether to send her that file. Everything in this chapter is a different way of making that one decision, and every one of them gets a different answer for a different reason.

Two neighbouring subjects are deliberately left alone here. How Priya’s account came to exist, and how it will be removed when she leaves, belongs to chapter 45, on provisioning and the lifecycle. How the Vault service itself proves who it is when it calls the document store belongs to chapter 47, on machine identity. This chapter is only about the decision.

The plain version#

The hotel with a thousand doors#

Imagine a large hotel. Not a small one: a thousand rooms, plus kitchens, plant rooms, a wine cellar, a safe, a laundry, a server cupboard and a roof.

At the front desk, a clerk checks who you are. You hand over a passport, the clerk looks at your face, and decides that you are the person on the booking. That is one job, it happens once, and when it is finished it is finished.

Then the clerk hands you a plastic card, and a second job begins that never finishes. Every time you walk up to a door and hold the card against the reader, something has to decide whether that door should open for that card, right now. The reader does not care who you are in any deep sense. It cares about one thing: may this card open this door at this moment.

Those are two entirely different jobs. The first is checking identity. The second is deciding permission. A hotel that is excellent at the first and careless at the second is a hotel where a guest can walk into the wine cellar. This happens more often than anyone likes to admit, and in software it happens constantly.

The second job is harder, and it is worth being precise about why. There is one of you and there are twelve hundred doors. The clerk checked your identity once, against one passport, using one procedure. The doors must be checked twelve hundred times, and every one of them has its own reason for opening or not opening. Some open for guests. Some open for guests in a particular wing. Some open only for staff. Some open only for staff during their shift. Some open only for two named people and the fire brigade. The number of rules is not the number of guests. It is closer to the number of guests multiplied by the number of doors, and that number gets very large very fast.

Three ways to write down who may go where#

There are exactly three ways to record the permissions in that hotel, and every access system ever built is one of them or a mixture.

The first way is a grid. In the manager’s office there is an enormous sheet of paper. Down the left-hand side are all the people: every guest, every member of staff, every contractor. Along the top are all the doors, and also all the other things that can be used, like the safe and the till and the laundry chute. In each square of the grid is written what that person may do to that thing: open it, or open and change it, or nothing at all.

The grid is beautifully complete. You can answer any question by looking at one square. It is also useless in practice, because a thousand guests by twelve hundred doors is 1.2 million squares, almost all of them empty, and every time somebody checks in you must fill in a new row.

The second way is a list on each door. Take the grid and cut it into vertical strips, one strip per door. Stick each strip to its door. Now the wine cellar door carries a small list: the sommelier, the head chef, the general manager. When a card is presented, the door reads its own list and decides. Nobody keeps the giant grid. Each door keeps its own column of it.

The third way is a key in your pocket. Take the same grid and cut it into horizontal strips instead, one strip per person. Give each person their strip. Now you carry a set of keys, and each key by itself opens a particular door. The door does not have a list at all. The door simply checks whether the key fits. If you hand your key to a friend, your friend can open the door, and the door will never know the difference.

These three are the same information, stored in three different places. The grid is complete but enormous. The lists on the doors are easy to audit door by door: stand in front of the wine cellar and you can see exactly who may enter. The keys in pockets are fast, because there is no lookup, and they travel: you can lend one, or post one to somebody.

Each has a question it answers badly. With lists on doors, ask “which doors may the sommelier open” and you must walk the whole hotel reading every list. With keys in pockets, ask “who can open the wine cellar” and there is no way to know, because keys have been copied and lent and nobody kept a record. The grid answers both questions instantly, and cannot be built.

The rules the manager is not allowed to break#

So far every rule is somebody’s choice. The manager decides who gets a wine cellar key. A guest can lend their room card to a friend. Permissions flow around according to the wishes of whoever currently holds them.

Now imagine a second kind of rule, one that nobody in the building may override. The hotel is hosting a government conference. The papers in Room 300 are marked “restricted”. The rule is not “the manager decides who may read them”. The rule is: nobody may read a paper marked more secret than their own badge, and nobody carrying a secret paper may leave it in a room marked less secret than the paper. Not even the owner of the paper may give a copy to somebody with a lower badge. The manager cannot make an exception. The rule sits above everybody.

That second kind of rule feels strange in a hotel and completely normal in a defence ministry, which is exactly where it came from. It has two halves and they are not obvious.

The first half is easy: do not read above your level. A person with a “confidential” badge may not read a “secret” paper.

The second half is the one people get wrong: do not write below your level. A person who has just read a “secret” paper may not write anything into a “confidential” notebook, because whatever they write might contain what they just read. They may write upwards, into a more secret notebook, but not downwards. Read up is forbidden; write down is forbidden. It is a one-way valve for information, and the point of it is that it works even if the person is careless, or dishonest, or running a program that has been tampered with.

There is an exact mirror of this rule used for a different purpose. Suppose the concern is not secrecy but correctness: the hotel’s official accounts must never be contaminated by numbers scribbled by a guest. Now the valve turns round. Do not read below your level, because low-quality data would contaminate you. Do not write above your level, because you would contaminate something more trustworthy than yourself. One model protects secrets and lets information flow up. The mirror model protects accuracy and lets information flow down. They are two different rules and using one when you needed the other is a real and common mistake.

Keys by job, not by name#

Back to the ordinary hotel, and to the practical problem that made the grid unmanageable. Nobody wants to write “Priya may open door 214” three hundred times a day.

So the hotel stops writing keys for people and starts writing keys for jobs. There is a housekeeping key, a maintenance key, a night manager key and a kitchen key. When Tomas joins as a porter, nobody decides what doors Tomas may open. Somebody decides that Tomas is a porter, and the porter key does the rest. When Tomas leaves, the key comes back and no door has to be reprogrammed.

This is an enormous improvement and it is why almost every organization does it. It turns a question about a person into a question about a job, and jobs change far more slowly than people do.

It also fails in a very specific way, and the failure is arithmetic rather than philosophy. The first exception arrives when the night porters need a door the day porters do not. So now there are two keys: day porter and night porter. Then the hotel opens a second building, and the keys must not cross between buildings, so there are four: day porter building A, night porter building A, day porter building B, night porter building B. Then the trainees must not enter the safe room, so there are eight. Every distinction anyone ever wants to draw multiplies the number of keys instead of adding to it. Within a few years the hotel has more key types than staff, which is exactly the situation the whole scheme was invented to avoid.

Keys that think, and doors that ask about relationships#

Two more ideas fix that, in two different ways.

The first idea is to stop putting the answer on the key and start putting facts on the key. The card no longer says “porter key”. It says: this person is a porter, they are on shift, they work in building B, they joined more than ninety days ago, and they have completed fire training. The door no longer holds a list of key types. It holds a sentence: open for anyone who is on shift, works in this building, and has completed fire training. Now nobody has to invent a new key type when a new distinction appears. They add a fact, or they change the sentence.

This is more powerful and it costs something real. The door must now be told all those facts at the moment the card is presented, and the facts must be current. If the shift roster server is slow, the door is slow. If the fire training record is a day out of date, the door is wrong in a way that nobody will notice until an audit. And the sentence on the door can be written badly, in ways that are hard to see: three sentences on three doors that contradict each other will not announce themselves.

The second idea is different again, and it is the one that fits documents better than doors. Stop asking about the person’s properties and start asking about the connection between the person and the thing. You may enter room 214 because you are the guest booked into room 214. You may enter the meeting room because you are a member of the party that booked it. You may enter the connecting room because it is joined to yours. The answer is not on the key and not on the door. It is in the shape of the relationships between people, groups and things, and answering it means walking that shape: this person is in that group, that group was granted access to that folder, that folder contains this document.

That third way is how sharing actually works in every document system you have used. A file is shared with a person; a folder is shared with a team; the file is inside the folder; a person joins the team and suddenly can read ten thousand files that nobody explicitly listed them on.

Priya’s Tuesday afternoon, with real values#

It is 14:32 on Tuesday 18 August 2026. Priya Nair, an associate at Kestrel Advisory, clicks a document.

Here are the real values we will carry through the rest of the chapter.

The person: Priya Nair, account name pnair, employed since 3 March 2025, currently active, department Deals, grade Associate, member of the Northwind deal team, working from the London office network, on a company-managed laptop.

The thing: a document with the identifier contract-2026-0817, created on 17 August 2026 by Tom Okafor, a partner. It sits in a folder called northwind. That folder sits inside a folder called deals. The document is marked as commercially sensitive.

The action: read it.

Now watch how the same question gets three different treatments.

If the hotel uses keys by job, Vault asks whether Priya’s job grants reading of deal documents. It does: associates read deal documents. Vault opens the file. It has not asked whether she is on the Northwind team, so she can also read the folder of a rival transaction two doors down, which is a problem that will not be discovered until somebody looks.

If the door reads facts, Vault checks a sentence: allow reading when the reader’s deal list includes the document’s deal, and the reader is employed, and the connection comes from the office network, and the laptop is managed. All four are true at 14:32, so the file opens. At 14:33 Priya moves to a coffee shop, the network fact changes, and the same click produces a refusal.

If the system walks relationships, Vault asks a different question entirely: is there a path from Priya to this document. There is: Priya is a member of the Northwind team, the Northwind team is a viewer of the northwind folder, and this document’s parent is that folder. Three hops, and the answer is yes.

Three mechanisms, three yeses on this particular afternoon, three completely different failure modes on a different afternoon. Learning to see which mechanism a system is using, and what it therefore cannot express, is most of the skill in this subject.

Where the plain version stops being true#

A door is not the unit of a real decision#

The hotel analogy has one door and one verb: open. Real systems have many verbs, and the verbs are where the interesting mistakes live.

Priya may read contract-2026-0817. May she download it, which is different from reading it on screen? May she print it? May she rename it, which does not change its contents but breaks everyone else’s links? May she move it out of the northwind folder into her personal folder, which quietly removes it from the reach of every rule attached to that folder? May she share it with somebody outside Kestrel? May she delete it? May she see that it exists without opening it, which matters, because the mere existence of a document called contract-2026-0817 in a folder called northwind tells a competitor something.

Each of those is a separate decision, and a system that models permission as a single “open” is a system in which somebody will eventually confuse read with share.

The unit is smaller than a document too. Priya may be allowed to see a client record but not the field holding the bank details. She may be allowed to see a list of a hundred deals but only the names, not the values. A permission model that can only talk about whole objects will be worked around by application programmers, and the workarounds are where the leaks are.

The honest version: authorization is a decision about a triple, not a pair. Who, what action, on which thing, in which context. Every model in this chapter is a way of writing down rules about that triple, and every model that quietly drops one element of it produces a class of bug named after the element it dropped.

The grid is too big to exist, and it is not just about size#

The manager’s grid was described as impractical because it is large. That is true and it is the least of it.

At Kestrel, the grid would have about 900 people down the side and, as of August 2026, roughly 41 million documents across the top. That is 37 billion squares. But documents are created continuously, so the grid is not merely large, it is growing while you read it. A row cannot be filled in for a person until the objects exist, and objects are created by people, at a rate of thousands a day.

The deeper problem is a mathematical one, and it was proved fifty years ago. Suppose you have the grid, and the rules by which the grid itself may be modified: who may grant what to whom. A natural question is: can this configuration ever leak a particular permission to a particular person, through some sequence of legal grants. In 1976, Michael Harrison, Walter Ruzzo and Jeffrey Ullman showed that in the general case that question is undecidable. Not hard, not slow: undecidable, in the sense that no algorithm can answer it for all systems. You can restrict the model until the question becomes answerable, and much of the history of this field is the search for restrictions that are useful and still analyzable.

The honest version: you can never fully answer “could this system ever allow that” for an arbitrary permission system. You can only answer it for a deliberately limited one. This is the strongest single argument for choosing a policy language that gave something up on purpose.

Keys get copied, and helpful staff get tricked#

The key in the pocket looked like the fastest of the three storage schemes, and it is. It also has a specific and much-misunderstood failure, and a specific and much-misunderstood strength.

The failure: a key that is stolen or lent works for the thief, and the door cannot tell. There is no list to consult and no name to check.

The strength is subtler and it is the reason people keep coming back to keys. Consider a member of staff who does jobs for other people, and who has a master key of their own because their job requires it. A guest asks them to fetch a newspaper from the reading room. The staff member uses their master key, quite properly. Now the guest asks them to fetch the contents of the safe. The staff member has the authority to open the safe, and they are trying to be helpful, and they cannot see that the request came from somebody who has no right to make it. They open the safe on behalf of a person who could not have opened it themselves.

That is the confused deputy, described by Norman Hardy in a short paper in ACM SIGOPS Operating Systems Review in 1988, and it is not a hotel curiosity. It is the shape of most privilege escalation bugs in real software. A service with broad rights performs work on behalf of a caller with narrow rights, and forgets to check whose rights should apply. Server-side request forgery is a confused deputy. Many injection attacks end as confused deputies. The reason capability people argue for keys is that if the staff member had been handed the guest’s own key for that specific errand, rather than using their own master key, the trick would not work.

The honest version: lists on doors invite confused deputies, because the door checks the identity of whoever is standing in front of it rather than the authority of whoever asked. Keys resist confused deputies and are hard to revoke. Neither is simply better. Almost every real system uses lists for storage and passes short-lived keys around inside itself.

The list on the door answers the wrong question for an auditor#

Stand in front of the wine cellar door and read its list: three names. Excellent. Now the auditor asks the question auditors always ask, which is the reverse: what can the sommelier reach.

There is no efficient way to answer that from lists on doors. You must read every list in the building. In software this is exactly the situation: file systems, object stores and document systems store permissions with the object, so “who can read this file” is a fast query and “what files can this person read” is a full scan of the estate.

This is not a minor inconvenience. It is the reason access reviews are painful, the reason leavers keep access nobody noticed, and the reason organizations buy an entire category of product whose only job is to build the reverse index that the storage model does not provide. It is also the reason relationship-based systems added a specific operation for expanding the set of things a person can reach, and then had to build a separate index to make it fast.

Nothing about the answer stays true#

The hotel card either opens the door or it does not, and the answer is stable for the length of your stay. Real authorization decisions decay in at least four ways at once.

The permission changes. Priya leaves the Northwind team at 16:10 and should lose access at 16:10.

The object changes. The document is moved into a different folder, and the rules that apply to it change without anybody touching the rules.

The context changes. She moves from the office network to a train.

And the decision itself is often cached. If the system put her group memberships inside a token at sign-in, that token is a photograph of the truth at 14:32, and it keeps being believed until it expires. This is why a token with a one-hour lifetime is, in effect, a promise that removal of access may take up to an hour.

The honest version: there is always a window between a permission changing and every enforcement point behaving as though it changed. Good systems make that window short, measure it, and publish it. Bad systems pretend it does not exist. If your answer to “how quickly does revocation take effect” is not a number with a unit, you do not have an answer.

Rules conflict, and something must decide who wins#

In the hotel, one door has one list. In a real system, the same request is covered by four rules written by three teams in two decades.

Kestrel’s Vault has a rule that says associates may read deal documents. It has a rule saying documents marked commercially sensitive may only be read by the deal team. It has a rule saying nothing may be read from an unmanaged device. It has a rule saying the audit service may read everything. On Priya’s click, three of those apply and one of them, on a different afternoon, will disagree with the others.

Every serious authorization system therefore needs a stated procedure for combining conflicting results, and the choice of procedure is a policy decision with real consequences. Deny wins over allow is the common choice and it is not the only one. Some systems take the first rule that applies in written order. Some treat an error while evaluating a rule as a deny, some as an allow, and the difference between those two decisions has caused outages on one side and breaches on the other.

The honest version: “deny by default” is only half a policy. You also need to say what happens when two rules disagree, what happens when a rule cannot be evaluated because an attribute is missing, and what happens when the decision service itself is unreachable. Most incident reports in this area are about the third case.

Most access decisions never reach the policy engine at all#

This is the largest gap between the tidy picture and the world, and it deserves to be stated bluntly.

Draw the beautiful architecture: policy written centrally, enforced consistently, decisions logged. Then read the actual code of the Vault application. There is a database query that filters documents by the current user’s team. There is an if statement in a controller that checks whether the requester is the owner. There is an admin page that was never wired to the policy service because it was only for two people. There is a report generator that runs as a service account with rights over everything, because that was the quickest way to make the nightly job work.

Every one of those is an authorization decision. None of them is in the policy. When a breach happens, it is far more often in that undocumented half than in the policy that everybody reviewed.

The honest version: an authorization architecture is not the set of rules you wrote. It is the set of code paths that reach data, and the fraction of them that consult the rules. That fraction is a number you can estimate, and almost nobody does.

The technical version#

The access control matrix and its two projections#

The formal object underneath every access control system is the access control matrix: rows for subjects, columns for objects, and in each cell the set of rights that subject holds over that object. Butler Lampson set it out in a paper called “Protection”, presented at the 5th Princeton Symposium on Information Sciences and Systems in March 1971 and reprinted in ACM Operating Systems Review volume 8, number 1, January 1974, pages 18 to 24. Graham and Denning extended it the following year with rules for how the matrix itself may be changed.

A tiny slice of Kestrel’s matrix looks like this.

subject      doc:c-0817   folder:northwind   payroll.csv
-----------  -----------  -----------------  -----------
user:pnair   read         read               -
user:tokafor read,write   read,write,share   -
user:hchen   -            -                  read,write
svc:auditbot read         read               read

Two things are true of this matrix and of every other. It is almost entirely empty, and nobody stores it. What systems store is one of its two projections.

Take a column and store it with the object: that is an access control list, a list of subjects and rights attached to the thing being protected. Take a row and store it with the subject: that is a capability list, a set of unforgeable tokens each of which names an object and the rights over it. Capabilities in this technical sense date to Jack Dennis and Earl Van Horn’s paper “Programming Semantics for Multiprogrammed Computations” in Communications of the ACM volume 9, number 3, March 1966, pages 143 to 155.

You have used both today without noticing. A Unix file’s mode bits are a compressed access control list with exactly three entries: owner, group, other. The richer per-user lists that getfacl and setfacl manipulate on Linux come from the withdrawn POSIX.1e draft, which is a good example of something everybody implements that never became a standard. A Windows security descriptor holds a discretionary list of access control entries, and the deny entries are placed before the allow entries in what Microsoft calls canonical order, which is an implementation detail with security consequences: an out-of-order list can produce an allow that the administrator did not intend.

On the capability side, a Unix file descriptor is a genuine capability. It is unforgeable by user code, it names an open object with specific rights, and it can be passed to another process over a Unix domain socket. A signed URL to an object store is a capability. A bearer token is a capability. Every one of them has the property that possession is sufficient and the object never learns who is holding it.

The two projections answer opposite questions cheaply, and the table below is worth memorizing because architects choose without noticing that they are choosing.

View Stored with Cheap question
Matrix Nowhere, it is a model Any question
Access control list The object Who can reach this
Capability The subject What can this reach

The most instructive live example of both projections in one system is AWS. An identity-based policy is attached to a user or role and is a matrix row. A resource-based policy, such as an S3 bucket policy or a KMS key policy, is attached to the object and is a matrix column. Both are consulted for a single request, and the documented evaluation logic is that the request is denied unless something allows it and nothing explicitly denies it. Explicit deny always wins. That rule is the combining algorithm of AWS, and it is not the only possible one.

There is one more classical result to keep in mind. In “Protection in Operating Systems”, Communications of the ACM volume 19, number 8, August 1976, Michael Harrison, Walter Ruzzo and Jeffrey Ullman proved that the general safety question for the matrix model, meaning “can right r ever leak to subject s”, is undecidable. Restricted versions are decidable, and every analyzable policy language on the market today buys its analyzability by imposing exactly such restrictions.

Discretionary and mandatory control, and the two lattice models#

The terms discretionary and mandatory are not vague adjectives. They come from the United States Department of Defense Trusted Computer System Evaluation Criteria, the Orange Book, published as DoD 5200.28-STD in December 1985, which required both as separate features at different evaluation classes.

Discretionary access control means the owner of an object may pass rights to others at their discretion. Every file system you use daily is discretionary. Tom Okafor created contract-2026-0817, so Tom may share it. That is the model’s strength and its whole weakness: a single well-meaning share defeats every control above it, and a program running as Tom can share on his behalf without asking him.

Mandatory access control means the system enforces a rule that the object owner cannot override. The classical form uses security labels arranged in a lattice, and there are two canonical models that point in opposite directions.

The Bell-LaPadula model, developed by David Elliott Bell and Leonard LaPadula at MITRE and set out in its unified form in MTR-2997 Revision 1, also numbered ESD-TR-75-306, dated March 1976, protects confidentiality. It has two rules that matter and one that is often forgotten.

The simple security property says a subject may observe an object only if the subject’s clearance dominates the object’s classification. No read up.

The star property says that a subject with simultaneous access to two objects may not write into an object whose level is dominated by an object it can read. No write down. The purpose is not to stop a person misbehaving; it is to stop a program acting for that person from copying a secret paragraph into an unclassified file.

The discretionary security property says that on top of both, the ordinary owner-granted permissions must also allow the access. Mandatory rules restrict; they do not grant.

The Biba model, described by Kenneth Biba in MITRE report MTR-3153, also numbered ESD-TR-76-372, dated April 1977, protects integrity and is the exact mirror. Its simple integrity property forbids reading down: a high-integrity process may not read low-integrity data. Its integrity star property forbids writing up: a low-integrity process may not write to a high-integrity object. Confidentiality lets information flow upwards. Integrity lets it flow downwards. Choosing the wrong one gives you a system that is careful about the wrong direction.

These models are frequently described as historical. They are not. Windows has had Mandatory Integrity Control since Windows Vista in 2007, with integrity levels of low, medium, high and system, and the browser sandbox depends on it; that is Biba, in production, on a billion machines. SELinux type enforcement is mandatory in the strict sense that the policy is compiled and a root process cannot simply grant itself a new type transition. In cloud accounts, an AWS service control policy behaves mandatorily with respect to the account administrator: it can only remove permissions, and no identity policy inside the account can win against it.

Two later models belong here because they describe what commercial organizations actually want. Clark and Wilson, at the IEEE Symposium on Security and Privacy in 1987, argued that commercial integrity is not a lattice at all but a matter of well-formed transactions and separation of duty: users may not change data directly, only through certified procedures, and no single user may run every step. Brewer and Nash, at the same symposium in 1989, described the Chinese Wall policy, in which past access determines future access: once an adviser has worked on one company in a conflict-of-interest class, access to its competitors in that class is closed.

The Chinese Wall model is precisely Kestrel’s real problem. Priya should not be able to read the folder of the transaction on the other side of the same market, and the rule cannot be written in advance, because it depends on what she has already touched. Notice that none of the classical mandatory models expresses it comfortably, and that role-based access control cannot express it at all.

Year Model or document One-line contribution
1966 Dennis and Van Horn Capabilities
1971 Lampson, Protection The matrix
1975 Saltzer and Schroeder Least privilege
1976 Harrison, Ruzzo, Ullman Safety undecidable
1976 Bell and LaPadula No read up, no write down
1977 Biba The integrity mirror
1985 TCSEC, Orange Book DAC and MAC named
1987 Clark and Wilson Duty separation
1989 Brewer and Nash Chinese Wall
1992 Ferraiolo and Kuhn RBAC

RBAC: what the NIST model actually standardized#

Role-based access control was given its modern form by David Ferraiolo and Richard Kuhn in a paper presented at the 15th National Computer Security Conference in 1992, pages 554 to 563. Ravi Sandhu, Edward Coyne, Hal Feinstein and Charles Youman published the influential RBAC96 family in IEEE Computer volume 29, number 2, in February 1996. The two lines were merged into a proposed standard by Sandhu, Ferraiolo and Kuhn at the 5th ACM Workshop on Role-Based Access Control in 2000, and published in full as “Proposed NIST Standard for Role-Based Access Control” in ACM Transactions on Information and System Security volume 4, number 3, August 2001, pages 224 to 274.

It became a standard in the formal sense: ANSI INCITS 359, adopted on 11 February 2004 and revised as INCITS 359-2012. When somebody says their product implements RBAC, that is the document they are implicitly claiming to follow, and almost none of them follow all of it.

The model is defined in levels, and the levels are cumulative.

Core RBAC defines users, roles, permissions and sessions. A permission is an approval to perform an operation on an object. Users are assigned to roles, permissions are assigned to roles, and no permission is ever assigned to a user directly. Sessions are the part everybody forgets: a user activates some subset of their assigned roles for a session, so that a person who holds an administrative role need not carry it all day.

Hierarchical RBAC adds inheritance. A senior role inherits the permissions of a junior role. The standard distinguishes general hierarchies, which allow multiple inheritance, from limited hierarchies, which restrict it to a tree, and the distinction exists because multiple inheritance makes the reverse question harder to compute.

Static separation of duty adds constraints on assignment: a user may not be assigned both of a pair of conflicting roles at all.

Dynamic separation of duty adds constraints on activation: a user may hold both roles, but may not have both active in the same session. Payment initiator and payment approver is the canonical pair.

Two properties of the model are frequently misunderstood. There are no negative permissions in the NIST model: denial is the absence of a grant, not the presence of a prohibition. And roles are not groups. A group is a set of users; a role is a set of permissions with a set of users attached and an activation model on top. Directory groups can implement roles, which is why the confusion is durable; chapter 44 covers how those groups behave inside a directory.

Kubernetes is the cleanest widely deployed implementation of the core level, made generally available in Kubernetes 1.8, released on 29 September 2017. It has roles, cluster roles, bindings and verbs, and it has no deny rules at all, exactly as the model prescribes.

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: vault-prod
  name: deal-associate
rules:
- apiGroups: [""]
  resources: ["documents"]
  verbs: ["get", "list"]

The binding that attaches Priya to it is a separate object.

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: vault-prod
  name: pnair-deal-associate
subjects:
- kind: User
  name: pnair
roleRef:
  kind: Role
  name: deal-associate
  apiGroup: rbac.authorization.k8s.io

A warning about a word. An AWS IAM role is not a role in the NIST sense. It is an identity with policies attached that other principals may assume, which makes it closer to a temporary account than to a job title. Systems that use the word in both senses inside one architecture generate a specific and tedious class of design meeting.

Role explosion, counted#

Role-based access control fails through arithmetic, not through theory, and the arithmetic is worth doing once with real values.

Kestrel starts sensibly. There are four job families in the deal business, and three grades. That is twelve roles, and twelve roles is a catalogue a human being can hold in their head.

Then the requirements arrive, each of them individually reasonable. Deal teams must be separated from each other, because of the conflict-of-interest rule; there are twelve live transactions. Regional data rules mean Frankfurt material may not be visible from Singapore; there are three regions. Documents marked commercially sensitive need a narrower audience than ordinary ones; that is two tiers. Reading and editing must be separable, because associates draft and partners approve; that is two access levels.

Each requirement multiplies.

Requirement added Factor Roles needed
Job family and grade 12 12
Per-deal separation 12 144
Regional separation 3 432
Sensitivity tier 2 864
Read versus edit 2 1,728

One thousand seven hundred and twenty-eight roles for nine hundred people. That is the disease, and its name is role explosion: the number of roles grows as the product of the distinctions the business wants to draw, while the number of people grows linearly or not at all.

The symptoms are diagnosable without any product. Count the roles and the users; if the ratio approaches or exceeds one, you have role explosion. Count roles with exactly one member; those are people, not jobs, wearing a role’s clothing. Count roles not activated in ninety days; those are permissions nobody is watching. Count the roles created in the last quarter against the joiners in the same quarter.

The structural reason for the explosion is that a role is a set of permissions fixed in advance, and every one of those five requirements is a property of the request rather than of the job. Which deal this document belongs to is a property of the document. Which region the reader is sitting in is a property of the moment. Whether the reader is on the deal team is a property of a relationship. RBAC can only express these by pre-computing every combination into a role name, which is precisely what the multiplication above does.

Richard Kuhn, Edward Coyne and Timothy Weil set out the way forward in “Adding Attributes to Role-Based Access Control”, IEEE Computer volume 43, number 6, June 2010, pages 79 to 81. They describe three combinations: dynamic roles, where attributes decide which roles a user gets; attribute-centric, where roles become just another attribute; and role-centric, where roles grant a maximum set of permissions and attributes can only narrow it. The role-centric arrangement is the one most large organizations converge on, because it keeps the auditable upper bound that auditors like and adds the flexibility that engineers need.

ABAC: attributes, policies, and the cost of evaluating them#

Attribute-based access control is defined in NIST Special Publication 800-162, “Guide to Attribute Based Access Control (ABAC) Definition and Considerations”, published in January 2014 and last updated on 25 February 2019, by Vincent Hu, David Ferraiolo, Richard Kuhn, Adam Schnitzer, Kenneth Sandlin, Robert Miller and Karen Scarfone. Its definition is worth stating carefully: authorization to perform a set of operations is determined by evaluating attributes of the subject, attributes of the object, the requested operations, and in some cases environment conditions, against policy or rules that describe the allowable operations.

Four attribute sources, then. Subject, object, action, environment. The Kestrel rule for our running question uses all four.

PERMIT read
  WHEN subject.employment_status = "active"
   AND subject.deal_codes CONTAINS resource.deal_code
   AND resource.sensitivity <= subject.clearance
   AND environment.network = "corporate"
   AND environment.device_managed = true

Every distinction that produced a new role in the previous section becomes a term in one rule here. The catalogue of 1,728 roles collapses to a handful of policies, and a thirteenth transaction does not require anybody to create anything: the new document carries deal_code = "cirrus", the new team members carry it in their deal_codes attribute, and the rule already covers it. That is the real attraction of ABAC and it is genuine.

It buys that flexibility with three costs that are equally genuine.

The first cost is attribute supply. The decision cannot be made until every attribute in the rule is available. employment_status comes from the human resources system. deal_codes comes from the deal management system. device_managed comes from the device management platform. sensitivity comes from the document itself. Each source has its own latency, its own outage schedule and its own idea of freshness. A policy is only as correct as the least fresh attribute it consults, and SP 800-162 devotes substantial space to attribute management and provenance for exactly this reason. When somebody tells you their ABAC deployment failed, the cause is nearly always here.

The second cost is evaluation. This is where the combinatorial problem lives, and it has two distinct faces.

The face people notice is per-request work. Each policy in the policy set must be tested for applicability, and each applicable rule may require attribute fetches. Cost grows with the number of policies times the cost of the attributes they mention.

The face people miss is the size of the input space, which is what makes ABAC hard to test rather than merely slow. Our small rule uses five terms. Suppose employment_status has 4 values, deal_codes is any subset of 12 live transactions, sensitivity and clearance have 3 levels each, network has 4 values and device_managed is boolean. The number of distinct request shapes is 4 times 4,096 times 3 times 3 times 4 times 2, which is 1,179,648. That is one rule with five terms. A policy set of forty rules over thirty attributes cannot be enumerated by any test suite, ever. You cannot achieve coverage by testing; you can only achieve confidence by analysis, which is the argument for policy languages that can be reasoned about mechanically.

The third cost is that the explosion does not disappear; it changes address. Roles stop multiplying and policies start. Two rules written eighteen months apart by different teams overlap in a corner of the input space that neither author considered, and unlike two roles, two rules do not sit in a list where a human can see them side by side.

There is a second standardized approach to attribute-based control that deserves a mention because its computational properties differ. Next Generation Access Control, developed at NIST, models policy as a graph of assignments and associations rather than as a rule set, which lets it answer per-object and per-user enumeration questions directly instead of by search. NIST Special Publication 800-178, published on 3 October 2016 by David Ferraiolo, Ramaswamy Chandramouli, Vincent Hu and Richard Kuhn, compares the two standards head to head and is the fairest short treatment of the trade-off in print. NGAC was published through INCITS [UNVERIFIED: the exact INCITS standard number and year for the NGAC functional architecture].

ReBAC and Zanzibar: the graph, the tuple and the zookie#

Relationship-based access control asks a different question from both of the above. Not “what job does this person hold” and not “what attributes does this request carry”, but “is there a path through the relationship graph from this subject to this object with this permission”.

The system that made this mainstream is Zanzibar, described in “Zanzibar: Google’s Consistent, Global Authorization System” by Ruoming Pang, Ramon Caceres, Mike Burrows and colleagues, presented at the USENIX Annual Technical Conference in Renton, Washington, in July 2019. It is the authorization system behind Calendar, Cloud, Drive, Maps, Photos and YouTube, and the figures in the paper are the reason everybody copied it.

Measure Value in the 2019 paper
Relation tuples stored Over 2 trillion
Client namespaces Over 1,500
Server fleet Over 10,000 servers
95th percentile check Under 10 milliseconds
Availability, 3 years Over 99.999 percent

The data model is one idea and it is very small. Everything is a relation tuple, written as an object, then a hash, then a relation, then an at sign, then a user or a userset.

doc:contract-2026-0817#parent@folder:northwind#...
folder:northwind#viewer@group:northwind-team#member
group:northwind-team#member@user:pnair

Read the second line aloud: the folder northwind has a viewer relation to the members of the group northwind-team. The right-hand side there is a userset, meaning “every user who stands in the member relation to that group”, rather than a single user. The trailing #... on the first line is the paper’s notation for a tuple whose userset is not meaningful, used when a tuple exists only to record structure, in this case that the document sits inside the folder.

The rules that turn stored tuples into effective permissions live in a namespace configuration, and there are exactly three composable primitives: _this, meaning the users stored directly in this relation; computed_userset, meaning the users of another relation on the same object; and tuple_to_userset, meaning follow a relation to another object and take a relation there. The Kestrel document namespace looks like this.

name: "doc"

relation { name: "owner" }

relation {
  name: "editor"
  userset_rewrite {
    union {
      child { _this {} }
      child { computed_userset { relation: "owner" } }
    }
  }
}

relation {
  name: "viewer"
  userset_rewrite {
    union {
      child { _this {} }
      child { computed_userset { relation: "editor" } }
      child { tuple_to_userset {
        tupleset { relation: "parent" }
        computed_userset { relation: "viewer" }
      } }
    }
  }
}

Those twenty lines say: owners are editors, editors are viewers, and viewers of a document’s parent folder are viewers of the document. Inheritance, without a single rule about Priya or Northwind. The check for our running question is Check(user:pnair, viewer, doc:contract-2026-0817), and it is answered by walking three hops: document to folder by parent, folder to group by viewer, group to user by member.

Two engineering problems dominate the rest of the paper, and both are instructive.

The first is deep and wide groups. If a group contains groups which contain groups, a check becomes a graph search with unpredictable depth, and a group with millions of members makes the search wide as well. Zanzibar answers this with a separate index called Leopard, which flattens group-to-group and member-to-group relations into denormalized sets that can be intersected quickly with skip lists. The lesson generalizes: relationship systems are fast when the graph is shallow and need an index the moment it is not.

The second is consistency, and this is the part that most Zanzibar imitations get wrong. Consider what the paper calls the new enemy problem. Alice removes Bob from a folder’s access list, and then moves a sensitive document into that folder. If the check for Bob’s later read is evaluated against a snapshot of the access lists taken before the removal, Bob reads the document. Nothing was misconfigured; the two changes were simply observed out of order. The mirror case is the same thing with the order of the two writes reversed.

Zanzibar’s answer is the zookie: an opaque token that encodes a globally meaningful timestamp, handed back to the client when content changes and presented on later checks. The rule is that a check presented with a zookie must be evaluated at a snapshot no earlier than that timestamp, which guarantees that the check observes every access change that causally preceded the content change. Underneath, this rests on Spanner’s globally consistent timestamps. In normal operation Zanzibar serves checks from a local replica that is a few seconds behind, with replication heartbeats every eight seconds and a default staleness bound of about ten seconds, and only pays for a cross-region round trip when a zookie demands something more recent.

The honest version: a relationship system without something like a zookie is not merely slightly stale, it is vulnerable to a specific ordering attack, and the window is exactly the replication lag. If you are evaluating a Zanzibar-inspired product, that is the question to ask first.

Three open implementations exist as of August 2026. SpiceDB, from Authzed, is the closest to the paper and implements zookies under the name ZedTokens. Ory Keto was the earliest open re-implementation. OpenFGA, which grew out of work at Auth0 and is now part of Okta, was accepted into the Cloud Native Computing Foundation as a sandbox project on 14 September 2022 and moved to incubating status on 28 October 2025.

Where relationship-based control stops is context. The graph knows that Priya is on the Northwind team. It does not naturally know that she is on a train, on an unmanaged laptop, at 03:00. Implementations have bolted attribute conditions onto tuples to close that gap, which quietly turns them into hybrid systems, and the hybrid is where most large deployments end up.

PDP, PEP, PIP and PAP: four components, four places to fail#

The four-letter vocabulary comes from the XACML specification, which defines each actor precisely. The policy enforcement point is the entity that performs access control by making decision requests and enforcing the decisions. The policy decision point evaluates applicable policy and renders an authorization decision. The policy information point acts as a source of attribute values. The policy administration point creates policy. XACML adds a fifth, the context handler, which converts a native request into the canonical form the decision point expects.

        +-----------------------+
        |  PAP: where policy    |
        |  is written, reviewed |
        |  and versioned        |
        +-----------+-----------+
                    | deploy
                    v
   request   +------+--------+   fetch     +---------+
  --------> |      PDP       | <---------> |   PIP   |
            |  evaluates     |  attributes | HR, MDM |
            +------+---------+             +---------+
                   ^   | decision
        ask        |   v
            +------+---------+
  user ---> |      PEP       | ---> resource
            |  enforces      |
            +----------------+

Where each one physically lives is the design decision that determines the behaviour of the whole system.

The enforcement point must sit on every path to the data. That is the entire requirement and it is the one most often violated. If the web application enforces and the nightly report generator does not, the report generator is your access control system. In practice enforcement points appear in API gateways, in service meshes, in application middleware, in database row-level security and inside individual controllers, and the fragmentation is why the same rule gets implemented five times with four different bugs.

The decision point can be embedded, co-located or central, and the three choices trade the same two currencies.

An embedded decision point is a library inside the application process. Cedar and Open Policy Agent both ship as libraries. Cost: microseconds. Weakness: every service must be redeployed to change behaviour if policy is compiled in, so policy is normally pulled from a bundle service instead.

A co-located decision point is a sidecar on the same host, which is how Open Policy Agent is most often deployed. Cost: a loopback call, a fraction of a millisecond. Weakness: N copies of the policy and the attribute cache, and a distribution problem.

A central decision point is a service. Zanzibar is one, and so are the managed services built on Cedar. Cost: a network round trip, and physics sets the floor. Light in fibre travels about 200,000 kilometres per second, so every kilometre of path costs about 5 microseconds each way. London to Frankfurt and back is roughly 1,300 kilometres of fibre, which is about 6.5 milliseconds of round trip before any software runs. A central decision point in one region and an application in another cannot be fast, and no amount of engineering will change that.

The information point is where freshness is decided. Every attribute has an age. A decision point that caches group membership for five minutes has, by construction, a five-minute revocation window, and that is a number that belongs in the service documentation.

The administration point is where governance lives, and the modern answer is unglamorous and effective: policy in a version control repository, changes by pull request, tests in continuous integration, deployment as a signed bundle. That converts “who changed the rule” from an investigation into a git log.

Then there is the failure question, and it has no universally right answer. If the decision point is unreachable, does the enforcement point deny or allow? Deny means a decision service outage is a total outage. Allow means a decision service outage is a total breach. The usual compromise is a short-lived local cache of recent decisions with a hard expiry, which converts the choice into a bounded window, plus an explicit break-glass path that is logged loudly; chapter 45 covers the governance of break-glass accounts.

One protocol is worth naming because it is new and it addresses the interoperability gap between enforcement points and decision points from different vendors. The OpenID Foundation’s AuthZEN working group has published the Authorization API, whose first draft appeared on 6 September 2024 and which had reached final status by August 2026 [UNVERIFIED: the exact date the Authorization API 1.0 final specification was approved]. It is deliberately small: a POST to an evaluation endpoint, four JSON objects in and a boolean decision out.

POST /access/v1/evaluation HTTP/1.1
Content-Type: application/json

{
  "subject":  { "type": "user", "id": "pnair" },
  "action":   { "name": "read" },
  "resource": { "type": "document",
                "id": "contract-2026-0817" },
  "context":  { "network": "corporate" }
}

The response carries a decision and optional context, which is where reasons, advice and step-up instructions can travel.

HTTP/1.1 200 OK
Content-Type: application/json

{ "decision": true }

XACML has a richer answer shape that is worth borrowing conceptually even if you never write XACML: alongside the decision it can return obligations, which the enforcement point must carry out, and advice, which it may ignore. “Permit, but redact the bank account field” and “Permit, and write this to the audit log” are obligations. A system whose decision service can only say yes or no pushes all such logic back into application code, where it is invisible.

The same rule in XACML, Rego and Cedar#

Three policy languages dominate, they were designed twenty years apart, and the differences between them are differences of philosophy rather than syntax. We will write one rule in all three: permit a read when the reader is employed, the document’s deal code is one of the reader’s deal codes, the document’s sensitivity does not exceed the reader’s clearance, the connection comes from the corporate network, and the device is managed.

Language Origin Status in August 2026
XACML 3.0 OASIS standard Errata 01, July 2017
Rego Open Policy Agent OPA 1.0, December 2024
Cedar AWS, open sourced Version 4.5, April 2026

XACML, the eXtensible Access Control Markup Language, became an OASIS Standard at version 3.0 on 22 January 2013, and the current text is the version incorporating Approved Errata 01, dated 12 July 2017. It is XML, it is verbose, and it is also the most carefully specified of the three: it defines the decision values Permit, Deny, NotApplicable and Indeterminate, three extended indeterminate values that record which decision could have resulted, and a set of combining algorithms including deny-overrides, permit-overrides, first-applicable, only-one-applicable, deny-unless-permit and permit-unless-deny, in ordered and unordered forms.

The listing below abbreviates the OASIS identifier prefixes so that every line fits the page. The real document carries the full URNs.

listing            real identifier prefix
-----------------  ------------------------------------
oasis:xacml:       urn:oasis:names:tc:xacml:
:rca:              :rule-combining-algorithm:
:fn:               :function:
:ac:               :attribute-category:
:sc:               :subject-category:
xs:string          http://www.w3.org/2001/XMLSchema#string
<Policy PolicyId="kestrel:deal-read"
   Version="1.0"
   RuleCombiningAlgId="oasis:xacml:3.0:rca:deny-overrides">
  <Target/>
  <Rule RuleId="permit-deal-read" Effect="Permit">
    <Target>
      <AnyOf><AllOf>
        <Match MatchId="oasis:xacml:1.0:fn:string-equal">
          <AttributeValue
             DataType="xs:string">read</AttributeValue>
          <AttributeDesignator
             Category="oasis:xacml:3.0:ac:action"
             AttributeId="kestrel:action-id"
             DataType="xs:string"
             MustBePresent="true"/>
        </Match>
      </AllOf></AnyOf>
    </Target>
    <Condition>
      <!-- the five terms go here -->
    </Condition>
  </Rule>
</Policy>

One of those five terms, on its own, looks like this. The verbosity is not an accident of taste; it comes from XACML treating every attribute lookup as a bag of values with an explicit data type and an explicit rule for what happens when the bag is empty.

<Apply FunctionId="oasis:xacml:1.0:fn:string-is-in">
  <Apply
     FunctionId="oasis:xacml:1.0:fn:string-one-and-only">
    <AttributeDesignator
       Category="oasis:xacml:3.0:ac:resource"
       AttributeId="kestrel:deal-code"
       DataType="xs:string"
       MustBePresent="true"/>
  </Apply>
  <AttributeDesignator
     Category="oasis:xacml:1.0:sc:access-subject"
     AttributeId="kestrel:deal-codes"
     DataType="xs:string"
     MustBePresent="false"/>
</Apply>

Rego is the language of Open Policy Agent, a project created at Styra, accepted into the Cloud Native Computing Foundation on 29 March 2018, promoted to incubating on 2 April 2019 and graduated on 29 January 2021. Version 1.0 was announced on 20 December 2024, which made the newer syntax, with mandatory if on rule bodies and contains on set-building rules, the default. Rego descends from Datalog: a rule is a set of conditions that must all hold, and the whole document is a query over structured input.

package vault.authz

default allow := false

allow if {
    input.action == "read"
    input.subject.employment_status == "active"
    input.resource.deal_code in input.subject.deal_codes
    input.resource.sensitivity <= input.subject.clearance
    input.context.network == "corporate"
    input.context.device_managed
}

Twelve lines against roughly seventy of XML, and the difference is not only brevity. Rego is a general-purpose policy language: the same engine decides Kubernetes admission, Terraform plan checks and API authorization, and it will happily compute values as well as decisions. That generality is bought by giving up analyzability. You cannot in general ask a Rego policy “is there any input for which this returns allow”, because the language is expressive enough to make that question undecidable.

Cedar is the youngest of the three. Amazon published it as open source in May 2023, with the language version numbered 2.0 on 10 May 2023, and the language reference documented version 4.5, dated 23 April 2026, as of August 2026. It is the policy language of Amazon Verified Permissions and AWS Verified Access.

permit (
  principal in Group::"northwind-team",
  action    in [Action::"ReadDoc"],
  resource  in Folder::"northwind"
)
when {
  principal.employment_status == "active" &&
  context.network == "corporate" &&
  context.device_managed
};

forbid (principal, action, resource)
unless { context.device_managed };

Three design decisions in that listing are worth naming. First, the scope line, with principal, action and resource, is mandatory and indexable, so an engine can discard irrelevant policies without evaluating them. Second, in does the work of both role-based and relationship-based models: principal in Group::"northwind-team" is a group membership test over an entity hierarchy the application supplies. Third, Cedar has explicit forbid, and the rule is absolute: the result is allow only when at least one permit matches and no forbid matches, and the default with no policies at all is deny.

The deliberate restriction is the point of Cedar. Because the language is not Turing-complete and its semantics are formally modelled, policies can be analyzed mechanically: you can ask whether one policy set is strictly more permissive than another, which is exactly the question a change review needs answered. Amazon built the language using what it calls verification-guided development, modelling the authorizer and validator in the Dafny verification language and proving properties about them, then checking the production Rust against the model by differential random testing. That process was described publicly on 10 May 2023.

The trade-off across the three is consistent and it is not about syntax at all.

Language Strong at Gives up
XACML Precision, obligations Brevity, adoption
Rego Generality, one engine Formal analysis
Cedar Analysis, speed Doing anything else

The same question in RBAC, ABAC and ReBAC, side by side#

Now the promised comparison. One question, three models, at 14:32 on 18 August 2026.

Question: may user:pnair read doc:contract-2026-0817?

RBAC  role deal-associate-northwind-read
      -> read on folder deals/northwind
      decided by: which roles pnair holds

ABAC  permit read when
        subject.deal_codes contains
          resource.deal_code
      decided by: attribute values at 14:32

ReBAC Check(user:pnair, viewer,
        doc:contract-2026-0817)
      decided by: path through the graph

All three say yes this afternoon. They differ everywhere else, and the differences are the reason mature systems use more than one.

Model Cost of a 13th deal Reverse query
RBAC New roles, new grants Easy, read the roles
ABAC Nothing, if attributed Hard, must search
ReBAC New tuples only Built in, but indexed

Consider what each model cannot say. RBAC cannot say “the document’s owner”, because ownership is a relationship between two specific things, not a property of a job. ABAC can say it only by promoting the owner to an attribute of the resource and comparing, which works for one hop and becomes unpleasant for three. ReBAC says it in one tuple and follows three hops without effort, but struggles to say “and not from an unmanaged laptop at three in the morning”, because that is not a relationship.

Consider what each costs to change. Adding a thirteenth transaction to RBAC means creating roles and assigning people, which is a provisioning workflow with human latency measured in days. In ABAC it means the deal system emitting a new deal code, and nothing else, provided every person’s attributes are maintained. In ReBAC it means writing three tuples.

Consider the audit question, “what can Priya reach”. RBAC answers it from the role assignments in seconds. ABAC cannot answer it without enumerating resources and evaluating the policy against each, which at Kestrel means 41 million evaluations. ReBAC has an expansion operation for exactly this question, and a system like Zanzibar needs a dedicated index to make it fast at scale.

The practical answer that large organizations reach, usually after building two of the three, is a layered one. Roles carry the coarse, auditable, slow-moving grants that governance cares about. Relationships carry sharing and containment, which is what users actually do all day. Attributes carry context, and act only as a narrowing filter over what the other two already allowed. That ordering matters: attributes that can only remove access, never add it, keep the auditable upper bound intact.

Least privilege in practice, and why it decays#

The principle is older than every model in this chapter. Jerome Saltzer and Michael Schroeder stated it in “The Protection of Information in Computer Systems”, Proceedings of the IEEE volume 63, number 9, September 1975: every program and every user should operate using the least set of privileges necessary to complete the job. It appears today as control AC-6 in NIST Special Publication 800-53 Revision 5, whose current release 5.2.0 was published on 27 August 2025, with enhancements that include AC-6(5) on privileged accounts, AC-6(7) on the review of user privileges and AC-6(9) on logging the use of privileged functions.

Least privilege is not a state. It is a rate, and the rate is always positive, because of a structural asymmetry that no amount of good intention fixes.

Grants have a requester and a beneficiary. Priya needs access to the Cirrus data room, she asks, somebody approves, the grant happens within a day. Removals have neither. When the Cirrus deal closes, nobody is inconvenienced by Priya keeping her access, and nobody’s Tuesday is improved by removing it. Over-granting produces no visible symptom; under-granting produces a support ticket within minutes. Every feedback loop in the organization pushes in one direction.

That is why least privilege decays without measurement, and why the useful intervention is arithmetic rather than exhortation. Four numbers are computable in most environments this week.

Metric How to compute it
Unused grant rate Grants with no use in 90 days
Privilege age Days since a grant’s last use
Wildcard count Policies granting all actions
Role tenancy Roles with exactly one member

The published figures are not encouraging. Datadog’s 2025 State of Cloud Security study, drawn from thousands of organizations using AWS, Azure and Google Cloud, found that 19.4 percent of AWS EC2 instances had overprivileged permissions, roughly flat against 19 percent in 2024; that 12.2 percent of roles created for third-party integrations were dangerously overprivileged, up from 10 percent the year before; and that 23 percent of Google Cloud virtual machines held overly permissive access through default service accounts, an improvement from 37 percent in 2023. The pattern is that the well-publicized problems improve slowly and the quiet ones do not.

Two practices actually reduce the rate rather than measuring it. The first is expiry by default: a grant that lasts thirty days and must be renewed makes the removal automatic and the retention deliberate. The second is elevation on demand, where a standing role carries no dangerous permission and the dangerous one is acquired for minutes with a reason attached; chapter 45 covers the governance around that and around recertification campaigns.

There is a third practice that is less discussed and more effective than either: make the enforcement point log the decision it would have made under a tighter policy alongside the decision it did make. After thirty days of that shadow log, the tighter policy is no longer a proposal but a measured change with a known blast radius.

Where authorization actually breaks in production#

The failures in real systems are not failures of the models. They are failures to consult them.

In the OWASP Top Ten for 2021, broken access control moved into first position, and in the 2025 edition it remained there as A01. The API-specific list is even more direct: in the OWASP API Security Top 10 for 2023, API1:2023 is Broken Object Level Authorization, API3:2023 is Broken Object Property Level Authorization, and API5:2023 is Broken Function Level Authorization. Three of the ten entries, including the first, are the same mistake at three different granularities.

Object level is the classic. Vault exposes GET /api/documents/{id}. The handler validates Priya’s token, loads document contract-2026-0817 and returns it. Everything about the request was authenticated. Nothing about it was authorized, because the handler never asked whether this document is one of hers. Change the identifier to contract-2026-0818 and you have the whole estate. The fix is not a clever policy; it is to make the data access itself scoped, so that the query cannot express “any document” without a tenant or team predicate.

Property level is the same bug one layer in. The document object returned by the API contains an internal_valuation field which the user interface does not display. The interface is not a security control.

Function level is the same bug one layer out. The administrative endpoint is not linked in the menu for associates, and it is reachable by anyone who types the path.

The fourth failure is the confused deputy already met in the plain section, wearing a service account. Kestrel’s nightly report generator runs with rights over every document because that was the shortest path to a working job. It accepts a parameter naming which folders to summarize. Any user who can influence that parameter has borrowed the report generator’s authority. Chapter 47 covers how such a workload should be given an identity of its own; the authorization lesson is narrower and universal: a component acting on behalf of a caller must evaluate the caller’s rights, not its own.

Testing a policy, and proving one#

Because the input space cannot be enumerated, testing a policy is a different discipline from testing a function.

Three techniques carry most of the weight. Unit tests over representative decisions, which Open Policy Agent supports directly with opa test over rules named with a test_ prefix, catch the intended cases and the obvious regressions. Shadow evaluation, where the new policy runs beside the old one and disagreements are logged without being enforced, catches the cases nobody thought of, and is the single most useful practice in this chapter. Mechanical analysis, available only in deliberately restricted languages, answers the question tests cannot: whether the new policy is strictly less permissive than the old one, for every possible input.

That last capability is why Cedar gave up expressiveness on purpose, and it is the clearest illustration of the Harrison, Ruzzo and Ullman result from 1976 having practical consequences fifty years later. You may have a policy language that can express anything, or one whose consequences you can prove. You cannot have both, and the choice should be made deliberately rather than by picking whichever engine the platform team already runs.

One last operational note. Every decision point should emit a decision log containing the subject, the action, the resource, the decision, and the identifier of the policy that produced it. Without the last field, an incident investigation degenerates into re-running history by hand. With it, the question “why was this allowed” has a one-line answer, and that is the difference between a control and a hope.

46.98 Common wrong ideas#

Wrong: Authentication and authorization are two names for the same check. Right: They are separate decisions with separate failure modes. Authentication establishes an identity once and produces a fact that stays true for a session. Authorization decides one action on one resource in one context, is asked again on every request, and depends on data that authentication never touched.

Wrong: A valid token means the request is allowed. Right: A token proves that some authentication happened and may carry claims about the holder. It says nothing about whether this holder may perform this action on this resource. Every broken object level authorization bug in the OWASP API Security Top 10 for 2023 involves a perfectly valid token.

Wrong: The access control matrix is a data structure you build. Right: It is a model that nobody stores. Real systems store one of its two projections: a column with the object, which is an access control list, or a row with the subject, which is a capability. Choosing between them decides which questions are cheap and which require a full scan of your estate.

Wrong: ABAC replaces RBAC, so role-based systems are legacy. Right: NIST’s own guidance, from Kuhn, Coyne and Weil in IEEE Computer in June 2010 onwards, describes combining them, and the arrangement most large organizations reach is role-centric: roles set an auditable maximum, and attributes may only narrow it. Roles remain the artefact that governance and auditors can actually read.

Wrong: ABAC removes role explosion. Right: It moves the combinatorics from the role catalogue into the policy set and the attribute supply chain. A rule with five terms over modest attribute domains can already cover more than a million distinct request shapes, which is why attribute-based systems must be analyzed rather than merely tested.

Wrong: Capabilities are just stealable tokens, so lists are safer. Right: Each resists a different attack. Lists invite the confused deputy, described by Norman Hardy in ACM SIGOPS Operating Systems Review in 1988, because the resource checks who is standing in front of it rather than whose authority was intended. Capabilities resist that and are harder to revoke. Most real systems store lists and pass short-lived capabilities internally.

Wrong: A Zanzibar-style system is eventually consistent, so occasional stale answers are the only risk. Right: Stale answers in the wrong order create the new enemy problem, in which a user removed from a folder can read a document added to it afterwards. Zanzibar addresses this with zookies, opaque tokens encoding a timestamp that force evaluation at or after the moment the content changed. An implementation without that mechanism has an exploitable ordering window equal to its replication lag.

Wrong: Deny by default makes a system safe. Right: Deny by default protects the paths that reach the policy engine. It says nothing about the report generator running with rights over everything, the database query that filters in application code, or the admin page nobody wired up. The real question is what fraction of the code paths that reach data consult the policy at all.

Wrong: Least privilege is a project you can finish. Right: It is a rate, not a state. Grants have requesters and removals do not, so permissions accumulate unless something measures them. Useful measurements are grants unused for ninety days, days since last use, wildcard policy counts and roles with exactly one member.

Wrong: XACML failed because XML is verbose. Right: Verbosity was a symptom. The deeper reasons were the cost of a remote decision point on every request, the difficulty of supplying every attribute a rule needed at decision time, and a decision model richer than most applications could use. XACML 3.0 remains an OASIS Standard, current as of Errata 01 dated 12 July 2017, and its four-component vocabulary is now used by systems that have never seen a line of its XML.

46.99 Chapter summary in 20 lines#

  1. Authentication answers who you are and ends; authorization answers what you may do and is asked again on every single request.
  2. The access control matrix, set out by Butler Lampson in 1971, is the model behind every access system, and no system stores it.
  3. Storing a matrix column with the object gives an access control list; storing a row with the subject gives a capability.
  4. Access control lists make “who can reach this” cheap and “what can this person reach” a full scan, which is why access reviews are painful.
  5. Capabilities travel and are fast, they resist the confused deputy problem named by Norman Hardy in 1988, and they are hard to revoke.
  6. Harrison, Ruzzo and Ullman proved in 1976 that the general safety question for the matrix model is undecidable, which is why analyzable policy languages restrict themselves on purpose.
  7. Discretionary control lets the owner share; mandatory control enforces a rule the owner cannot override, and both were named in the Orange Book of December 1985.
  8. Bell-LaPadula protects secrecy with no read up and no write down; Biba protects integrity with the mirror rules of no read down and no write up.
  9. Role-based access control was formalized by Ferraiolo and Kuhn in 1992, published as a proposed NIST standard in 2001, and adopted as ANSI INCITS 359 in February 2004.
  10. The NIST model has core, hierarchical, static separation of duty and dynamic separation of duty levels, and contains no negative permissions at all.
  11. Roles explode multiplicatively: five reasonable distinctions at one nine-hundred-person firm turn twelve roles into one thousand seven hundred and twenty-eight.
  12. Attribute-based access control, defined in NIST SP 800-162, evaluates subject, object, action and environment attributes against rules, and collapses that role catalogue into a handful of policies.
  13. The price of attributes is supply and analysis: a decision is only as fresh as its slowest attribute source, and a five-term rule can already cover over a million request shapes.
  14. Relationship-based access control asks whether a path exists through a graph, which is how document sharing has always actually worked.
  15. Zanzibar, published by Google at USENIX ATC in July 2019, stores over two trillion relation tuples across more than fifteen hundred namespaces and keeps 95th percentile checks under ten milliseconds.
  16. A zookie is an opaque timestamp token that prevents the new enemy problem, in which a removed user reads content added after their removal.
  17. The four components are the enforcement point on every data path, the decision point that evaluates, the information point that supplies attributes, and the administration point where policy is authored.
  18. XACML is precise and verbose, Rego is general and unanalyzable, and Cedar gave up expressiveness to buy mechanical proof that one policy set is no more permissive than another.
  19. Least privilege decays because grants have requesters and removals do not, so it must be measured in unused grants, privilege age, wildcard counts and single-member roles.
  20. Most real breaches are not failures of the model but failures to consult it, which is why broken access control sits at the top of the OWASP Top Ten for both 2021 and 2025.

Chapter sources: Butler Lampson, “Protection”, 5th Princeton Symposium on Information Sciences and Systems, March 1971, reprinted in ACM Operating Systems Review volume 8 number 1, January 1974, pages 18 to 24; Jack Dennis and Earl Van Horn, “Programming Semantics for Multiprogrammed Computations”, Communications of the ACM volume 9 number 3, March 1966, pages 143 to 155; Michael Harrison, Walter Ruzzo and Jeffrey Ullman, “Protection in Operating Systems”, Communications of the ACM volume 19 number 8, August 1976; Jerome Saltzer and Michael Schroeder, “The Protection of Information in Computer Systems”, Proceedings of the IEEE volume 63 number 9, September 1975; David Elliott Bell and Leonard LaPadula, “Secure Computer System: Unified Exposition and Multics Interpretation”, MITRE MTR-2997 Revision 1, ESD-TR-75-306, March 1976; Kenneth Biba, “Integrity Considerations for Secure Computer Systems”, MITRE MTR-3153, ESD-TR-76-372, April 1977; Norman Hardy, “The Confused Deputy”, ACM SIGOPS Operating Systems Review, 1988; the Trusted Computer System Evaluation Criteria, DoD 5200.28-STD, December 1985; David Clark and David Wilson, IEEE Symposium on Security and Privacy, 1987; David Brewer and Michael Nash, “The Chinese Wall Security Policy”, IEEE Symposium on Security and Privacy, 1989; David Ferraiolo and Richard Kuhn, “Role-Based Access Control”, 15th National Computer Security Conference, 1992, pages 554 to 563; Ravi Sandhu, Edward Coyne, Hal Feinstein and Charles Youman, IEEE Computer volume 29 number 2, February 1996; David Ferraiolo, Ravi Sandhu, Serban Gavrila, Richard Kuhn and Ramaswamy Chandramouli, “Proposed NIST Standard for Role-Based Access Control”, ACM Transactions on Information and System Security volume 4 number 3, August 2001, pages 224 to 274; ANSI INCITS 359-2004, adopted 11 February 2004, revised as INCITS 359-2012; Richard Kuhn, Edward Coyne and Timothy Weil, “Adding Attributes to Role-Based Access Control”, IEEE Computer volume 43 number 6, June 2010, pages 79 to 81; NIST Special Publication 800-162, “Guide to Attribute Based Access Control (ABAC) Definition and Considerations”, January 2014, updated 25 February 2019; NIST Special Publication 800-178, comparing XACML and NGAC, 3 October 2016; NIST Special Publication 800-53 Revision 5, control AC-6, release 5.2.0 of 27 August 2025; OASIS eXtensible Access Control Markup Language Version 3.0, OASIS Standard of 22 January 2013, and the version incorporating Approved Errata 01 of 12 July 2017; Ruoming Pang, Ramon Caceres, Mike Burrows and colleagues, “Zanzibar: Google’s Consistent, Global Authorization System”, USENIX Annual Technical Conference, Renton, Washington, July 2019; the Cedar policy language documentation, version 2.0 of 10 May 2023 and version 4.5 of 23 April 2026, and the Amazon Science account of verification-guided development published 10 May 2023; Open Policy Agent CNCF milestones of 29 March 2018, 2 April 2019 and 29 January 2021, and the OPA 1.0 announcement of 20 December 2024; OpenFGA CNCF sandbox acceptance of 14 September 2022 and incubating status of 28 October 2025; the OpenID Foundation AuthZEN Authorization API, first draft of 6 September 2024; the OWASP Top Ten for 2021 and 2025 and the OWASP API Security Top 10 for 2023; Datadog’s 2025 State of Cloud Security study; and the Kubernetes 1.8 release of 29 September 2017 in which the RBAC API became generally available.