Business Challenge
A platform team at a payments company introduces permissions boundaries. The goal is ordinary and sensible: developers can create IAM roles for their own services, but cannot create a role more powerful than themselves. The boundary policy is written, attached to the developer role, and reviewed by two people. It is correct.
Within a day, three pipelines fail. One cannot write to an S3 bucket it has written to for a year. One cannot decrypt with a KMS key whose key policy explicitly names it. One fails only in the production account, with the same code and the same role definition that works in staging.
The team starts debugging the way most teams do β by reading the identity policy, deciding it looks fine, and then adding permissions until something works. Two of the three pipelines are eventually fixed by granting broader access than they had before the boundary was introduced. The control meant to reduce privilege has increased it.
Nothing here is a bug. Every one of those three failures is the documented behaviour of a different gate, and the three gates are not the same kind of thing. The team was treating six policy types as one undifferentiated pile of permissions, and there is no way to debug that pile without knowing the order AWS reads it in.
It is "which of the seven gates denied this, and can that gate ever say yes?" Four of the seven can only ever subtract permission β adding a statement to them never grants anything. If the denial came from one of those four, every minute spent broadening the identity policy is wasted, and the access that eventually appears came from somewhere you did not intend.
Architecture
AWS documents the evaluation as an ordered pipeline, not a set. A request starts in a known state and passes through gates in a fixed sequence. The order is not a mnemonic somebody invented; it is the order the enforcement code runs, and the consequences of the ordering are what make certain combinations behave unintuitively.
The starting state is deny
AWS's wording is worth taking literally: by default, all requests are implicitly denied with the exception of the AWS account root user, which has full access. Permission is not a property a principal has; it is a conclusion reached by a policy statement, for one request, at one moment. Nothing carries over.
This is why "the role has S3 access" is an imprecise sentence that causes real confusion. The role has an identity policy containing a statement that will allow certain S3 actions on certain resources when no other gate objects. That is a much weaker claim, and the difference is where the three broken pipelines live.
Gate 1: the deny sweep runs before anything else
Before AWS decides whether anything allows the request, it looks across every applicable policy β SCPs, RCPs, resource-based, identity-based, boundaries and session policies alike β for a statement with "Effect": "Deny" that matches. One match anywhere ends the evaluation immediately with a final decision of Deny.
An explicit deny overrides an explicit allow, and no later gate can reverse it. There is no precedence between policy types at this stage and no notion of a more-specific policy winning. This is the single most useful property in the whole model, because it makes an explicit Deny the only construct in IAM that is genuinely unconditional.
Gates 2 and 3: the two organisation-level ceilings
Service control policies and resource control policies are both Organizations guardrails, and they are frequently described as a pair. They act on opposite ends of the request.
SCPs bound the principal
An SCP limits what principals in the attached account may do, wherever the target resource lives. If the SCP produces no applicable Allow, the request is denied. SCPs grant nothing on their own β a principal with an SCP permitting everything and no identity policy still has no access at all.
RCPs bound the resource
An RCP limits what may be done to resources in the attached account, whoever is asking β including principals from outside the organisation entirely. AWS is explicit that an RCP never grants permissions. It is the answer to "can an external principal be given access to my bucket by a bucket policy I did not review", and the answer is now no.
RCPs apply to resources for a subset of AWS services β a real list, currently including S3, KMS, SQS, Secrets Manager and STS, but not everything. And they do not apply to calls made by service-linked roles, nor to resources in the management account at all. An RCP is a strong control over the surface it covers and silent everywhere else, so "we have an RCP" is not the same claim as "this is prevented".
Gate 4: the resource-based policy, and why it is not symmetric
Within a single account, identity-based and resource-based policies combine as a union: the resulting permissions are the union of the permissions of the two types. An allow in either is sufficient. This is the opposite of every other combination in the model, and it is the reason a bucket policy can grant access that the role's own identity policy never mentions.
It goes further, and this is the part that surprises people. If a resource-based policy grants permission directly to an IAM role session ARN or an IAM user ARN, then an implicit deny in an identity policy, a permissions boundary or a session policy does not affect the outcome. Permission granted directly to a session is not filtered by the ceilings that would otherwise apply.
Name the role ARN instead and the behaviour changes: a resource-based policy that grants to a role ARN is still limited by an implicit deny in a permissions boundary or session policy. Two policies that read almost identically, one naming arn:aws:iam::111122223333:role/app and one naming arn:aws:sts::111122223333:assumed-role/app/session, pass through different gates.
Gates 5, 6 and 7: identity, boundary, session
The identity-based policy is the one most teams write and the only one many teams know exists. If nothing in it allows the action, the request is implicitly denied here.
The permissions boundary is a ceiling attached to the entity, and it intersects: the resulting permissions are the intersection of the two categories. It cannot grant. A boundary allowing s3:* gives nobody S3 access; it merely declines to remove it.
The session policy applies only when the principal is a role session or a federated user, and is passed at the moment credentials are minted. It is the most commonly forgotten gate precisely because it is not attached to anything you can list later β it lives in the AssumeRole call that created the session.
Why This Architecture Holds Up
It converts "why is this denied" into a finite checklist
Access debugging without the ordering is unbounded: any of six policy types might be responsible, and the natural move is to broaden whichever one you can edit. With the ordering, there are seven places to look and they have a sequence. The three broken pipelines in the opening resolve immediately once each is attributed to its gate β the S3 failure to a boundary intersection, the KMS failure to a key policy that granted to a role ARN rather than a session, the production-only failure to an SCP that exists in one account and not the other.
It tells you which policy is even capable of fixing the problem
Only gates 4 and 5 can grant. That single fact removes most of the wasted work in access debugging. If an SCP is the reason, no amount of identity policy will help; if a boundary is the reason, the fix is a boundary change or a different role, not a broader grant. Teams that do not know this reliably end up granting more than they started with, because they keep widening the one policy they have permission to edit until the symptom disappears.
The asymmetry between deny and allow is a design tool
An explicit Deny is the only unconditional construct available. Everything else is contingent β an allow can be removed by four different ceilings, a resource policy can be overridden by an SCP, a boundary can be replaced. This is why guardrails that must genuinely hold are written as explicit denies with narrow conditions rather than as narrow allows, and why an SCP with a Deny on cloudtrail:StopLogging is worth more than any number of carefully scoped grants.
Cross-account is the same pipeline, run twice
There is no separate cross-account algorithm to learn. AWS performs two evaluations, one in each account, and the request is allowed only if both evaluations return a decision of Allow. The trusted account evaluates the identity policy and the things that limit it; the trusting account evaluates the resource policy and the things that limit it. Both must independently reach Allow.
Key Architecture Decisions
A guardrail expressed as a restrictive allow depends on nothing else granting the permission by another route β and gate 4 means a resource-based policy in another account can do exactly that. A guardrail expressed as an explicit deny is evaluated in the first sweep and cannot be overridden by any later gate, in any account, by any principal including the root user of a member account.
Decision signalIf the requirement is "this must never happen", it belongs in an SCP or RCP Deny with condition keys narrowing the exceptions. If the requirement is "this team should normally have these permissions", that is an identity policy. Confusing the two produces guardrails that hold until someone writes a bucket policy.
The document limits are tighter than most teams expect and they are hard limits. A service control policy document may be 10,240 characters; a resource control policy document may be 5120 characters β the SCP budget is exactly twice the RCP budget. A maximum of 10 SCPs and 5 RCPs attach to any one root, OU or account, and inheritance does not count against those numbers, only direct attachment.
The trapThe RCPFullAWSAccess policy is attached automatically when RCPs are enabled, cannot be detached, and counts towards the 5 policies quota. The usable budget at any single node is four RCPs, not five. Teams that plan for five discover this at the point where the fifth attachment fails, which is usually during an incident.
Granting to a role session ARN or an IAM user ARN bypasses the implicit denies in the boundary and session policy. Granting to a role ARN does not. This is a deliberate design in AWS's evaluation logic and not an edge case β it is how a resource policy can hand access to a specific session β but it means a permissions boundary is not a complete ceiling on what a principal can reach.
Concrete ruleIf the boundary is a compliance control rather than a convenience, pair it with an explicit deny at gate 1. A boundary alone can be routed around by a resource policy in another account naming the session; an SCP deny cannot.
Managed policies per role default to 20 and can be raised to a maximum of 25. Aggregate inline policy on a role can't exceed 10,240 characters, and each customer managed policy can't exceed 6,144 characters. Session policies are tightest of all: the passed JSON document and all passed managed policy ARNs combined can't exceed 2,048 characters.
ImplicationThe session policy limit is the one that shapes designs. A pattern that mints per-tenant sessions with a generated policy hits 2,048 characters quickly, and the usual escape β passing managed policy ARNs instead β consumes the same budget. Multi-tenant isolation built on session policies needs the character count estimated at design time, not discovered in load testing.
What each gate can and cannot do
| Gate | Can grant? | Combines by | Reach for it when |
|---|---|---|---|
| Explicit deny (any type) | No | Overrides everything | The requirement is absolute and must survive every other policy in every account |
| Organizations RCP | No | Intersection | You need to bound who can reach a resource, including principals outside the organisation |
| Organizations SCP | No | Intersection | You need to bound what an account's principals may do, regardless of who grants them what |
| Resource-based policy | Yes | Union with identity | Cross-account sharing, or granting to a specific session rather than a role |
| Identity-based policy | Yes | Union with resource | The ordinary case β what this principal is for |
| Permissions boundary | No | Intersection | Delegating role creation without delegating privilege escalation |
| Session policy | No | Intersection | Narrowing a broad role at the moment of assumption, per tenant or per task |
Reference: a guardrail that holds at gate 1
An SCP written as an explicit deny with a condition exception. The Deny is evaluated in the first sweep, so no identity policy, resource policy or boundary in any member account can reverse it β while the condition keeps a named break-glass role usable.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyCloudTrailTampering",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"cloudtrail:UpdateTrail"
],
"Resource": "*",
"Condition": {
"ArnNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/OrgBreakGlassAudit"
}
}
}
]
}
Note what this does not do: it grants nobody the ability to manage CloudTrail. The break-glass role still needs an identity policy allowing those actions. The SCP has only declined to remove a permission that something else must still grant β which is the defining property of every gate except 4 and 5.
Closing Thought
Most IAM debugging difficulty comes from a single missing distinction: the difference between a policy that grants and a policy that declines to remove. Five of the seven gates only ever subtract. Written down that way it is obvious; encountered at 2am against a pipeline that worked yesterday, it is not, and the instinct is always to add permissions to the one policy you can edit.
The ordering is worth learning as an ordering rather than as a list of policy types, because the surprising behaviours are all consequences of position. Resource-based policies behave asymmetrically because they sit ahead of the three narrowing gates. Explicit denies are absolute because the sweep runs first. Cross-account requires agreement from both sides because the pipeline runs twice. None of that is arbitrary once the sequence is in view.
The practical test for any access control you design: which gate is it, and can that gate say yes? If the answer is that it cannot grant β an SCP, an RCP, a boundary, a session policy β then it is a ceiling, and a ceiling with nothing underneath it protects an empty room. Pair it with the grant that it is bounding, and check that the grant cannot arrive by a route the ceiling does not cover.
Security & Identity β permissions boundaries in depth: delegating role creation to product teams without handing them privilege escalation, and the four ways a boundary is routinely defeated.
Official AWS Reference
- AWS Documentation β How AWS enforcement code logic evaluates requests to allow or deny access
- AWS Documentation β Policy evaluation logic
- AWS Documentation β Cross-account policy evaluation logic
- AWS Documentation β Resource control policies (RCPs)
- AWS Documentation β Quotas and service limits for AWS Organizations
- AWS Documentation β IAM and AWS STS quotas
Comments