AWS IAM policy evaluation: why AccessDenied happens and how to fix it
This guide is for developers who keep hitting AWS AccessDenied and want to know which policy layer actually blocked the request. You’ll get a concrete mental model for how identity policies, resource policies, SCPs, permission boundaries, and explicit Deny combine, plus commands you can run today to diagnose real failures.
TL;DR — AWS authorization is not "first matching policy wins." A request is allowed only if some applicable policy explicitly allows it and no applicable policy anywhere explicitly denies it; SCPs and permission boundaries act as ceilings, and resource policies can grant access only within those ceilings. The single most likely fix for confusing
AccessDeniedis to identify which policy type is in play for the caller, then check for an explicitDenyor a missingAllowat that layer before editing the wrong policy. Reading time: ~7 min
What it is and where it sits
AWS IAM policy evaluation logic is the authorization engine AWS runs after authentication and before the target service executes the API call. In a normal request path, your SDK/CLI signs a request with credentials, AWS authenticates the principal, gathers all relevant policy documents, evaluates them together, and only then hands the request to S3, KMS, Lambda, DynamoDB, or whatever service you called.
This is the layer that answers: "Can this principal perform Action on this Resource under these request conditions?"
The important architectural point: there is no single policy file. Authorization can be influenced by multiple policy families attached in different places:
- Identity-based policies on a user or role
- Resource-based policies on the target resource, such as an S3 bucket policy or KMS key policy
- Service Control Policies (SCPs) at the AWS Organizations level
- Permission boundaries on the principal
- Session policies on temporary credentials
- Explicit
Denyin any applicable policy
For most developers, the practical replacement is not "what replaces what," but "what layer overrides what." If you came from a simpler RBAC system, IAM evaluation replaces the idea that one role assignment alone determines access.
App / aws cli / SDK
|
| signed API request
v
AWS authn (who are you?)
|
v
Policy evaluation engine
- identity policies
- resource policies
- permission boundary
- session policy
- SCP / RCP if applicable
- explicit Deny check
|
+--> Deny -> AccessDenied error returned
|
v
Target service executes request
Where this shows up in real life:
aws s3 cp ...fails even though the role hass3:*- Lambda can assume a role but still cannot call KMS
- Cross-account access works for one bucket and fails for another
- A platform team adds an SCP and suddenly deployments break in one OU
How it actually works
The shortest accurate mental model is:
- Start from implicit deny.
- Collect all applicable policies for the request context.
- If any applicable statement says
Effect: Deny, final answer is deny. - Otherwise, the request must be allowed by the relevant allow path.
- SCPs, permission boundaries, and session policies do not grant permissions by themselves; they limit what an identity-based or resource-based allow can achieve.
One end-to-end example
Scenario: a CI role in account 111111111111 tries to upload an artifact to an S3 bucket in account 222222222222.
The command:
aws s3api put-object \
--bucket shared-artifacts-prod \
--key builds/app-42.tar.gz \
--body app-42.tar.gz
The caller identity:
aws sts get-caller-identity
Typical output:
{
"UserId": "AROAEXAMPLE:github-actions-run-1842",
"Account": "111111111111",
"Arn": "arn:aws:sts::111111111111:assumed-role/ci-deploy-role/github-actions-run-1842"
}
You inspect the role and find an identity policy that allows S3 writes:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::shared-artifacts-prod/*"
}
]
}
So why can it still fail? Walk the evaluation in order.
Step 1: Identity policy says "yes"
The role policy allows s3:PutObject on the object ARN. Good, but not sufficient.
Step 2: Permission boundary must also allow it
If the role has a permissions boundary attached, AWS intersects the role's effective permissions with that boundary. If the boundary omits s3:PutObject or explicitly denies it, the request is denied.
This is the common misunderstanding: a boundary is not an extra allow; it is a max-permissions filter.
Step 3: Session policy must also allow it
If the role was assumed with a session policy, that policy can further reduce permissions. This is common in federation and some CI setups.
Step 4: SCP must also allow it
If account 111111111111 is in an AWS Organization, the SCP attached to its root/OU/account must not block s3:PutObject. An SCP deny wins. Also, if your org uses allow-list style SCPs, the action must be included there.
Step 5: Resource policy on the bucket must allow cross-account access
Because this is cross-account S3 access, the bucket policy in account 222222222222 usually needs to allow the principal from account 111111111111.
A working bucket policy statement might look like:
{
"Sid": "AllowCIRoleWrite",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111111111111:role/ci-deploy-role"},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::shared-artifacts-prod/builds/*"
}
If that statement is missing, the cross-account write is denied even though the caller's own role policy says allow.
Step 6: Any explicit deny anywhere ends it
Now the killer edge case. Suppose the bucket policy also contains:
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::shared-artifacts-prod/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
}
Your CLI command did not set server-side encryption headers, so this explicit deny matches. Final result: deny.
Typical error shape:
aws s3api put-object \
--bucket shared-artifacts-prod \
--key builds/app-42.tar.gz \
--body app-42.tar.gz
An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
The fix is not to broaden the role policy. The fix is to satisfy or change the deny condition:
aws s3api put-object \
--bucket shared-artifacts-prod \
--key builds/app-42.tar.gz \
--body app-42.tar.gz \
--server-side-encryption AES256
That example captures the real evaluation model:
- Identity policy provided an allow
- Bucket policy provided a needed cross-account allow
- Explicit deny in bucket policy overrode both
- SCP/boundary/session policy could also have blocked it even if the bucket policy were perfect
When to use it (and when not to)
Use this model any time you are debugging AWS authorization beyond a single-account, single-role setup.
| Scenario | Recommendation |
|---|---|
| Single account app role cannot call one service | Start with identity policy simulation and check for permission boundary or session policy before touching resource policies |
| Cross-account S3, KMS, SNS, SQS access | Evaluate both sides: caller identity policy and target resource policy |
| Entire OU suddenly loses an action after platform changes | Check SCPs first |
| Delegated admin wants teams to create roles but not exceed a cap | Use permission boundaries |
| You want a simple app-local authorization model | You probably don't need to think about SCPs or boundaries unless your org/platform team already uses them |
| You are trying to grant access by attaching only an SCP | Don’t; SCPs limit permissions, they do not grant them |
You are trying to fix AccessDenied by adding Action: * to a role | Don’t; explicit deny or missing resource policy will still block you |
You probably don’t need deep IAM evaluation analysis if all of these are true:
- same account
- no Organizations SCPs
- no permission boundaries
- no temporary session policy
- target service does not use a resource policy for your case
In that narrow case, it’s often just an identity policy ARN mismatch or a condition mismatch.
Trade-offs
Every control layer gives safety, and every layer adds another place to break deployments.
| Benefit | Cost |
|---|---|
| SCPs prevent entire classes of dangerous actions across accounts | Centralized blast radius when misconfigured; debugging requires org-level visibility many app teams don’t have |
| Permission boundaries enable safe delegation | Harder mental model; developers think they granted access when they only edited the role policy |
| Resource policies enable cross-account access without role chaining in some cases | Two-sided configuration; drift between identity and resource policy is common |
| Explicit deny is reliable guardrail logic | One broad deny with a condition can silently override many intended allows |
| Conditions let you enforce encryption, source VPCe, tags, MFA, regions | Condition keys are brittle; wrong key/operator means confusing AccessDenied |
Latency and cost are usually not the issue here. IAM evaluation is part of the control plane path and not something you pay for per policy statement in a way that changes architecture decisions for normal workloads. The real cost is operational: slower diagnosis, more coordination between app teams and platform/security teams, and higher chance of outages from policy changes.
In practice
Simulate what IAM thinks before changing policies
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111111111111:role/ci-deploy-role \
--action-names s3:PutObject \
--resource-arns arn:aws:s3:::shared-artifacts-prod/builds/app-42.tar.gz \
--context-entries ContextKeyName=s3:x-amz-server-side-encryption,ContextKeyType=string,ContextKeyValues=AES256
Typical output shape:
{
"EvaluationResults": [
{
"EvalActionName": "s3:PutObject",
"EvalResourceName": "arn:aws:s3:::shared-artifacts-prod/builds/app-42.tar.gz",
"EvalDecision": "allowed",
"MatchedStatements": [
{
"SourcePolicyId": "ci-deploy-role-inline",
"StartPosition": {"Line": 5, "Column": 7},
"EndPosition": {"Line": 10, "Column": 8}
}
],
"MissingContextValues": []
}
]
}
What it does: tests the identity side of the decision for a specific principal/action/resource, optionally with condition context. Gotcha: this does not fully evaluate every resource-policy scenario the same way the live service does, so treat it as a strong hint, not the final truth for cross-account resource access.
Inspect whether a role is capped by a permissions boundary
aws iam get-role --role-name ci-deploy-role
Typical output shape:
{
"Role": {
"Path": "/",
"RoleName": "ci-deploy-role",
"RoleId": "AROAXXXXX",
"Arn": "arn:aws:iam::111111111111:role/ci-deploy-role",
"CreateDate": "2026-03-10T12:44:18+00:00",
"AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": [...]},
"PermissionsBoundary": {
"PermissionsBoundaryType": "Policy",
"PermissionsBoundaryArn": "arn:aws:iam::111111111111:policy/team-boundary"
}
}
}
Then fetch the boundary policy:
aws iam get-policy-version \
--policy-arn arn:aws:iam::111111111111:policy/team-boundary \
--version-id v7
What it does: confirms whether the role has a boundary and lets you inspect the actual cap. Gotcha: teams often edit the role inline policy and forget the boundary exists; the boundary wins by omission even without an explicit deny.
Read the resource policy that may be denying you
⚠️ Editing a bucket policy can break production reads/writes immediately for multiple principals. Export the current policy first and review the diff before applying changes.
aws s3api get-bucket-policy --bucket shared-artifacts-prod --query Policy --output text | jq .
If you find a deny like DenyUnencryptedUploads, test the corrected request instead of weakening the policy:
aws s3api put-object \
--bucket shared-artifacts-prod \
--key builds/app-42.tar.gz \
--body app-42.tar.gz \
--server-side-encryption AES256 \
--debug
What it does: shows the live bucket policy and retries the request with the condition satisfied. Gotcha: --debug is noisy but useful; look for the final service error and request parameters, not just the SigV4 chatter.
Further reading
- AWS IAM User Guide: Policy evaluation logic
- AWS Organizations User Guide: Service control policies
- AWS IAM User Guide: Permissions boundaries for IAM entities
- Amazon S3 User Guide: Bucket policies and user policies
- AWS IAM Policy Simulator documentation
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI