Cloud Infrastructure · Identity and Access
Deny Wins: How One Request Is Evaluated Against Many Policy Documents
The order in which identity policies, resource policies, boundaries, and organization policies combine to produce a single allow or deny, and why that order is not additive.
A policy document on its own is a readable thing. It has a small grammar, a handful of fields, and a meaning that can be worked out by inspection. The difficulty begins when a request is governed by six documents written by four different people in three different accounts, none of whom can see the others’ work. The interesting part of cloud authorization is not the grammar. It is the rule that combines the documents into one bit.
The request is a fixed tuple, not a call
Every authorization decision reduces the same four-part question: which principal is asking, which action is being requested, which resource is named, and under what request context. The context is the part people forget. It carries the source address, the time, whether multi-factor authentication was used in the session, which VPC endpoint the request arrived through, whether the connection used TLS, and any tags supplied in the request body. Two calls that differ only in a source address can produce different decisions, because the tuple is different.
Evaluation is a pure function of that tuple against a set of documents. Nothing about prior requests enters into it, with the exception of session-level facts baked into temporary credentials at the moment they were issued. This purity is what makes the model analysable, and it is also what makes it surprising: a permission that worked yesterday can fail today because a tag changed, not because any policy changed.
Documents come in kinds, and the kind determines which part of the tuple the document is allowed to constrain. Identity-based policies attach to a user, group, or role, and their Principal is implied. Resource-based policies attach to a bucket, queue, key, or function, and must state a Principal explicitly, which is why they can grant access to a principal in another account without that principal holding any policy of its own. Permission boundaries attach to an identity and act as a ceiling. Organization policies attach to an account or an organizational unit and apply to every principal inside it. Session policies are passed as a parameter at the moment temporary credentials are minted and apply only to that session.
A single statement carries an Effect, a set of actions, a set of resources, and optional conditions. The sets are matched with * and ? wildcards, which is where most of the expressive power and most of the surprise lives; the matching rules are covered in resource ARN wildcard matching.
Explicit deny is checked first, and it is global
The documented evaluation order puts deny evaluation at the front. Any statement with Effect: Deny that matches the request tuple, in any applicable document, terminates evaluation with a deny. It does not matter that forty other statements allow the same action. One deny wins.
This is a deliberate inversion of the usual additive intuition. In a filesystem, permissions accumulate: if any applicable rule grants, the operation proceeds. In this model, grants must survive a filter. The consequence is that a resource owner can veto access to their resource without negotiating with the identity owner, and an organization administrator can veto a whole class of actions across every account in a unit without touching a single identity policy.
The veto is why Deny statements deserve more scrutiny than Allow statements. An Allow with an over-broad wildcard is a hole; a Deny with an over-broad wildcard is an outage. A deny on s3:* intended to stop public bucket policies will also stop the deployment pipeline that writes build artifacts, and it will do so at three in the morning rather than at review time.
Absent any matching deny, the remaining question is whether something allows the request. The default is deny, and the default is reached silently: a request that no statement mentions produces the same answer as a request that a statement explicitly forbids. This is Saltzer and Schroeder’s complete mediation principle in its bluntest form, and it means the failure mode of a missing permission is indistinguishable from the failure mode of a forbidden one.
Where the intersection happens
Once no deny matches, the model asks whether each gate in the path allows. Gates are the documents that cannot grant on their own, only restrict.
An organization policy is a gate. It does not grant anything to anybody; it says which actions are permitted to exist within the account at all. An account whose organization policy omits ec2:RunInstances cannot launch an instance no matter what its identity policies say, and the error message will not mention the organization policy at all.
A permission boundary is a gate of the same shape but narrower scope: it applies to one identity, and it caps what identity-based policies can grant to that identity. The effective grant is the intersection of the identity policy set and the boundary. A boundary that allows everything is a no-op; a boundary that allows nothing makes the identity inert regardless of its attached policies.
A session policy is a third gate, applied at credential issuance. It intersects with everything downstream of it, so a role session created with a restrictive session policy cannot exceed that policy even if the role’s own policies are broad. This is the mechanism that lets a service hand out a narrowed version of its own role rather than a separate role per caller. The mechanics of that issuance are in role assumption and session tokens.
The composition rule is worth stating as an equation, because the intuition that “policies add up” is wrong in a way that costs people days:
effective = (identity_allow ∩ boundary ∩ session_policy ∩ org_policy) − explicit_deny
Every term except the deny term is an intersection, and the deny term is subtracted last. Nothing in the model is a union across document kinds. A union happens only within a single kind, across the statements of the documents of that kind.
There is one asymmetry worth flagging: grants that arrive through a resource-based policy are not filtered by the permissions boundary of the calling identity. The boundary caps identity-based grants, and a resource policy in the same account can therefore hand out access the boundary was meant to prevent. A boundary is a cap on one channel, not a global ceiling.
Cross-account requests evaluate twice
When the principal and the resource live in different accounts, both accounts get a say. The resource account must allow through a resource-based policy, and the principal’s account must allow through an identity policy, and either side can deny. This is the mechanism behind the pattern in cross-account trust and the confused deputy: the trust relationship is the resource-side allow, and the caller’s own policies are the identity-side allow.
Two independent evaluations mean two independent failure surfaces. A cross-account call that fails produces an error from one side, and the error names the side that refused. Reading the message as if it described the other account is a common and expensive mistake.
What the model cannot express
The model has no notion of a request being partially authorized, no delegation of a subset of one’s own rights, and no negative permission that survives a later grant. It also has no ordering among statements: within a document, statement order is irrelevant, which is a feature until someone tries to write “allow this, except for that” as two statements and discovers the second one needs NotResource to mean anything.
The alternative design, visible in XACML, makes the combination rule explicit and configurable: a policy set declares a combining algorithm such as deny-overrides, permit-overrides, or first-applicable. That flexibility is real, and it is bought with the requirement that every policy set author choose and document an algorithm, because two policy sets with different algorithms cannot be composed without a third rule saying which wins. The fixed deny-overrides order used by the large cloud providers trades that flexibility for the guarantee that independently authored documents compose without negotiation.
The practical residue is that debugging is not local. A denial can originate in any of five document kinds across two accounts, and the only reliable method is to enumerate the applicable documents and test each against the tuple rather than to read the one policy someone remembered editing. Tooling that answers “which statement produced this decision” is worth more than tooling that answers “does this policy look right”, and the second question is harder than it appears, as policy satisfiability analysis describes.
A final constraint is size. A managed policy document is capped at 6,144 characters, inline policies have smaller per-entity caps, and only a fixed number of managed policies can be attached to one identity. Those limits push large permission sets into splits across documents, and splitting is not semantically neutral: an intersection computed over two documents is not the same as an intersection computed over one, and a wildcard that was safe in a narrow document can become broad when the document it lives in grows. When a policy approaches the character limit, the useful move is to replace enumerations with tag-based conditions rather than to add another attachment, which is the subject of attribute conditions in policy.
The model’s real virtue is that the answer is a function of a tuple and a document set, both of which can be captured and replayed. That is what makes automated reasoning about access possible at all, and it is why the fixed order is worth memorizing rather than looking up.
References
- Policy evaluation logic — AWS Identity and Access Management User Guide
- eXtensible Access Control Markup Language (XACML) Version 3.0 — OASIS Standard
- The Protection of Information in Computer Systems — Saltzer and Schroeder, Proceedings of the IEEE, 1975
- IAM JSON policy elements reference — AWS Identity and Access Management User Guide