Why AWS IAM Policy Evaluation Works the Way It Does
A comprehensive cloud security architect's guide to IAM evaluation logic — covering implicit vs explicit denies, identity vs resource-based policies, trust relationships, and condition evaluations.
In cloud computing, access control is the primary security boundary. In Amazon Web Services (AWS), this boundary is governed by Identity and Access Management (IAM). Every API call made to AWS—whether reading an S3 object, launching an EC2 instance, or updating a database—is intercepted by the IAM evaluation engine, which determines if the caller has permission to perform the action.
The IAM evaluation engine does not simply search for a match. It runs a multi-layered evaluation algorithm that combines organization-level constraints, resource policies, permission boundaries, and identity permissions. Understanding this logic is vital for implementing the Principle of Least Privilege and debugging complex access issues (like AccessDenied exceptions). This guide walks you through the details of IAM policy anatomy, explains the implicit vs explicit deny rules, details the evaluation algorithm, and traces decision paths in our interactive simulator.
1. The Security Authorization Problem: Least Privilege
1.1 Restricting Scope of Credentials
In legacy network architectures, security was perimeter-based: if you were inside the corporate firewall, you had wide access. In modern cloud environments, this perimeter is replaced by micro-segmentation. Authentication verifies *who* you are (identity), but authorization determines *what* you can do (permissions). Wildcard permission profiles (e.g. * for all resources) present a severe security risk: if a credential is leaked, the entire cloud account is compromised.
The Principle of Least Privilege states that identities must only be granted the minimum permissions necessary to complete their specific task. AWS IAM policies enforce this by requiring developers to explicitly define the allowed actions, resources, and conditions for every identity. This limits the blast radius of compromised credentials.
1.2 Wildcard Permissions vs Granular Scope
A wildcard profile permits unconstrained API operations. A granular scope policy evaluates metadata attributes to restrict access, protecting sensitive database resources.
Common Misconception — Assigning policies directly to users is best: A common developer misconception is that assigning IAM policies directly to individual users is best practice. In reality, this leads to permission drift and makes audits difficult. Best practice is to assign policies to IAM Groups or Roles, and place users in these groups. This centralizes policy management and ensures consistency.
2. The Anatomy of an IAM Policy
2.1 Policy JSON Elements
AWS IAM policies are written in JSON. A policy contains one or more Statements, and each Statement has six key elements: (1) **Version**: the policy language version (always use "2012-10-17"). (2) **Sid** (Statement ID): an optional description name. (3) **Effect**: whether the statement "Allow" or "Deny" access. (4) **Action**: a list of specific API operations (e.g. "s3:GetObject"). (5) **Resource**: the specific AWS resource target (defined using Amazon Resource Names, or ARNs). (6) **Condition**: optional criteria that must be met for the policy to apply (e.g. restricting access by IP address or time of day).
Mermaid Diagram: The structural components of an AWS IAM policy statement, mapping actions to resources under specific conditions.
3. The Default Deny Rule: Implicit vs Explicit Deny
3.1 The Logic Priority
The IAM evaluation engine follows a strict priority logic. By default, all requests are denied. This is called an **Implicit Deny**. If a policy statement explicitly grants access ("Effect": "Allow"), the status changes to allowed. However, if any policy evaluates to "Effect": "Deny", the status changes to an **Explicit Deny**.
An explicit deny overrides all allows, regardless of the order or scope of the policies. If one policy statement allows S3 access, but a second policy denies it, the request is blocked. This priority ensures that security teams can set guardrails that cannot be bypassed by other policies.
4. Policy Types: Identity-Based vs Resource-Based
4.1 User vs Resource Bindings
IAM policies are classified into two main types: (1) **Identity-Based Policies**: attached directly to an IAM user, group, or role. They define what that identity can do (e.g. "User Alice can read S3 bucket X"). (2) **Resource-Based Policies**: attached directly to a resource (e.g. S3 Bucket Policies, KMS Key Policies). They define who can access that resource (e.g. "Allow User Alice to write to S3 bucket X").
When evaluating access, resource-based policies can grant direct cross-account access, bypassing the need to assume a role. The IAM engine merges these rules during evaluation.
5. The IAM Policy Evaluation Algorithm
5.1 The Step-by-Step Flow
When an API call is made, the IAM engine runs the following evaluation steps:
- It evaluates Organization Service Control Policies (SCPs). If SCP denies the action, the request is denied immediately.
- It evaluates Resource-Based Policies. If the resource policy allows the request, and the resource is in the same account, the request may be allowed.
- It evaluates Identity-Based Policies. If there is no explicit allow, the request is denied.
- It checks Permissions Boundaries. If a boundary is defined, and it does not allow the action, the request is denied.
- It checks Session Policies (if using assumed roles). If the session policy does not allow the action, the request is denied.
5.2 Policy Union and Intersection Mathematics
To determine the final authorization decision, the IAM engine performs a set of union and intersection operations on the permission sets defined in each policy layer. Let $P_{\text{identity}}$ be the permissions granted by identity-based policies, $P_{\text{resource}}$ be the permissions granted by resource-based policies, $P_{\text{boundary}}$ be the permissions allowed by the permissions boundary, $P_{\text{session}}$ be the permissions allowed by the session policy, and $P_{\text{scp}}$ be the permissions allowed by the organization SCP. Let $D_{\text{explicit}}$ represent any explicit deny statement across any of these layers.
First, within a single account scope, identity-based and resource-based policies operate as a **union** ($\cup$). If either allows the action, the access is granted at the account level. Next, this result is filtered by the **intersection** ($\cap$) of the permissions boundaries, session policies, and SCPs. Finally, the decision is subject to the subtraction of any explicit deny:
This mathematical representation explains why adding an administrative policy to a user will still result in access denied if the user's permissions boundary restricts that action. The boundary acts as a filter mask. Even though $P_{\text{identity}}$ contains the administrative permission, intersecting it with a restrictive $P_{\text{boundary}}$ deletes that permission from the final active set.
Pitfall — Cross-account resource policies: When accessing resources in a different AWS account, a resource-based policy alone is not enough. Access must be allowed in BOTH accounts: the identity policy in account A must allow the action, and the resource policy in account B must also allow the action. If either is missing, the request is blocked.
6. Trust Policies: Assuming Roles and AssumeRole
6.1 Role Assumptions
An IAM Role is a container for permissions, but unlike a user, it has no persistent credentials. Instead, it is assumed by a principal (a user, service, or different account). Every role must have a **Trust Policy** (or assume role policy document). This is a resource-based policy attached to the role that defines who is allowed to assume it.
When a principal calls sts:AssumeRole, the IAM engine checks if the caller matches the trust policy. If allowed, AWS Security Token Service (STS) returns temporary credentials (access key, secret key, session token) that expire after a specified duration, securing session boundaries.
6.2 STS Temporary Sessions and the ExternalId Guardrail
When a principal assumes a role, the credentials returned are generated dynamically by the AWS Security Token Service (STS). These credentials are bounded by a session lifetime $\Delta t_{\text{session}}$, which can range from 15 minutes to 12 hours (configurable by the administrator). When the session expires, the credentials are invalidated automatically by the global authentication gate:
Additionally, when assuming a role, you can pass an optional **Session Policy** as an argument. A session policy does not grant permissions. Instead, it acts as a filter boundary. The resulting session's permissions are the mathematical intersection of the role's identity policies and the session policy:
For third-party SaaS integrations, trust policies must guard against the "confused deputy" vulnerability. This is a vulnerability where an attacker tricks a third-party service into accessing resources in another client's account. To prevent this, trust policies require an ExternalId condition. The third-party service must supply a unique client-specific token when calling AssumeRole, confirming that the request was made on behalf of the correct client.
Below is a trust policy statement incorporating the ExternalId guardrail, allowing a third-party account to assume the role only if they supply the correct token:
Pitfall — Overlooking Trust Policy Principal wildcards: Using a wildcard in the Principal element of a trust policy (e.g. "Principal": "*") allows any AWS user in the world to attempt to assume the role. Even if you have conditions restricting access by IP or MFA, a wildcard principal creates a security hole. Always explicitly define the allowed AWS account ID or user ARN in trust policies.
7. Advanced: Context Keys and Policy Conditions
7.1 Dynamic Attributes
Condition blocks allow developers to write granular access controls using context keys. These keys represent metadata about the request (e.g. IP address, time, tags). For example, you can write a policy that only allows S3 uploads if the uploaded object has a specific tag, or if the request originates from a corporate IP range.
This enables Attribute-Based Access Control (ABAC), where permissions are updated automatically as resource tags change, avoiding the need to edit individual policies.
7.2 Condition Block Logic Operators and Set Comparisons
The Condition block is evaluated by the IAM engine as a set of logical equations. A Condition contains one or more **Condition Operators** (e.g. StringEquals, NumericLessThan, DateGreaterThan, Null, IpAddress). Each operator maps a context key to a list of allowed values. For the statement to match, the evaluation must satisfy the following logical rules:
- **Multiple values under one key**: Evaluated as a logical **OR** (e.g. if IP is A *OR* B).
- **Multiple keys under one operator**: Evaluated as a logical **AND** (e.g. if IP is A *AND* username is B).
- **Multiple operators under one Condition block**: Evaluated as a logical **AND**.
For multi-valued request keys (such as aws:PrincipalTag or s3:RequestObjectTag), we apply set comparison operators: ForAllValues and ForAnyValue. Let $R$ be the set of values in the request context key, and $P$ be the set of allowed values defined in the policy statement. The evaluation behaves as follows:
A common pitfall is using ForAllValues with a key that might be missing from the request. If the key is missing, the request set $R$ is empty ($\emptyset$). Since the empty set is a subset of any set ($\emptyset \subseteq P$), the condition evaluates to **True**, potentially granting unintended access. Below is a comparison table of condition operators:
| Condition Operator | Type Category | Evaluation Pass Condition | Best Practice Use Case |
|---|---|---|---|
| StringEquals / StringLike | String / Pattern Matching | Exact match or wildcard string match (* or ?) | Restrict access by principal name or resource tags |
| IpAddress / NotIpAddress | Network IP Address | IP falls within specified CIDR range | Enforce access only from corporate network endpoints |
| Null | Key Existence | Checks if the context key is missing from the request | Enforce Multi-Factor Authentication (MFA) presence |
Pitfall — Confusing StringEquals and StringLike: If you use StringEquals with a wildcard character (e.g. "s3:prefix": "home/*"), the engine will treat the asterisk as a literal character, matching only the path "home/*" exactly. Always use StringLike when using wildcard asterisks in values.
7.3 Granular ABAC Condition Example
To illustrate how these conditions secure API operations dynamically, let us look at an Attribute-Based Access Control (ABAC) statement. The following statement allows deleting an S3 object only if the caller's principal department tag matches the object's resource department tag:
In this statement, the value "${s3:ExistingObjectTag/Department}" is a policy variable resolved dynamically at runtime by the IAM engine. This allows you to scale access control without updating individual user profiles when departments are restructured.
8. Advanced: Comparison of Authorization Boundaries
8.1 Policy Guardrails Comparison
The table below compares the scope, target, and enforcement properties of the primary AWS policy types:
| Policy Type | Target Scope | Default Behavior | Primary Responsibility |
|---|---|---|---|
| Service Control Policy (SCP) | AWS Organization Accounts | Implicit Deny (must allow explicitly) | Set maximum permission boundaries for account scopes |
| Permissions Boundary | IAM Users / Roles | Implicit Deny | Limit maximum permissions delegated to administrators |
| IAM Policy (Identity-Based) | IAM Users / Groups / Roles | Implicit Deny | Grant permissions to specific identities |
| Resource-Based Policy | S3 / KMS / SQS resources | Implicit Deny | Grant cross-account access directly to resources |
Pitfall — Overlooking SCP Boundaries: If an SCP blocks an action (like deleting database instances), even the Root User of that account cannot perform the operation. SCPs set absolute boundaries that override all local account configurations. Always check organization SCPs if administrator API calls fail with Access Denied.
9. Complete IAM Policy JSON and Node.js Simulator Implementation
9.1 The JavaScript Code
The following Node.js code implements a mock IAM evaluation engine, testing policy statements against simulated API requests:
10. Interactive: IAM Policy Evaluation Simulator
Click "Step Evaluation" to trace how the IAM engine evaluates a request. Watch how it evaluates SCP, Resource Policy, Permissions Boundary, and Identity-based Policy:
Multi-Policy Evaluation Latency Comparison
The chart below compares the time taken (in milliseconds) to evaluate a policy request by the IAM engine based on the number of attached policies:
12. Frequently Asked Questions
Q1: Does an explicit Deny in an SCP override an Allow in a local IAM Policy?
Yes. An explicit Deny in an SCP (Service Control Policy) applies to all principals in the member account. No local IAM policy can override an SCP deny.
Q2: What is the difference between an Identity-Based Policy and a Resource-Based Policy?
Identity-based policies are attached to IAM users, groups, or roles, defining what they can do. Resource-based policies are attached directly to resources (e.g. S3 buckets, KMS keys), defining who can access them.
Q3: How does a Permissions Boundary restrict user permissions?
A permissions boundary is an advanced configuration that sets the maximum permissions an identity-based policy can grant. If an action is allowed in the identity-based policy but NOT allowed in the boundary, the action is denied.
Q4: Why does IAM evaluation default to a Deny?
For safety and security (Zero Trust). Denying access by default ensures that identities only gain permissions when explicitly granted. This prevents accidental exposure of cloud resources.
Q5: Can I attach a Resource-Based Policy to an EC2 Instance?
No. EC2 instances do not support resource-based policies directly. Instead, you attach an IAM Role to the EC2 instance using an Instance Profile, which allows applications running on the instance to make API calls using temporary credentials.
Q6: What is a NotAction element in an IAM Policy?
The NotAction element matches all API actions *except* those specified in the list. It is often combined with a Deny effect to create guardrails (e.g. Deny all actions except changing password).
Q7: How does cross-account S3 bucket access work?
Cross-account access requires permissions in both accounts. The identity policy in the source account must allow the S3 action, and the bucket policy in the target account must also allow the caller's ARN, maintaining trust boundaries.
Q8: How do I verify my IAM policies for security errors?
Verify: (1) use AWS IAM Access Analyzer to run static checks, (2) run CloudTrail log reviews to check denied calls, (3) verify that wildcards are not used for resources, and (4) verify that all trust relationships are bounded.