AWS IAM Policy Evaluation Logic
Introduction¶
Every AWS API call is authorized by the same algorithm across all policy types. Memorising the order turns "access denied" from guesswork into a checklist.
The decision, in order¶
- Default: deny. Nothing is allowed until something allows it.
- Explicit
Denyanywhere → denied, full stop. No laterAllowoverrides it. This is checked across all policy types. - Service Control Policy (SCP) — for accounts in an Organization. If the action isn't allowed by every SCP from the root down to the account, it's denied (SCPs are a permission ceiling, they don't grant).
- Resource-based policy — an
Allowhere can be enough on its own for same-account access (and is required for cross-account). - Permission boundary — if set on the principal, the action must be allowed by the boundary too (another ceiling).
- Session policy — if the credentials came from
AssumeRolewith an inline session policy, that's another ceiling. - Identity-based policy — an
Allowattached to the user/role/group.
Result is allow only if: no explicit deny, and it passes every applicable
ceiling (SCP, boundary, session), and at least one Allow exists (identity
policy or resource policy).
Same-account vs cross-account¶
- Same account: an
Allowin either the identity policy or the resource policy is sufficient (for services that support resource policies). - Cross-account: you need an
Allowin both — the identity policy in the caller's account and the resource policy in the resource's account. Either one missing = denied.
Policy structure¶
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadBucket",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"IpAddress": { "aws:SourceIp": "203.0.113.0/24" }
}
}
]
}
Common mistakes:
s3:ListBucketacts on the bucket ARN (:::my-bucket),s3:GetObjecton the object ARN (:::my-bucket/*). Listing them under oneResourcewith only/*breaksListBucket.- A
Conditionthat never matches (wrong key,aws:SourceIpwhen traffic comes via a VPC endpoint — useaws:VpcSourceIp/aws:sourceVpce). NotAction/NotResource— powerful and easy to get backwards.
Conditions and least privilege¶
aws:PrincipalTag/aws:ResourceTagfor attribute-based access control.aws:MultiFactorAuthPresentto require MFA.aws:SourceIp,aws:SourceVpc,aws:SourceVpceto fence access.s3:prefix,dynamodb:LeadingKeysto scope within a resource.
Verification and troubleshooting¶
# Simulate before deploying
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/app \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::my-bucket/key.txt
# Decode the encoded message in an AccessDenied error
aws sts decode-authorization-message --encoded-message "<blob>"
AccessDeniedwith an "explicit deny" in the decoded message — an SCP, a permission boundary, or aDenystatement. Check SCPs first (aws organizations describe-policy), then the principal's boundary, then all attached policies for"Effect": "Deny".AccessDenied, no explicit deny mentioned — nothing allowed it. MissingAllow, wrongResourceARN, or aConditionthat didn't match.- Works for you, not for a role / CI — you have extra policies (admin group), or you're not subject to the SCP/boundary the role is. Simulate as the role.
- Cross-account call denied despite the identity policy allowing it — the resource policy (bucket policy, KMS key policy, role trust policy) doesn't allow the caller. Both sides required.
- KMS
AccessDeniedon an S3GetObjectthat "should work" — the object is SSE-KMS encrypted; you also needkms:Decrypton the key, and the key policy must allow you. - IAM change not taking effect — IAM is eventually consistent; wait a minute. Also, an active session's session policy is fixed until re-assumed.
iam:PassRoledenied — passing a role to a service (EC2, Lambda) needs explicitiam:PassRolepermission for that role ARN, separate from creating the resource.
Related tools and reading¶
- On-site: JSON formatter.
- Related posts: Security groups vs NACLs, EC2 instance metadata and IMDSv2.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Cloud & AWS
AWS WAF: The Rules That Matter
AWS WAF filters HTTP requests at the ALB/CloudFront/API Gateway edge. Here's a sensible baseline Web ACL, how to tune with count mode, and the false-positive traps.
October 24, 2026An S3 Static Site Behind CloudFront
The right way to host a static site on AWS: a private S3 bucket, CloudFront with OAC in front, HTTPS via ACM, and SPA fallback routing. Plus the 403s that trip everyone.
October 24, 2026CloudWatch Logs and Metric Filters
CloudWatch Logs is cheap to write and expensive to forget about. Here's retention, turning error patterns into metrics and alarms, and Logs Insights for ad-hoc queries.
October 24, 2026