Server

AWS IAM Policy Evaluation Logic

By Prabath Thalangama· October 24, 2026· 3 min read
#aws#iam#security

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

  1. Default: deny. Nothing is allowed until something allows it.
  2. Explicit Deny anywhere → denied, full stop. No later Allow overrides it. This is checked across all policy types.
  3. 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).
  4. Resource-based policy — an Allow here can be enough on its own for same-account access (and is required for cross-account).
  5. Permission boundary — if set on the principal, the action must be allowed by the boundary too (another ceiling).
  6. Session policy — if the credentials came from AssumeRole with an inline session policy, that's another ceiling.
  7. Identity-based policy — an Allow attached 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 Allow in either the identity policy or the resource policy is sufficient (for services that support resource policies).
  • Cross-account: you need an Allow in 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:ListBucket acts on the bucket ARN (:::my-bucket), s3:GetObject on the object ARN (:::my-bucket/*). Listing them under one Resource with only /* breaks ListBucket.
  • A Condition that never matches (wrong key, aws:SourceIp when traffic comes via a VPC endpoint — use aws:VpcSourceIp / aws:sourceVpce).
  • NotAction / NotResource — powerful and easy to get backwards.

Conditions and least privilege

  • aws:PrincipalTag / aws:ResourceTag for attribute-based access control.
  • aws:MultiFactorAuthPresent to require MFA.
  • aws:SourceIp, aws:SourceVpc, aws:SourceVpce to fence access.
  • s3:prefix, dynamodb:LeadingKeys to 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>"
  • AccessDenied with an "explicit deny" in the decoded message — an SCP, a permission boundary, or a Deny statement. 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. Missing Allow, wrong Resource ARN, or a Condition that 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 AccessDenied on an S3 GetObject that "should work" — the object is SSE-KMS encrypted; you also need kms:Decrypt on 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:PassRole denied — passing a role to a service (EC2, Lambda) needs explicit iam:PassRole permission for that role ARN, separate from creating the resource.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.