Short answer: AWS evaluates every request against all policies that apply. It starts from an implicit deny, an explicit Deny anywhere ends the evaluation, and an Allow only works if every other layer that applies also allows it.
The evaluation order
- Explicit Deny in any policy: the request is denied. Nothing overrides it.
- Organizations SCPs and RCPs: if they apply to the account and do not allow the action, it is denied.
- Resource-based policy (for example a bucket policy): can grant access on its own in the same account.
- Permissions boundary: caps what the identity can do. The action must be inside it.
- Session policy (assumed-role sessions): caps the session further.
- Identity-based policy: must allow the action, unless a resource-based policy already did.
Simplified for a single account. Cross-account requests need an Allow on both sides: the identity policy in the caller account and the resource policy in the resource account.
Four situations where your Allow “does not work”
| Situation | What is really happening |
|---|---|
Role has s3:* and still gets AccessDenied | A Deny in the bucket policy, an SCP or the KMS key policy |
| Admin in a member account cannot use a Region | An SCP restricts aws:RequestedRegion |
| New role created by a pipeline cannot do what its policy says | A permissions boundary attached by the pipeline |
| Cross-account access works in one direction only | Only one side (identity or resource policy) allows it |
Debug it with the tools AWS gives you
debug-iam.sh
# Which identity is actually making the call?
aws sts get-caller-identity
# Simulate a principal policy against an action and resource
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/app-role \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::my-bucket/reports/2026.csv
Many AccessDenied messages now include the type of policy that denied the request (for example “with an explicit deny in a service control policy”). Read the full message before changing anything.
The policy simulator does not evaluate every layer the same way the real request does (for example some resource policies and SCP details). Use it to narrow the cause, then confirm with CloudTrail.
Rules of thumb
- Look for Deny statements first; they are the most common cause and the fastest to rule out.
- Check the resource ARN format: bucket ARN versus object ARN (
bucket/*) is a classic mistake. - For encrypted data, the KMS key policy is part of the decision.