IAM Policy Evaluation Logic Step-by-Step - Explicit Deny, SCP, RCP, Resource Policy, Identity Policy, Permission Boundary, and Session Policy
First Published:
Last Updated:
The article is a deliberate companion to the IAM Policy Simulator (Offline) on this site. Every walk-through case carries a Reproduce in Simulator? badge (YES, PARTIAL, or NO) and, for YES cases, a three-step recipe to replay the case in the Simulator. The NO cases are exactly the situations where the Simulator's documented scope ends — SCP, RCP, Permission Boundary, Session Policy — and the article spells out how to reason through them with public AWS documentation when an online sandbox is not available.
If you want the evaluation order, see §2 and the IAM Decision Diamond figure. If you came here to walk twelve concrete request shapes through the Diamond, jump straight to §3. If you want to wire the Simulator into your review workflow, jump to §6.
Table of Contents:
1. Why IAM Policy Evaluation is Confusing
AWS IAM evaluation now spans seven layers — six configurable policy types (Identity-Based, Resource-Based, Permission Boundary, SCP, RCP, Session) plus the universal Explicit Deny check that fires across all of them — accumulated over fifteen years of production hardening, and each layer was added to solve a real production problem. The result is a system where a single API request — one principal, one action, one resource — is filtered through up to seven separate evaluations before the request returnsAllow or AccessDenied. The complexity is not accidental. It reflects fifteen years of layered defense-in-depth requirements. But it is unforgiving: misread any one of the seven layers and you ship a policy that looks correct in review and evaluates to the opposite of what you intended.This section addresses why the confusion persists, lists the five most common misconceptions we see in real reviews, and shows the Scope Map that tells you, at a glance, which parts of evaluation can be reproduced in our offline Simulator and which parts cannot.
1.1 Seven Evaluation Layers in One Request
When an AWS service evaluates an API call, every applicable layer below is checked. None is skipped, even if not configured — an unconfigured ceiling simply does not deny. The canonical evaluation order is shown in §2 and the IAM Decision Diamond figure; the list below introduces each layer category-by-category.- Identity-based policies — IAM user, group, and role policies attached to the calling principal.
- Resource-based policies — bucket policies, KMS key policies, SQS access policies, Secrets Manager resource policies, Lambda resource policies, IAM role trust policies.
- Permission Boundary — a managed policy that caps the maximum effective permissions of an IAM user or role.
- Service Control Policy (SCP) — an Organizations-level policy that caps the maximum permissions for principals in an account or OU.
- Resource Control Policy (RCP) — an Organizations-level policy that caps what can be done to resources in an account or OU. New since November 2024.
- Session policy — a policy passed at AssumeRole time as the
PolicyorPolicyArnsparameter, which intersects with the role's effective permissions for the resulting session. - Implicit / Explicit Deny check — not a policy type itself but a cross-cutting evaluation step: the request is denied implicitly when nothing allows it, or explicitly when any of the above contains a matching
"Effect": "Deny".
1.2 Five Common Misconceptions
In code review and incident review, the same five misconceptions account for more than half of all IAM-related findings:- "If I have an Allow, the request goes through." A single Allow is necessary but not sufficient. Any SCP, RCP, Permission Boundary, or Session Policy that does not include the action will quietly cause an Implicit Deny at that ceiling. Conversely, any Explicit Deny anywhere in the seven layers overrides every Allow.
- "Deny only matters when I write it." The default for AWS IAM is Implicit Deny. A statement that does not grant anything is, in effect, denying everything that is not granted elsewhere. Engineers who internalize "explicit Allow" frequently forget that "nothing matches" produces the same outcome as
"Effect": "Deny"— and the two are usually indistinguishable in CloudTrail. - "SCP Allow means access is granted." An SCP is a filter, not a grant. The default
FullAWSAccessSCP says "do not constrain the account", not "everyone in the account has full AWS access". You still need an identity policy or resource policy that grants the specific action. - "Identity policy and resource policy fight." They do not fight; they combine. In same-account access, the request is allowed if either policy allows it (union semantics). In cross-account access, both sides must allow (intersection semantics). The context-dependent switch between union and intersection is one of the largest single sources of confusion.
- "Permission Boundary is just another permission set." A Permission Boundary does not grant anything. If a role has Identity Policy
s3:PutObjectand Permission Boundarys3:GetObject, the effective permission is nothing. The intersection of{PutObject}and{GetObject}is empty. Treating the Boundary as a permission grant rather than a ceiling produces silent Implicit Denies that are extremely hard to debug.
1.3 Scope Map — What This Article and the Simulator Cover
The companion IAM Policy Simulator (Offline) on this site evaluates Identity Policy + Resource Policy + Condition keys, but does not model SCP, RCP, Permission Boundary, or Session Policy. That is a deliberate scope decision: the Simulator is a single-page offline tool you can run with no AWS account, no IAM permissions, and no network call. The price is that four of the seven Diamond stages must be reasoned about by hand.This article fills in the missing four. Every walk-through case carries one of three coverage badges:
- YES — the case can be fully reproduced in the Simulator.
- PARTIAL — the Simulator returns a result, but the result alone is not the full story (typically: cross-account, where Simulator does not enforce both-sides-required).
- NO — the case depends on a Diamond stage the Simulator does not model. The article tells you what AWS Policy Simulator (the AWS-hosted official one) or a sandbox account would show, and walks through the reasoning.
2. The IAM Decision Diamond
The Diamond is a single seven-stage flowchart that names every stage of AWS IAM evaluation. We use the name The IAM Decision Diamond because the shape of the evaluation is genuinely diamond-like: the request enters at the top, passes through a fixed sequence of decision boxes, and exits at the bottom either asAllow or as Deny (Implicit or Explicit). At every stage, an Explicit Deny short-circuits the entire evaluation to Deny without looking at later stages.The figure below shows the canonical order. Carry this figure in your head. Most IAM debugging time is spent at one of these seven nodes.
2.1 How to Read the Diamond
Two visual conventions matter:- Vertical flow — top to bottom in the order Explicit Deny, SCP, RCP, Resource Policy, Identity Policy, Permission Boundary, Session Policy. Every request passes through every stage that applies.
- Side arrows to DENY — any stage can short-circuit the evaluation to
Deny. Some stages can produceImplicit Deny(no match) and some can produceExplicit Deny(a matching Deny statement). The distinction matters for debugging, even though both produceAccessDeniedin the API response.
- Intersection (ceiling) stages — SCP, RCP, Permission Boundary, Session Policy. These do not grant; they cap. The request must be allowed by every applicable ceiling.
- Union (granting) stages — Resource Policy and Identity Policy. In same-account access, either one is sufficient. In cross-account access, both are required.
2.2 Stage 1: Explicit Deny
Explicit Deny is the only universal short-circuit in the entire system. If any of the seven policy types contains a matching"Effect": "Deny" statement, the evaluation halts and returns AccessDenied immediately. There is no "Allow that overrides Deny", anywhere, ever. This is the one absolute rule in IAM.A pedagogical note: AWS does not perform a separate "Deny-only" pre-pass before evaluating other stages. Explicit Deny is checked within each of the other six stages as their policy documents are parsed. We label it Stage 1 in the Diamond because it short-circuits the rest of the evaluation the moment it matches at any layer — treating it as the first thing to look for is the right reading order when debugging an
AccessDenied.The implication is that Deny is the strongest possible IAM primitive. Use it sparingly and use it precisely — a broad
"Resource": "*" Deny in an Identity policy will produce confusing failures across actions you did not intend to restrict. But a tight, narrow Explicit Deny is the most reliable way to guarantee a behavior across an organization. SCPs and RCPs are built on top of this guarantee.2.3 Stage 2: Service Control Policy (SCP)
SCPs apply at the AWS Organizations level (root, OU, or account). They cap the maximum permissions for principals in the affected account: every IAM user, role, and federated identity in that account is constrained by the union of the SCPs at the root, the OU chain, and the account itself.Two SCP modes exist:
- Allow-list mode — the SCP enumerates allowed actions. Everything not listed is denied. The default
FullAWSAccesspolicy is an allow-list with"Action": "*","Resource": "*". - Deny-list mode — the SCP enumerates denied actions. Everything else passes the SCP filter. Most production SCPs are deny-list.
FullAWSAccess from an OU does not "remove the default access"; it makes every action in that OU implicitly denied at the SCP layer, regardless of what identity policies say. The first principle of SCP design: keep FullAWSAccess attached at the root unless you understand exactly which actions will become impossible.2.4 Stage 3: Resource Control Policy (RCP)
RCPs are the resource-side counterpart of SCPs, introduced in November 2024. Where SCPs constrain what principals in an Organization account can do, RCPs constrain what can be done to resources in an Organization account. The set of services that support RCPs is smaller than SCPs (S3, STS, KMS, SQS, and Secrets Manager at launch) and growing.The mental model: SCP is a perimeter around the caller; RCP is a perimeter around the resource. If the caller's account has an SCP that allows
s3:PutObject and the resource's account has an RCP that denies s3:PutObject from external principals, the request fails at the RCP stage, not the SCP stage.RCPs are particularly useful for stopping accidental public-bucket configurations across an entire OU without depending on every account's bucket policy being correct.
2.5 Stage 4: Resource-Based Policy
Resource-based policies attach to the resource rather than the principal. The canonical examples are S3 bucket policies, KMS key policies, SQS queue policies, Secrets Manager resource policies, Lambda resource policies, and IAM role trust policies. (The role trust policy is technically a resource policy attached to the role-as-resource.)A resource policy lists
Principal explicitly. Without an explicit Principal matching the caller, the resource policy does not grant the caller anything.The key behavior:
- Same-account access — the request is allowed if either the resource policy or the identity policy allows it. Union semantics.
- Cross-account access — the request requires both a resource policy on the resource side (the resource owner explicitly grants access to the external principal) and an identity policy on the caller side. Intersection semantics.
2.6 Stage 5: Identity-Based Policy
Identity-based policies attach to the principal: an IAM user, IAM group (which is a membership shortcut, not a real principal), or IAM role. Identity-based policies do not listPrincipal — the principal is the entity the policy is attached to.In same-account access, an Identity policy alone can authorize an action (no resource policy needed) for most services. The notable exception is AWS KMS: the key policy must explicitly enable IAM delegation (the "Enable IAM User Permissions" statement that grants
kms:* to arn:aws:iam::<account>:root) before any identity policy can authorize an action on that key. Always check the service's authorization documentation for the specific service-level rules.Effective permissions across multiple attached identity policies are the union of all statements. Attaching
ReadOnlyAccess plus a custom PutObject policy gives you both read-only-everywhere and PutObject on the specified resources.2.7 Stage 6: Permission Boundary
A Permission Boundary is a managed policy attached to an IAM user or role as that principal's ceiling. The effective permissions of the principal are the intersection of:- The union of all identity-based policies attached to the principal, and
- The Permission Boundary.
s3:* and the Permission Boundary grants s3:Get*, the principal can only do s3:Get*. If the identity policy grants ec2:* and the Permission Boundary grants s3:*, the principal can do neither — the intersection of {ec2:*} and {s3:*} is empty.Permission Boundaries are critical for delegated administration. The pattern is: grant developers
iam:CreateRole permission, but require that any role they create has a specified Permission Boundary attached. This bounds what the developers' roles can do without bounding what the developers can write into the identity policy.2.8 Stage 7: Session Policy
Session policies are passed at AssumeRole time. The caller invokessts:AssumeRole (or AssumeRoleWithSAML, AssumeRoleWithWebIdentity) with a Policy parameter (JSON-inline) or PolicyArns (managed policy ARNs). The resulting temporary credentials have effective permissions equal to the intersection of:- The role's identity policy and permission boundary (everything from stages 5 and 6), and
- The session policy.
3. Step-by-Step Walk-through with Concrete Examples
The twelve cases below walk through the Diamond in concrete terms. Each case is presented with the request shape (principal, action, resource, context), the policy JSON involved, a step-by-step trace through the Diamond stages, the final result, and the Reproduce in Simulator? badge with reproduction steps.We group the cases by Simulator coverage:
- 3.1 Fully Simulator-Reproducible (Cases 1–4 and 6–7) — Identity Policy + Resource Policy + Conditions only.
- 3.2 Partial Simulator Coverage (Cases 5 and 12) — Resource policy patterns that hit the same-account-vs-cross-account boundary the Simulator does not model.
- 3.3 Beyond the Simulator (Cases 8–11) — SCP, RCP, Permission Boundary, Session Policy.
3.1 Cases 1-4 and 6-7: Fully Simulator-Reproducible
Case 1 — Basic Allow via Identity Policy YES
Scenario: An IAM user in account111122223333 has an identity policy granting s3:GetObject on a specific bucket. The user invokes GetObject on that bucket. No resource policy is configured. No SCP, RCP, Permission Boundary, or Session Policy is in effect.Policy JSON (Identity):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3Read",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-bucket/*"
}
]
}
Diamond trace:
- Explicit Deny — no Deny statements anywhere. Pass.
- SCP — not configured. Pass.
- RCP — not configured. Pass.
- Resource Policy — not configured. Defers to identity policy.
- Identity Policy —
AllowS3Readmatches the action and resource. Allow. - Permission Boundary — not configured. Pass.
- Session Policy — not an assumed-role session. Not applicable.
Allow.Reproduce in Simulator (3 steps):
- Open IAM Policy Simulator (Offline).
- Paste the Identity policy JSON above into the Identity-based Policies panel.
- Set Action =
s3:GetObject, Resource ARN =arn:aws:s3:::example-bucket/file.txt, Principal ARN =arn:aws:iam::111122223333:user/alice. Click Evaluate. The badge will showAllow.
Case 2 — Explicit Deny Overrides Allow YES
Scenario: The same IAM user as Case 1, but the identity policy also contains an Explicit Deny statement that overlaps the Allow. The user invokesGetObject. The Deny takes precedence.Policy JSON (Identity):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3Read",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-bucket/*"
},
{
"Sid": "DenyConfidential",
"Effect": "Deny",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-bucket/confidential/*"
}
]
}
Diamond trace (request:
GetObject on arn:aws:s3:::example-bucket/confidential/secret.txt):- Explicit Deny —
DenyConfidentialmatches. Short-circuit to Deny. - The remaining stages are not evaluated.
ExplicitDeny.Reproduce in Simulator (3 steps):
- In the Simulator, paste the full identity policy with both statements.
- Set Resource ARN =
arn:aws:s3:::example-bucket/confidential/secret.txt. - Click Evaluate. The badge will show
ExplicitDeny, and the trace will list theDenyConfidentialstatement as the matching deny.
Case 3 — MFA Condition Gate with BoolIfExists YES
Scenario: A role permits ec2:TerminateInstances only when the session was authenticated with MFA. Without MFA, the request must be denied. The policy uses BoolIfExists so that sessions without an MFA context (long-term access keys) are also blocked.Policy JSON (Identity):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowTerminateWithMFA",
"Effect": "Allow",
"Action": "ec2:TerminateInstances",
"Resource": "*"
},
{
"Sid": "DenyTerminateWithoutMFA",
"Effect": "Deny",
"Action": "ec2:TerminateInstances",
"Resource": "*",
"Condition": {
"BoolIfExists": { "aws:MultiFactorAuthPresent": "false" }
}
}
]
}
Diamond trace (request:
TerminateInstances with aws:MultiFactorAuthPresent = false):- Explicit Deny —
DenyTerminateWithoutMFAmatches becauseMultiFactorAuthPresentisfalse. Short-circuit to Deny.
ExplicitDeny.Diamond trace (request:
TerminateInstances with aws:MultiFactorAuthPresent = true):- Explicit Deny —
DenyTerminateWithoutMFAcondition does not match (MFA is present). Pass. - Identity Policy —
AllowTerminateWithMFAmatches. Allow.
Allow.Reproduce in Simulator (3 steps):
- Paste the identity policy.
- Set Action =
ec2:TerminateInstances, Resource ARN =*, and add Context Keyaws:MultiFactorAuthPresent=false. - Click Evaluate. Result:
ExplicitDeny. Change the context key totrueand re-evaluate:Allow.
Bool vs BoolIfExists is non-trivial. Bool would not match if aws:MultiFactorAuthPresent is absent from the session context (e.g., when using long-term access keys), so a Deny guarded by plain Bool leaves an open door. The IfExists suffix changes the missing-key behavior: BoolIfExists returns true when the key is absent, so the Deny still fires on long-term-key sessions. AWS recommends "BoolIfExists": {"aws:MultiFactorAuthPresent": "false"} in Deny statements for MFA gating.Case 4 — IP Address Restriction YES
Scenario: A bucket should be readable only from the corporate IP range. Requests from outside that range get Implicit Deny because the Allow's condition does not match. There is no Explicit Deny — the condition failure produces no match at all.Policy JSON (Identity):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowFromCorp",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::corp-bucket/*",
"Condition": {
"IpAddress": { "aws:SourceIp": "203.0.113.0/24" }
}
}
]
}
Diamond trace (request:
GetObject from aws:SourceIp = 198.51.100.5):- Explicit Deny — no Deny statements. Pass.
- Identity Policy —
AllowFromCorpcondition does not match (IP outside CIDR). No statements match. No Allow.
ImplicitDeny.Reproduce in Simulator (3 steps):
- Paste the policy. Set Action =
s3:GetObject, Resource ARN =arn:aws:s3:::corp-bucket/file.txt. - Add Context Key
aws:SourceIp=198.51.100.5. - Click Evaluate. Result:
ImplicitDeny. Change the IP to203.0.113.42to seeAllow.
AccessDenied, but the cause is "no Allow matched", not "a Deny matched". When debugging, always check whether your Allow's Condition is matching the actual context.Case 6 — ABAC with aws:ResourceTag YES
Scenario: A role can manage EC2 instances only when the instance's Team tag matches the role's Team session tag. This is the canonical Attribute-Based Access Control (ABAC) pattern.Policy JSON (Identity):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ManageMyTeamInstances",
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances", "ec2:DescribeInstances"],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Team": "${aws:PrincipalTag/Team}"
}
}
}
]
}
Diamond trace (request:
StartInstances with aws:PrincipalTag/Team = data and aws:ResourceTag/Team = data):- Explicit Deny — none. Pass.
- Identity Policy —
ManageMyTeamInstancesmatches; conditionaws:ResourceTag/Team == aws:PrincipalTag/Teamevaluatesdata == data. Allow.
Allow. If the resource tag were payments, the condition would not match and the result would be ImplicitDeny.Reproduce in Simulator (3 steps):
- Paste the policy. Set Action =
ec2:StartInstances. - Add Context Keys
aws:PrincipalTag/Team=dataandaws:ResourceTag/Team=data. - Click Evaluate. Result:
Allow. Changeaws:ResourceTag/Teamtopayments:ImplicitDeny.
Case 7 — NotAction Misuse YES
Scenario: An engineer wants to grant "everything except IAM", and writes aNotAction Allow statement, intending "deny IAM, allow everything else". This is the wrong reading. NotAction with Allow means "Allow everything that is not in the NotAction list".Policy JSON (Identity):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowEverythingExceptIAM",
"Effect": "Allow",
"NotAction": ["iam:*"],
"Resource": "*"
}
]
}
Diamond trace (request:
s3:DeleteBucket on arn:aws:s3:::production-data):- Explicit Deny — none. Pass.
- Identity Policy —
AllowEverythingExceptIAMmatches becauses3:DeleteBucketis not iniam:*. Allow.
Allow. (Likely the engineer's actual intent was to disallow IAM but keep all other actions controlled by a more restrictive policy — but NotAction with Allow and Resource: "*" is effectively "AllowAll except IAM", including destructive APIs.)Reproduce in Simulator (3 steps):
- Paste the policy. Set Action =
s3:DeleteBucket, Resource =arn:aws:s3:::production-data. - Click Evaluate. Result:
Allow. - (Optional) Try Action =
iam:CreateUser:ImplicitDeny(because the statement'sNotActionexcludes IAM, no match).
NotAction paired with Allow is rarely what you want. The two correct forms are:NotActionpaired withDeny("Deny everything that is not in the list" — i.e., a default-deny allow-list inversion).- Explicit positive
Actionlist ("Allow only these actions").
NotAction + Allow + Resource: "*" in a code review, treat it as a finding.3.2 Cases 5 and 12: Partial Simulator Coverage
Case 5 — Cross-Account Access via Resource Policy Only PARTIAL
Scenario: Account A's user wants to read an S3 bucket in Account B. The bucket policy in Account B grantss3:GetObject to Account A's user. Account A's user has no matching identity policy. The Simulator (which only looks at the policies you give it) returns Allow because the resource policy allows. The real AWS evaluation returns ImplicitDeny because cross-account access requires both sides to allow.Policy JSON (Resource, on bucket in Account B):
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowAccountA",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:user/alice" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::accountB-bucket/*"
}]
}
Diamond trace (real AWS, cross-account):
- Explicit Deny — none. Pass.
- Resource Policy (Account B side) — allows alice. Pass.
- Identity Policy (Account A side) — no identity policy grants
s3:GetObjectonaccountB-bucket. Both-sides-required fails.
ImplicitDeny (in real AWS).Why the Simulator returns
Allow: The Simulator does not have a concept of "which account a policy belongs to". It evaluates union semantics by default — if the Resource policy or Identity policy allows, the result is Allow. This is correct for same-account access (Case 12 below) but under-models cross-account.How to validate the real behavior: Use the AWS-hosted Policy Simulator (which is account-aware), or test in a sandbox by attempting the cross-account
GetObject and inspecting the CloudTrail event in Account A (errorCode: AccessDenied, errorMessage mentioning "no identity-based policy allows").Teaching point: The same-account-vs-cross-account split is the single largest source of cross-org confusion. Memorize the rule: same-account = OR, cross-account = AND. See §4 for the full treatment.
Case 12 — Same-Account Resource Policy Allow with No Identity Policy PARTIAL
Scenario: An IAM user in account111122223333 has no identity policy granting s3:GetObject. The bucket (also in 111122223333) has a resource policy granting the user s3:GetObject. The user reads. In real AWS, this is Allow because same-account semantics are union. The Simulator agrees, but the reason it gets the right answer here is that it always uses union — which is right for same-account and wrong for cross-account.Policy JSON (Resource, on bucket in same account):
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowAlice",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:user/alice" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::same-account-bucket/*"
}]
}
Diamond trace (same-account):
- Explicit Deny — none. Pass.
- Resource Policy — allows alice. Pass.
- Identity Policy — no matching statement. But because this is same-account, the resource-policy Allow is sufficient.
Allow.Reproduce in Simulator (3 steps):
- Paste the resource policy into the Resource-based Policies panel.
- Leave Identity Policies empty.
- Set Principal =
arn:aws:iam::111122223333:user/alice, Action =s3:GetObject, Resource =arn:aws:s3:::same-account-bucket/file.txt. Result:Allow.
222233334444), the Simulator will still return Allow, which is wrong for the cross-account case. The Simulator is a useful tool, but you cannot use it to test cross-account boundaries.3.3 Cases 8-11: Beyond the Simulator
These cases involve Diamond stages the Simulator does not model. We trace the evaluation by hand and tell you what would happen in a real AWS environment.Case 8 — SCP Blocks Despite Identity Policy Allow NO
Scenario: An OU-level SCP deniess3:DeleteBucket. A role in the OU has identity policy s3:* on *. The role calls DeleteBucket. Identity policy allows. SCP denies. SCP wins.Policy JSON (SCP):
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyBucketDeletion",
"Effect": "Deny",
"Action": "s3:DeleteBucket",
"Resource": "*"
}]
}
Diamond trace:
- Explicit Deny — SCP contains an Explicit Deny matching
s3:DeleteBucket. Short-circuit to Deny at the SCP layer.
ExplicitDeny.Why the Simulator does not model this: The Simulator does not have an SCP input. It evaluates only Identity and Resource policies. To test SCP behavior, use the AWS-hosted Policy Simulator (which integrates with your real Organizations configuration) or test in a sandbox account attached to a test OU.
Reasoning shortcut: An SCP deny in a deny-list SCP is equivalent to an Explicit Deny at every principal in every affected account. There is no way around it short of removing the SCP attachment.
Teaching point: When debugging "the role has admin permissions but the API call fails", check the SCPs attached to the account and OU chain. Use
aws organizations list-policies-for-target --target-id <account-id> --filter SERVICE_CONTROL_POLICY to enumerate the effective SCPs.Case 9 — Permission Boundary Caps a Role NO
Scenario: A role has identity policys3:* on * but a Permission Boundary that only allows s3:Get*. The role calls s3:PutObject. Identity policy allows. Permission Boundary does not include PutObject. The effective permission is the intersection, which is empty for PutObject.Policy JSON (Permission Boundary):
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "BoundReadOnly",
"Effect": "Allow",
"Action": "s3:Get*",
"Resource": "*"
}]
}
Diamond trace:
- Explicit Deny — none. Pass.
- Identity Policy —
s3:*allowsPutObject. Pass. - Permission Boundary —
s3:Get*does not includes3:PutObject. The intersection at this stage is empty.
ImplicitDeny.Why the Simulator does not model this: Permission Boundary is a separate policy slot in AWS but is not represented in the Simulator. To validate, use
aws iam simulate-principal-policy --policy-source-arn <role-arn> (which is the AWS-hosted Policy Simulator API call) — it respects boundaries when the role has one attached.Reasoning shortcut: Permission Boundary is the intersection on top of identity policy. If you want to test "what can my role actually do?", compute
identity_policy ∩ permission_boundary by hand for the action set you care about.Teaching point: Permission Boundaries are silent. There is no AWS API call you can make that returns "the boundary blocked this". You see
AccessDenied. The boundary is exactly the kind of failure that produces "the policy says I can but I can't" tickets.Case 10 — Session Policy Narrows AssumeRole NO
Scenario: A CI/CD system assumes a deployer role with identity policys3:* on *, but passes a session policy that further narrows access to a single bucket. The session can write to that bucket, but not others.Policy JSON (Session Policy, passed to AssumeRole):
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "ThisDeploymentOnly",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::deploy-artifacts-2026-05-13/*"
}]
}
Diamond trace (request:
s3:PutObject on arn:aws:s3:::deploy-artifacts-2026-05-13/release.zip):- Explicit Deny — none. Pass.
- Identity Policy —
s3:*on*allows. Pass. - Session Policy — allows
s3:*ondeploy-artifacts-2026-05-13. Matches. Pass.
Allow.Diamond trace (request:
s3:PutObject on arn:aws:s3:::other-bucket/foo.zip):- Identity Policy — allows.
- Session Policy — does not match (different bucket). Intersection is empty for this resource.
ImplicitDeny.Why the Simulator does not model this: Session policies are a runtime artifact, not a persistent policy. To validate, call
aws sts assume-role --role-arn <role-arn> --role-session-name test --duration-seconds 900 --policy '<session-policy-json>', export the returned AccessKeyId, SecretAccessKey, and SessionToken as environment variables, and then attempt the action with those temporary credentials.Reasoning shortcut: Session policy is an intersection on top of the role's effective permissions (after identity + boundary). It can never grant beyond what the role already has.
Teaching point: Session policies are the right tool for short-lived CI/CD blast-radius reduction. They are also the right tool for human "I'm about to do something risky, let me narrow my own permissions first" workflows.
Case 11 — RCP Blocks Public S3 Write NO
Scenario: An OU-level RCP deniess3:PutObject from any principal whose account is outside the Organization. An external account tries to write to a bucket in the OU. The bucket policy allows, but the RCP overrides at the resource side.Policy JSON (RCP):
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyExternalWrite",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "*",
"Condition": {
"StringNotEqualsIfExists": {
"aws:PrincipalOrgID": "o-example12345"
}
}
}]
}
Diamond trace:
- Explicit Deny — the RCP statement matches (the external principal's org ID is not
o-example12345). Short-circuit to Deny at the RCP layer.
ExplicitDeny.Why the Simulator does not model this: RCPs are an Organizations-level resource-side perimeter. They are not represented in the offline Simulator. To validate, attempt the cross-org write to a sandbox account with the RCP attached and observe
AccessDenied in CloudTrail.Teaching point: RCPs are the right tool for "no S3 bucket in this OU can be written to by anyone outside our Org". You do not have to trust every bucket policy in every account; the RCP overrides at the resource side.
3.4 Tool Coverage Map
The following table summarizes which of the twelve cases above can be reproduced in the Simulator, and what the Simulator's badge will show.* You can sort the table by clicking on the column name.
| Case | Title | Simulator Reproduces? | Simulator Badge |
|---|---|---|---|
| 1 | Basic Allow via Identity Policy | YES full | Allow |
| 2 | Explicit Deny Overrides Allow | YES full | ExplicitDeny |
| 3 | MFA Condition Gate (BoolIfExists) | YES full | ExplicitDeny / Allow |
| 4 | IP Restriction (IpAddress) | YES full | ImplicitDeny / Allow |
| 5 | Cross-Account Resource Policy Only | PARTIAL Sim returns Allow, real AWS denies | Allow (misleading) |
| 6 | ABAC aws:ResourceTag | YES full | Allow / ImplicitDeny |
| 7 | NotAction Misuse | YES full | Allow |
| 8 | SCP Blocks Despite Identity Allow | NO Simulator does not model SCP | (cannot test) |
| 9 | Permission Boundary Caps Role | NO Simulator does not model Boundary | (cannot test) |
| 10 | Session Policy Narrows AssumeRole | NO Simulator does not model Session Policy | (cannot test) |
| 11 | RCP Blocks Public S3 Write | NO Simulator does not model RCP | (cannot test) |
| 12 | Same-Account Resource Policy Only | PARTIAL Sim correct for same-account only | Allow |
4. Cross-Account Evaluation Specifics
Cross-account access is the most context-dependent corner of IAM. The same Identity + Resource policy combination behaves differently depending on whether the caller and resource are in the same AWS account. Misreading this single dimension is the single largest source of confusion we see in real reviews.| Step | Same-Account (OR / union) Either side allows = pass | Cross-Account (AND / intersection) Both sides must allow |
|---|---|---|
| Resource Policy | Allows | (resource owner side) MUST Allow |
| Operator | OR | AND |
| Identity Policy | Allows | (caller side) MUST Allow |
| Result | ALLOW | ALLOW (both required) |
Fig. 2: Same-Account vs Cross-Account Evaluation — In the same account, either Resource Policy OR Identity Policy granting is sufficient (union). Across accounts, both sides must grant independently (intersection).
4.1 OR vs AND — The Switch
The rule is one line:- Same-account access: Identity Policy OR Resource Policy. Either is sufficient.
- Cross-account access: Identity Policy AND Resource Policy. Both are required.
This rule is asymmetric in a non-obvious way. Same-account is permissive (union). Cross-account is restrictive (intersection). Engineers who learn IAM in a single-account environment internalize "Identity policy is the source of truth" and then are surprised when their cross-account setup silently denies.
4.2 The "Both Sides Must Allow" Rule
In cross-account access:- The resource owner's account must have a resource policy (bucket policy, KMS key policy, role trust policy, etc.) that explicitly grants access to the external principal. Use
Principal: { "AWS": "arn:aws:iam::<external-account>:role/<role-name>" }or, for an entire account,Principal: { "AWS": "<external-account-id>" }. - The caller's account must have an identity policy attached to the calling principal that grants the action on the cross-account resource ARN.
AccessDenied. The error message will hint at which side, but reading CloudTrail in both accounts is the reliable diagnostic path.A common pitfall: the resource owner's bucket policy grants
arn:aws:iam::AAA:user/alice, but Alice's identity policy in account AAA does not grant s3:GetObject on the cross-account ARN. Alice gets AccessDenied. The administrator of account BBB (the bucket owner) sees the bucket policy looks correct and is confused. The fix is in account AAA, not BBB.4.3 SCP and RCP in Cross-Account
Both accounts' SCPs apply independently:- Caller's account SCP must allow the action (e.g.,
sts:AssumeRole,s3:GetObjecton the cross-account ARN). - Resource's account SCP must allow the resource-side actions.
The practical implication: when designing cross-account access, you have four policies to coordinate:
- Caller's account SCP allows the action.
- Caller's principal's identity policy allows the action on the cross-account ARN.
- Resource owner's account RCP allows the principal (does not deny).
- Resource owner's resource policy grants the principal.
4.4 Trust Policy Design Checklist for Cross-Account Roles
For cross-account role assumption specifically (the AssumeRole case), the trust policy on the role acts as the resource policy. A practical checklist:- Principal scoped to a specific role or user, not an entire account, unless the external account is fully trusted (e.g., another account you own).
- ExternalId is set when the caller is a third party (SaaS vendor). Not needed for first-party cross-account between your own accounts.
aws:PrincipalOrgIDCondition restricts the trust to principals in your Organization.- MFA Condition (
aws:MultiFactorAuthPresent) if a human will assume the role;sts:DurationSecondswithNumericLessThanEqualsin the trust policy (or the role'sMaxSessionDurationproperty) to bound session length. - Session tags are limited via
aws:TagKeysCondition so the assumer cannot inject arbitrary tags. - Permission Boundary is attached to the role to bound the post-assumption identity.
5. Common Pitfalls
The following eight pitfalls all stem from misreading the evaluation order itself, not from individual policy syntax errors. For a separate catalog of statement-level syntax mistakes, see the IAM Anti-Patterns - Real-World Mistakes and Their Root Causes article on this site.5.1 "Resource-Side Deny Can Be Overridden by Identity-Side Allow"
Explicit Deny is absolute, regardless of which policy type contains it. A"Effect": "Deny" in a bucket policy is just as final as one in an identity policy. Engineers who internalize "identity policy is what I control, so it wins" run into this pitfall when a bucket owner adds a Deny to the bucket policy and "their" Allow stops working. The Deny is final.5.2 "Without SCP, Permission Boundary is the Account-Level Ceiling"
A Permission Boundary applies to one principal. It is not a substitute for an SCP. An account without SCPs has no account-level ceiling; every principal's effective permissions are bounded only by their own identity policy (and their own boundary, if attached). If you want "no one in this account can disable CloudTrail", you need an SCP, not a Permission Boundary on every role.5.3 "Permission Boundary Constrains Resource Policy Too"
Permission Boundaries intersect with identity policies only. A resource policy on a bucket can still grant access regardless of the boundary on the calling principal — for same-account access via union semantics. If you want to constrain what bucket policies can grant about your principal, you need a Service Control Policy or RCP, not a Permission Boundary.5.4 "Session Policy Can Add Permissions to a Role"
Session policies intersect with the role's effective permissions. They cannot add anything. If you pass a session policy that allowss3:PutObject and the role's identity policy does not include s3:PutObject, the session still cannot do PutObject. Session policies are narrowing tools, not granting tools.5.5 "Same-Account Resource Allow Cannot Be Stopped by SCP or RCP"
Same-account Resource Policy Allow is sufficient under union semantics, but Explicit Deny anywhere still wins. An SCP Deny on the caller's account will block the request even if the same-account bucket policy allows. An RCP Deny on the resource's account will block similarly. (Note: a Permission Boundary on the calling principal does not apply to a resource-policy-only allow path — see §5.3 — so SCP and RCP are the right tools for capping resource-policy access in same-account.) Explicit Deny is universal.5.6 "Cross-Account Resource Policy Alone Is Enough"
Cross-account requires both sides. A cross-account bucket policy alone, without a matching identity policy in the caller's account, producesAccessDenied. The bucket owner's administrator will see the policy looks correct and be confused. The fix is in the caller's account.5.7 "Explicit Deny Is Confined to the Layer It Appears In"
Explicit Deny short-circuits the entire evaluation, not just the current stage. A Deny in a Resource policy can stop a request that an Identity policy has already allowed. A Deny in an SCP at the OU level stops every principal in every account in the OU. There is no scoping of Deny to "just this layer".5.8 "RCP Applies to Identities, Like SCP Does"
RCPs apply to the resource side. They constrain what can be done to resources in the affected accounts, regardless of where the caller is. SCPs constrain what callers in the account can do. The two are complementary perimeters: SCP guards outbound permissions; RCP guards inbound resource access. They are not interchangeable, and a deployment that uses only SCPs cannot fully replicate the resource-side guarantees an RCP provides.6. Tool: IAM Policy Simulator (Offline) — Companion to This Article
The IAM Policy Simulator (Offline) on this site evaluates Identity Policies, Resource Policies, and Condition keys entirely client-side, with no AWS account, no network call, and no IAM permissions. It is the right tool for:- Quickly checking a policy you are about to commit to a PR.
- Walking through Cases 1–4, 6, and 7 of this article hands-on.
- Validating Condition logic (
Bool,BoolIfExists,StringEquals,IpAddress,ArnLike,ForAllValues,ForAnyValue, etc.) on a sample request. - Teaching IAM evaluation to engineers who do not yet have an AWS sandbox.
6.1 What the Simulator Evaluates
The Simulator's documented scope:- Identity-based Policies — multiple policies can be loaded; they are unioned.
- Resource-based Policies — multiple policies; matched by
PrincipalARN. - Conditions — over 20 operators including
StringEquals,StringNotEquals,StringLike,Bool,BoolIfExists,IpAddress,NotIpAddress,DateGreaterThan,NumericLessThan,ArnEquals,ArnLike, plusForAllValues:andForAnyValue:modifiers, plusIfExistssuffix. - Request context — Action, Resource ARN, Principal ARN, and arbitrary Context Keys (e.g.,
aws:SourceIp,aws:MultiFactorAuthPresent,aws:ResourceTag/Team).
- A result badge:
Allow,ExplicitDeny, orImplicitDeny. - The list of matching statements (with Allow / Deny pills).
- A full evaluation trace (expandable
<details>) showing every statement and per-element match/skip with reasons. - A Copy Result button that serializes the decision, summary, and trace to the clipboard for sharing in a code review comment.
6.2 Reproducing Case 3 (MFA Condition Gate) Step-by-Step
As a concrete walkthrough, here is how to reproduce Case 3 in the Simulator:- Open the Simulator at hidekazu-konishi.com/tools/iam_policy_simulator_tool.html.
- Paste the identity policy from Case 3 into the Identity-based Policies panel.
- Set the request fields:
- Action:
ec2:TerminateInstances - Resource ARN:
* - Principal ARN:
arn:aws:iam::111122223333:role/Operator
- Action:
- Add a Context Key: key =
aws:MultiFactorAuthPresent, value =false. - Click Evaluate. The badge shows
ExplicitDeny. Expand the trace to seeDenyTerminateWithoutMFAlisted as the matching deny. - Change the context: edit the Context Key value to
true. Click Evaluate again. The badge changes toAllow, and the matching statement shifts toAllowTerminateWithMFA.
BoolIfExists semantics in concrete terms, in under thirty seconds.6.3 Simulator Limitations You Must Know
The Simulator does not model:- Service Control Policies (SCPs) — there is no SCP input slot. To test SCP behavior, use the AWS-hosted Policy Simulator or a sandbox account with the SCP attached.
- Resource Control Policies (RCPs) — same as SCPs.
- Permission Boundaries — there is no Boundary input slot. Use the AWS CLI
aws iam simulate-principal-policy --policy-source-arn <role-arn>to test against a real role's boundary. - Session Policies — there is no Session Policy input slot. Session policies are a runtime artifact; test by actually assuming the role with the session policy attached.
- Cross-account both-sides-required logic — the Simulator always uses union semantics, which is correct for same-account but incorrect for cross-account.
For static analysis of overly permissive identity policies (without needing a request), use the companion IAM Policy Least Privilege Analyzer on this site. It is the right tool for spotting
Action: * + Resource: *, dangerous iam:PassRole, conditionless wildcard statements, and other linter-style findings before they reach review.7. Frequently Asked Questions
7.1 What is the difference between Implicit Deny and Explicit Deny?
Implicit Deny is the default outcome when no policy statement matches the request — there is simply no Allow. Explicit Deny is an active"Effect": "Deny" statement that matches the request. Both produce AccessDenied in the API response, but they have different causes: Implicit Deny means nothing granted the request; Explicit Deny means something actively blocked it. The distinction matters when debugging: Implicit Deny points to a missing grant, while Explicit Deny points to an active blocking statement, typically in a Permission Boundary, SCP, RCP, or a Deny in an Identity or Resource policy.7.2 Does a Permission Boundary grant any permissions by itself?
No. A Permission Boundary defines the maximum allowed permission envelope, but it grants nothing on its own. The effective permissions are the intersection of the Boundary and the identity-based policies attached to the principal. If the Boundary containss3:* but no identity policy grants s3:PutObject, the action is still denied. You need both the Boundary (ceiling) and the identity policy (grant). The Boundary's role is to ensure that any future broadening of the identity policy is still capped by the Boundary.7.3 In the same AWS account, can a resource policy alone allow access without an identity policy?
Yes, with conditions. For same-account access, if a resource policy explicitly grants access to a principal in the same account and no Explicit Deny exists anywhere in the chain, the request is allowed — even without a matching identity policy. This is the union semantics for same-account access. Cross-account access is different: both the resource policy (on the resource side) and an identity policy (on the caller side) must grant access. See §4.7.4 Can an SCP be used to grant permissions?
No. SCPs define the maximum permissions available within an account or OU, but they do not grant permissions. An SCP with"Effect": "Allow" for s3:* does not allow any principal to access S3 unless that principal also has an identity policy or resource policy granting the action. Think of SCPs as a filter that can only remove permissions, not add them. The default FullAWSAccess SCP must remain attached at the root unless you understand exactly which actions will become impossible; removing it makes every action implicitly denied at the SCP layer.7.5 What happens if a Session Policy grants an action the role's identity policy does not have?
Nothing — the action remains denied. Session policies are intersected with the role's identity-based policies and Permission Boundary. If the identity policy does not includeec2:DescribeInstances, a Session Policy that grants ec2:DescribeInstances provides no additional access. Session policies can only narrow the role's existing permissions; they cannot expand them.7.6 How does RCP differ from SCP?
SCPs are applied to the caller (principal) side and restrict what IAM identities within an OU or account can do. RCPs are applied to the resource side and restrict what can be done to resources within an OU or account, regardless of where the caller is. An RCP on a production OU prevents any external principal from writing to S3 buckets in that OU, even if the caller's account has no SCP restrictions. They are complementary: SCP guards outbound caller permissions; RCP guards inbound resource access.7.7 Does the IAM Policy Simulator on this site model SCPs and Permission Boundaries?
No. The offline Simulator evaluates Identity Policies, Resource-based Policies, and Context Key Conditions. It does not model SCPs, RCPs, Permission Boundaries, or Session Policies. A result ofAllow from the offline Simulator means the identity and resource policy logic would allow the action — but in a real AWS environment, an SCP, RCP, Permission Boundary, or Session Policy could still deny it. To validate those layers, use the AWS-hosted Policy Simulator (account-aware) or a sandbox account.7.8 Why does aws:MultiFactorAuthPresent evaluate differently with Bool vs BoolIfExists?
Bool requires the context key to be present. If aws:MultiFactorAuthPresent is absent from the session context (for example, when using long-term access keys), Bool treats the condition as unmatched — which for a Deny statement means the Deny does not fire, leaving an open door. The IfExists suffix changes the missing-key behavior: BoolIfExists returns true when the key is absent. So with "BoolIfExists": {"aws:MultiFactorAuthPresent": "false"} on a Deny statement, a session that has no MFA context evaluates the condition as true and the Deny fires — closing the open door. AWS recommends this exact form in Deny statements so that long-term-key sessions are blocked along with non-MFA console sessions.7.9 In cross-account role assumption, which account's SCP applies?
Both accounts' SCPs apply independently. The caller's account SCP must allowsts:AssumeRole. The target account's SCP must allow the actions the assumed role will perform. If either SCP blocks the relevant action, the request fails — even if the trust policy and identity policies are correctly configured. RCPs on the resource account also apply independently to any resource operations performed after the role assumption.7.10 If an identity policy says Allow and a resource policy says Deny, which wins?
The resource policy's Explicit Deny wins, always. Explicit Deny from any policy type overrides Allow from any other policy type, regardless of evaluation order. This is the one absolute rule in IAM evaluation. Think of Explicit Deny as a global short-circuit that fires the moment it is encountered.8. Summary
The IAM Decision Diamond is one diagram and one rule:- The diagram: seven evaluation stages — Explicit Deny, SCP, RCP, Resource Policy, Identity Policy, Permission Boundary, Session Policy — every request passes through every applicable stage.
- The rule: Explicit Deny short-circuits everywhere; ceiling stages (SCP / RCP / Boundary / Session) intersect; granting stages (Resource / Identity) union for same-account and intersect for cross-account.
The same-account-vs-cross-account split is the single largest source of confusion. Memorize one line: same-account = OR, cross-account = AND.
For statement-level syntax mistakes (separate from evaluation order misreadings), see the IAM Anti-Patterns - Real-World Mistakes and Their Root Causes catalog. For the timeline of when each of the seven stages was introduced into IAM, see AWS History and Timeline regarding AWS IAM. For 60 IAM term definitions cross-referenced with this article's sections, see the AWS IAM Glossary. For the practical setup of Identity Center, including the Session Policy and ABAC patterns Case 6 and Case 10 hint at, see the AWS IAM Identity Center Complete Setup Guide.
9. References
AWS Official Documentation
- IAM Policy Evaluation Logic — IAM User Guide. The canonical decision-flow document. (verified 2026-05 at the Policy Evaluation Logic page)
- Cross-account policy evaluation logic — IAM User Guide. The same-account-vs-cross-account split. (verified 2026-05 at the Cross-account policy evaluation page)
- AWS Organizations User Guide — Service Control Policies (SCPs) and Resource Control Policies (RCPs). (verified 2026-05 at the AWS Organizations User Guide)
- IAM permissions boundaries — IAM User Guide. (verified 2026-05 at the Permission Boundaries page)
- Session policies — IAM User Guide and STS API Reference. (verified 2026-05 at the Session policies page)
- AWS IAM condition operators — full reference of
Bool,BoolIfExists,StringEquals,IpAddress,ArnLike,ForAllValues,ForAnyValue. (verified 2026-05 at the Condition Operators page) - AWS Policy Simulator — the AWS-hosted, account-aware policy simulator. (verified 2026-05 at policysim.aws.amazon.com)
Related Articles on This Site
- AWS History and Timeline regarding AWS IAM — the timeline of when each of the seven Diamond stages was introduced into IAM.
- AWS IAM Glossary — 60 IAM term definitions, cross-referenced with this article's sections.
- IAM Anti-Patterns - Real-World Mistakes and Their Root Causes — statement-level syntax catalog, complementary to this evaluation-order article.
- AWS IAM Identity Center Complete Setup Guide — end-to-end Identity Center setup, Permission Sets, ABAC with session tags, the practical context for Session Policy (Case 10) and ABAC (Case 6).
Companion Tools on This Site
- IAM Policy Simulator (Offline) — client-side evaluator for Identity Policies + Resource Policies + Conditions. Companion tool for Cases 1–4, 6, 7, and 12.
- IAM Policy Least Privilege Analyzer — static linter that detects
Action: *,Resource: *, dangerousiam:PassRole, conditionless wildcard statements, and similar over-permissive patterns.
References:
Tech Blog with curated related content
Written by Hidekazu Konishi