The IAM evaluation model
Deny wins, ceilings are conjunctive, grants additive.
Every action you take in AWS runs through the same silent question. Is this exact caller allowed to do this exact thing, to this exact resource, right now? The service that answers it is IAM, short for Identity and Access Management, and it answers on every single API call, meaning every request a program or a person makes to AWS, in a few milliseconds. Most people assume the answer comes from one policy saying yes. It doesn't. It comes from several policies arguing at once, and there are strict rules for who wins that argument.
One request, several locked doors
AWS starts every request at no. That's the default. Nothing happens until something explicitly grants the action. But a grant isn't the finish line, because the request still has to get past a run of gates. Hold it in your head like this: the caller is walking a corridor lined with locked doors, and to reach the room at the end, every door has to open. One door is the caller's own permissions. Another is a ceiling their team set that they can't rise above. Another is a ceiling the whole account can't rise above. If a single door stays shut, they don't get in.
There's also a guard in that corridor, and he has special authority. He can throw anyone out on sight, even with every door standing open. That guard is an explicit Deny, and he outranks everyone else in the building. A Deny written into any policy at all, the caller's own, the resource's, the account ceiling, or the identity cap, ends the decision on the spot. Nothing overrides it. That's what makes a Deny such a clean safety tool. You can hand out generous permissions and still guarantee that certain actions never happen.
Two more ideas make the rest click. Grants stack up. Attach five policies that each allow a little, and the caller ends up with the sum of all five. Ceilings work the other way, narrowing things down, so the caller's real power is only the overlap of what every ceiling permits. Add another ceiling and you can take permissions away, never add them. Two kinds of ceiling are worth naming now. An SCP, short for Service Control Policy, is a rule the organization above an account clamps onto the whole account. It grants nothing by itself. It only sets the maximum of what anyone in that account is even allowed to attempt. A permission boundary is the same idea pointed at a single user or role: a cap saying this identity can never do more than this, no matter which policies someone bolts on later.
Stop guessing. Make IAM show its work.
You don't have to run all of this in your head and hope you got it right. IAM ships a simulator that runs the real evaluation engine against a call you describe, then hands back the verdict along with the exact statement that decided it. It turns "I'm pretty sure this role is safe" into something you can check and paste into a review.
aws iam simulate-principal-policy \--policy-source-arn arn:aws:iam::123456789012:role/ci-deployer \--action-names s3:PutObject \--resource-arns arn:aws:s3:::acme-artifacts/build-4711.tar.gz{"EvaluationResults": [{"EvalActionName": "s3:PutObject","EvalResourceName": "arn:aws:s3:::acme-artifacts/build-4711.tar.gz","EvalDecision": "allowed","MatchedStatements": [{"SourcePolicyId": "ci-deployer-permissions","SourcePolicyType": "IAM Policy","StartPosition": { "Line": 3, "Column": 17 },"EndPosition": { "Line": 8, "Column": 6 }}],"MissingContextValues": []}],"IsTruncated": false}
Read that output like a verdict sheet. EvalDecision is the ruling, and it comes in three flavours. allowed means a statement granted the call and nothing blocked it. explicitDeny means a Deny fired. implicitDeny means nobody said yes, so the default no held. MatchedStatements points straight at the statement responsible, down to the line and column in the policy document, which is gold when you're hunting for why something is permitted. And if MissingContextValues comes back with entries, the decision leaned on a condition you never supplied, like whether MFA (multi-factor authentication, the second login step beyond a password) was present. Feed that value in and the live answer could flip.
Where a boundary actually bites
Deployment roles are a favourite target for attackers, because they pile up permissions nobody ever prunes. Take a role called ci-deployer, the automated account your build pipeline uses to ship code. Its real job is small: push build artifacts to S3 (Amazon's file storage) and update a couple of database tables. Then, over time, someone attaches a policy far broader than that, including full IAM access. Hunting down every over-grant one by one is a losing game. Better to bolt a hard ceiling onto the role, so even a careless policy can't become account takeover. That ceiling is a permission boundary.
{"Version": "2012-10-17","Statement": [{"Sid": "DataPlaneWork","Effect": "Allow","Action": ["s3:*", "dynamodb:*", "logs:*", "cloudwatch:*", "ecr:*"],"Resource": "*"},{"Sid": "NeverTouchIdentityOrOrg","Effect": "Deny","Action": ["iam:*", "organizations:*", "account:*", "sso:*"],"Resource": "*"}]}aws iam create-policy \--policy-name ci-deployer-boundary \--policy-document file://ci-deployer-boundary.json{"Policy": {"PolicyName": "ci-deployer-boundary","PolicyId": "ANPA1EXAMPLEBOUNDARY7","Arn": "arn:aws:iam::123456789012:policy/ci-deployer-boundary","DefaultVersionId": "v1","AttachmentCount": 0,"IsAttachable": true,"CreateDate": "2026-07-16T09:14:52Z"}}
With the boundary registered, the simulator can prove the cap actually holds. Point it at the same role, ask about the escalation moves you're worried about, like minting new IAM users or attaching policies to them, and feed in the boundary as the ceiling to test against.
aws iam simulate-principal-policy \--policy-source-arn arn:aws:iam::123456789012:role/ci-deployer \--action-names iam:CreateUser iam:AttachUserPolicy \--resource-arns "arn:aws:iam::123456789012:user/*" \--permissions-boundary-policy-input-list file://ci-deployer-boundary.json{"EvaluationResults": [{"EvalActionName": "iam:CreateUser","EvalResourceName": "arn:aws:iam::123456789012:user/*","EvalDecision": "explicitDeny","MatchedStatements": [{"SourcePolicyId": "PermissionsBoundaryPolicyInputList.1","SourcePolicyType": "IAM Policy","StartPosition": { "Line": 10, "Column": 5 },"EndPosition": { "Line": 15, "Column": 6 }}],"MissingContextValues": [],"PermissionsBoundaryDecisionDetail": {"AllowedByPermissionsBoundary": false}},{"EvalActionName": "iam:AttachUserPolicy","EvalResourceName": "arn:aws:iam::123456789012:user/*","EvalDecision": "explicitDeny","MatchedStatements": [{"SourcePolicyId": "PermissionsBoundaryPolicyInputList.1","SourcePolicyType": "IAM Policy","StartPosition": { "Line": 10, "Column": 5 },"EndPosition": { "Line": 15, "Column": 6 }}],"MissingContextValues": [],"PermissionsBoundaryDecisionDetail": {"AllowedByPermissionsBoundary": false}}],"IsTruncated": false}
Notice what happened. The role's own policy allows every IAM action, and the call still came back explicitDeny, because the boundary's Deny fired first and AllowedByPermissionsBoundary came back false. That's the entire model in one call. All four policy types have to agree before a single action is allowed, and any one of them can veto the rest. A Deny is the loudest voice in the room; it ends the argument no matter who else said yes. The same logic scales up to a whole fleet of accounts. Put a boundary on every workload role, or an SCP across the organization, and a single over-broad grant in one account can't climb past the ceiling. One mistake stays one mistake instead of turning into ten. That's what people mean by shrinking the blast radius, the amount of damage a single slip can cause. The actions worth denying in that ceiling are the ones that let a principal rewrite its own permissions, and the sharpest of those is iam:PassRole, which is where most real escalation paths begin.
Try this
Work through “Where a boundary actually bites” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: the simulator can lie by omission. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.