How IAM Policy Evaluation Actually Works

December 21, 2026 · 16 min read

Solutions Architect Professional · SAP-C02 · part of The Exam Room

The situation

Three separate incidents land in the platform team’s inbox on the same Monday morning.

Incident one. A developer in account 123456789012 has an IAM user with the AWS-managed AdministratorAccess policy attached. They open the console, navigate to their user, click Create access key, and get:

User: arn:aws:iam::123456789012:user/dev-jas is not authorized to perform:
iam:CreateAccessKey on resource: arn:aws:iam::123456789012:user/dev-jas
with an explicit deny in a service control policy

The policy on the user is */*. They’re an administrator. They cannot create an access key.

Incident two. An engineer assumes a role called EngineeringRole in a workload account. The role has a Permissions Boundary attached called EngineeringBoundary, which the engineer has read and understood as “only allows EC2 actions”. The engineer attaches an inline identity policy to the role granting s3:GetObject on a specific bucket. They expect it to do nothing, the boundary shouldn’t permit S3. Then they test it, and the S3 read works. They raise a ticket saying the Permissions Boundary is broken.

Incident three. A Lambda function in account A (111111111111) is trying to write to an S3 bucket in account B (222222222222). The Lambda’s execution role has an identity policy granting s3:PutObject on arn:aws:s3:::reports-b/*. The bucket policy on reports-b in account B contains a Principal block listing arn:aws:iam::111111111111:role/LambdaWriter and an action of s3:PutObject. Both sides appear to grant the action. The PutObject call returns AccessDenied.

Each looks like a different bug. They aren’t.

What actually matters

Three incidents, three shapes of confusion, one underlying model. Before chasing each ticket as its own mystery it’s worth naming what the three have in common.

The first property is that AWS authorisation is a pipeline, not a single check. Most engineers carry a mental model where a request is either allowed or denied by “the policy”, usually meaning the identity policy attached to the role. That’s one of six policy types. Understanding the pipeline is the difference between “this is broken” and “this is working exactly as designed, just not in the order I expected”.

The second is that the pipeline starts from a default deny. Nothing is allowed by accident. Every access granted is a chain of Allow statements reaching from some policy to the specific action on the specific resource. Absence of a grant is a deny; this is the rule that makes empty policies secure. An identity policy with no S3 statement doesn’t merely “not mention S3”, it actively fails to authorise S3, because there’s nothing to upgrade the default deny into an allow.

The third is that explicit deny wins, everywhere, immediately. Any Deny in any of the six policy types ends evaluation with a final decision of Deny. This is how SCPs beat AdministratorAccess (incident one) and how organisation-wide guardrails work without rewriting individual role policies. The SCP doesn’t need to “override” the identity policy; it just sits earlier in the pipeline and answers Deny first.

The fourth is that some policy types only restrict, never grant. SCPs, RCPs, and Permissions Boundaries are written with Allow statements, but those aren’t grants. They’re filters, saying “the effective permissions cannot be larger than this set”. An identity policy that grants s3:GetObject works if and only if the boundary’s Allow list also includes s3:GetObject. That’s what incident two is hiding: the boundary actually includes s3:Get*, so the intersection includes the read, so the read succeeds.

The fifth is that cross-account evaluation is two evaluations, not one. When a principal in account A touches a resource in account B, AWS runs the full pipeline once in each account’s context and both must independently return Allow. Same-account is a union (“identity policy or resource policy allows” is enough). Cross-account is an intersection. This is why both sides of incident three need to grant explicitly, and it’s why missing one side produces a silent-looking deny.

And the sixth is subtle: every API call in a request path has its own authorisation. A PutObject to an SSE-KMS-encrypted bucket is not one authorisation decision; it’s a PutObject plus a KMS GenerateDataKey, each with its own six-gate evaluation. Incident three’s bug isn’t the S3 policy; it’s the KMS policy the team forgot existed.

What we’ll filter on

  1. Six policy types, fixed evaluation order.
  2. Default deny is the starting state; absence of a grant is a deny.
  3. Explicit deny in any policy type ends evaluation with Deny.
  4. Guardrails (SCPs, RCPs, Permissions Boundaries) restrict; they do not grant.
  5. Cross-account requires both accounts to independently allow.
  6. Every API call in the request path authorises separately.

The six policy types

AWS’s evaluator considers up to six types of policy. Not all six apply to every request. The list is fixed.

1. Service Control Policies (SCPs)

Attached to OUs or accounts in AWS Organizations. Apply to principals in member accounts, including the root user, including anyone with AdministratorAccess. Never grant; only limit. Do not affect the management account.

2. Resource Control Policies (RCPs)

The newer sibling of SCPs (GA November 2024). Attached in Organizations; apply to resources across member accounts. Limit what resource-based policies can grant, regardless of who the principal is. Cover S3, SQS, KMS, Secrets Manager, and STS at launch. Never grant; only limit.

3. Permissions Boundaries

Attached to an individual IAM user or role. Cap the permissions that that principal's identity policies can grant. Never grant; only limit. Not inherited, not organisation-wide, just a ceiling on one principal.

4. Identity-based policies

Attached to IAM users, groups, or roles. The standard grant mechanism. Can both allow and deny. This is where most day-to-day permissions live.

5. Resource-based policies

Attached to resources themselves: S3 bucket policies, KMS key policies, SQS queue policies, Lambda function policies, IAM role trust policies. Name a Principal; can grant cross-account access. Can allow and deny.

6. Session policies

Passed inline during sts:AssumeRole, AssumeRoleWithSAML, or AssumeRoleWithWebIdentity. Scope-down the assumed role's permissions for this session only. Effective permissions are the intersection of the role's identity policy and the session policy.

Three of the six (SCPs, RCPs, Permissions Boundaries) are guardrails, they never grant permission on their own. The remaining three (identity, resource, session) are where actual allows come from. Every Allow originates from one of those three; every Deny can come from any of the six.

Side by side

Policy type Grants Restricts Scope Applies in mgmt account
SCP Principals in member accounts
RCP Resources in member accounts
Permissions Boundary One user or role
Identity policy Attached principal
Resource policy Attached resource
Session policy ✓ (intersects) One session

The evaluation order

Every request begins with a default deny. The evaluator’s job is to find a reason to upgrade that deny into an allow, checking along the way whether any policy type has an explicit deny that would veto it.

Incident 1: SCP deny AdministratorAccess beaten Incident 2: boundary read boundary actually allows S3 Incident 3: cross-account KMS is the missed API call CreateAccessKey user: dev-jas identity: AdministratorAccess SCP: Deny iam:CreateAccessKey s3:GetObject role: EngineeringRole boundary: ec2:*, s3:Get*, ... identity adds: s3:GetObject s3:PutObject (x-account) A: identity allows PutObject B: bucket policy allows role bucket has SSE-KMS default Gate 1: explicit deny? yes Gate 4: identity allow? yes S3 PutObject: both sides allow evaluation ends immediately identity grant never considered error cites SCP explicitly Gate 5: boundary allow? boundary includes s3:Get* intersection passes kms:GenerateDataKey triggered separate cross-account eval KMS policy missing principal Deny explicit deny wins SCP beats identity allow gate 1 is final Allow boundary is a ceiling read every Action line not the name Deny (via KMS) PutObject eval passes GenerateDataKey fails surfaces as S3 AccessDenied
Three incidents, three different gates failing. The vocabulary of "the policy" can't describe any of them.

Things worth spelling out:

  • Explicit deny anywhere wins. A Deny in any of the six types ends evaluation with a final decision of Deny. This is why an SCP Deny silently defeats an AdministratorAccess identity policy.
  • Guardrails don’t grant, they pass through. SCPs, RCPs, and Permissions Boundaries are written with Allow statements, but those aren’t grants, they’re permissions passed through to the next layer. If the boundary lacks an allow for the action, the action is implicitly denied even though the identity policy grants it cleanly.
  • Same-account gate 4 is a union of identity and resource policy. Either allow is enough, which is why a bucket policy can grant access to a principal whose identity policy says nothing about the bucket.
  • Cross-account gate 4 is not a union. AWS runs the evaluation twice, once in the principal’s account, once in the resource’s account, and both must independently return Allow.
  • Session policies only exist when a role was assumed with one. A plain role assumption skips gate 6.

Incident one, in depth

The cleanest demonstration of the explicit-deny-anywhere-wins rule. An SCP attached to the OU containing 123456789012 reads:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyAccessKeyCreationOrgWide",
    "Effect": "Deny",
    "Action": "iam:CreateAccessKey",
    "Resource": "*"
  }]
}

Walk that through the gates. Gate 1 scans for explicit denies. The SCP contains one. Game over. Final decision: Deny. Gates 2 onwards never run.

Two nuances. SCPs do not affect the management account, if the developer’s IAM user were there, the SCP would be inert. The error message tells you it was an SCP, the phrase “with an explicit deny in a service control policy” appears when that was the cause. Attribution covers SCPs, RCPs, VPC endpoint policies, session policies, and Permissions Boundaries.

Incident two, in depth

A Permissions Boundary is a ceiling, not an allow-list of what the engineer is supposed to do. The engineer looked at EngineeringBoundary and saw:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["ec2:*", "s3:Get*", "s3:List*", "cloudwatch:*", "logs:*", "iam:PassRole"],
    "Resource": "*"
  }]
}

They summarised it as “allows EC2 actions” because that’s the name and the line they spot-checked. But the ceiling explicitly includes s3:Get* and s3:List*. When the identity policy grants s3:GetObject, the intersection at gate 5 is s3:GetObject on bucket X, and the read works.

The rule: to read a Permissions Boundary, read every Action line. Not the name. Not the one Action that caught your eye.

Incident three, in depth

LambdaWriter role in account A tries s3:PutObject on arn:aws:s3:::reports-b/sales.csv in account B. Walk the gates.

Gate 1, explicit-deny scan. No explicit Deny anywhere. Continue.

Gate 2. SCPs. SCPs on account A’s path allow s3:PutObject via FullAWSAccess. Continue.

Gate 3. RCPs. No RCP blocks the write. Continue.

Gate 4, identity/resource, cross-account. AWS runs two independent evaluations. Account A: does the role’s identity policy allow s3:PutObject? Yes. Account B: does the bucket policy allow the ARN? Yes. Both pass.

Both sub-evaluations allow. Why does the call still fail?

The reports-b bucket has default SSE-KMS encryption with a customer-managed key in account B. When Lambda writes, S3 calls kms:GenerateDataKey on that key on behalf of the caller, a separate authorisation decision with its own six-gate evaluation. For cross-account KMS the dual-allow rule applies: the caller’s identity policy must allow kms:GenerateDataKey on the key ARN, and the key policy in account B must name the caller as a Principal.

The role’s identity policy allowed s3:PutObject but said nothing about kms:*. The bucket policy can’t help because KMS permissions live in the key policy, not the bucket policy. Both S3 sub-evaluations pass; the KMS sub-call’s account-A evaluation fails at gate 4 with an implicit deny. S3 surfaces this as AccessDenied on the PutObject.

The fix: add kms:GenerateDataKey (and kms:Decrypt for round-trips) on the key ARN to the Lambda role’s identity policy, and add the Lambda role ARN as a Principal in the KMS key policy for the same actions.

Resource-policy edge cases

Same-account and cross-account take the same gates in the same order, but gate 4’s answer differs.

Same-account, resource policy grants to an IAM user ARN. Alice’s bucket policy allow is enough on its own; her identity policy can say nothing about the bucket. “Resource-based policies that grant permissions to an IAM user ARN are not limited by an implicit deny in an identity-based policy or permissions boundary.”

Same-account, resource policy grants to an IAM role ARN. The boundary and identity policy do apply. The resource-policy allow has to pass gate 5.

Same-account, resource policy grants to a role session ARN (e.g. arn:aws:sts::A:assumed-role/AppRole/session). The grant goes directly to the session and bypasses the role’s boundary and identity-policy implicit deny.

Cross-account. The union rule of gate 4 does not apply. Both sides’ evaluations must independently decide Allow.

What’s worth remembering

  1. Six policy types, fixed order. SCPs, RCPs, and Permissions Boundaries are guardrails. Identity, resource, and session policies are where allows come from.
  2. Default deny is the starting state. No allow found, or any explicit deny seen, and the default deny holds.
  3. Explicit deny anywhere wins. A Deny in any of the six types ends evaluation immediately, including SCP denies against AdministratorAccess.
  4. SCPs affect member accounts only. They apply to every principal including root; they do not apply in the management account.
  5. Permissions Boundaries are ceilings, not replacements. Read every Action line. Effective permission is the intersection of boundary and identity policy.
  6. Same-account gate 4 is a union of identity and resource policies. Cross-account gate 4 is a double evaluation.
  7. RCPs are the resource-side organisational guardrail for S3, SQS, KMS, Secrets Manager, and STS.
  8. Resource-policy grants to IAM user ARNs in the same account bypass the boundary implicit deny.
  9. Every API call in the request path has its own authorisation. S3 PutObject to an SSE-KMS bucket is also a KMS GenerateDataKey.
  10. The error message names the policy type when attribution applies, trust it.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.