Introduction

AWS IAM uses a policy evaluation logic where explicit deny statements always override allow statements. This is a fundamental security principle: if any policy in the evaluation chain contains an explicit deny for an action, the request is blocked regardless of how many allow statements exist. This design prevents accidental access escalation through policy combination.

When an AssumeRole call fails with AccessDenied, administrators typically check the role's trust policy. However, AWS evaluates multiple policy types during authorization: identity-based policies attached to the calling principal, resource-based policies (trust policy for roles), permission boundaries, service control policies (SCPs), and session policies. An explicit deny in any of these layers blocks the request.

Understanding the complete policy evaluation chain is essential for debugging AssumeRole failures. The trust policy is necessary but not sufficient—every layer must permit the action. Debugging requires systematically checking each policy type that applies to the caller and the target role.

Symptoms

When AWS IAM AssumeRole fails due to explicit deny, you will observe these symptoms:

  • sts:AssumeRole returns AccessDenied error despite correct trust policy
  • Role trust policy appears correct when reviewed in isolation
  • One principal can assume the role while another cannot
  • Failure started after SCP, permission boundary, or tag-based policy changes
  • Cross-account role assumption works from some accounts but not others
  • CloudTrail shows AssumeRole attempt with AccessDenied and policy evaluation details
  • IAM Policy Simulator shows the action as denied

Common error messages:

```bash aws sts assume-role --role-arn arn:aws:iam::123456789012:role/MyRole --role-session-name test

# Error output: An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::123456789012:user/testuser is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::123456789012:role/MyRole ```

CloudTrail event showing explicit deny:

json
{
  "eventName": "AssumeRole",
  "errorCode": "AccessDenied",
  "errorMessage": "User is not authorized to perform: sts:AssumeRole",
  "userIdentity": {
    "arn": "arn:aws:iam::123456789012:user/testuser"
  },
  "resources": [{
    "ARN": "arn:aws:iam::123456789012:role/MyRole"
  }]
}

Common Causes

Several factors cause AssumeRole failures through explicit deny:

  1. 1.Explicit deny in caller's identity policy: The IAM user or role making the AssumeRole call has an attached policy with an explicit deny on sts:AssumeRole or a wildcard that includes it.
  2. 2.Permission boundary blocking the action: Permission boundaries set the maximum permissions a principal can have. A boundary with no sts:AssumeRole permission or explicit deny blocks the action.
  3. 3.Organization Service Control Policy (SCP): SCPs apply to AWS accounts and can deny actions account-wide. An SCP denying sts:AssumeRole blocks all role assumptions in affected accounts.
  4. 4.Trust policy conditions not met: The role's trust policy includes conditions (ExternalId, MFA, source identity, session tags) that the caller doesn't satisfy.
  5. 5.Session policy deny: When assuming a role with inline session policies, those policies may deny specific actions.
  6. 6.Attribute-based access control (ABAC) deny: Tag-based conditions in policies may deny access based on missing or mismatched tags on the caller or role.
  7. 7.Cross-account permission missing: For cross-account role assumption, the source account must also allow sts:AssumeRole for the target role.

Step-by-Step Fix

Follow these steps to diagnose and resolve explicit deny AssumeRole failures:

Step 1: Identify the calling principal

Determine exactly who is trying to assume the role:

```bash # Check current caller identity aws sts get-caller-identity

# Output: { "UserId": "AIDACKCEVSQ6C2EXAMPLE", "Account": "123456789012", "Arn": "arn:aws:iam::123456789012:user/testuser" }

# Note the ARN - this is the principal to investigate ```

Step 2: Check the role's trust policy

Verify the trust policy allows the calling principal:

```bash # Get role trust policy aws iam get-role --role-name MyRole --query 'Role.AssumeRolePolicyDocument'

# Example trust policy: { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::123456789012:root"}, "Action": "sts:AssumeRole", "Condition": { "StringEquals": {"sts:ExternalId": "my-external-id"} } }] }

# Verify caller's account is in Principal # Check if conditions are required and being met ```

Step 3: Check caller's identity policies

Look for explicit denies in attached policies:

```bash # List policies attached to the user aws iam list-attached-user-policies --user-name testuser

# List inline policies for the user aws iam list-user-policies --user-name testuser

# Get specific policy content aws iam get-policy --policy-arn arn:aws:iam::aws:policy/AWSDenyAll

# Search for explicit denies aws iam get-policy-version \ --policy-arn arn:aws:iam::123456789012:policy/MyPolicy \ --version-id v1 \ --query 'PolicyVersion.Document.Statement[?Effect==Deny]'

# Check for wildcard denies # Look for: "Action": "sts:AssumeRole" or "Action": "*" ```

Step 4: Check permission boundaries

Verify permission boundary allows AssumeRole:

```bash # Check if user has permission boundary aws iam get-user --user-name testuser --query 'User.PermissionsBoundary'

# Output if boundary exists: { "PermissionsBoundaryType": "Policy", "PermissionsBoundaryArn": "arn:aws:iam::123456789012:policy/MyBoundary" }

# Get boundary policy content aws iam get-policy-version \ --policy-arn arn:aws:iam::123456789012:policy/MyBoundary \ --version-id v1

# Check if boundary allows sts:AssumeRole # Boundary must explicitly allow the action ```

Step 5: Check Service Control Policies

Examine organization SCPs:

```bash # List SCPs attached to the account aws organizations list-policies-for-target --target-id 123456789012 --filter SERVICE_CONTROL_POLICY

# Get SCP content aws organizations describe-policy --policy-id p-example

# Look for explicit denies: { "Statement": [{ "Effect": "Deny", "Action": ["sts:AssumeRole"], "Resource": "*" }] }

# SCPs apply to all principals in the account # Even root user is subject to SCP ```

Step 6: Check trust policy conditions

Verify all conditions are being met:

```bash # If ExternalId required: aws sts assume-role \ --role-arn arn:aws:iam::123456789012:role/MyRole \ --role-session-name test \ --external-id my-external-id

# If MFA required, ensure MFA is used: aws sts get-session-token --serial-number arn:aws:iam::123456789012:mfa/user --token-code 123456

# If source identity required: aws sts assume-role \ --role-arn arn:aws:iam::123456789012:role/MyRole \ --role-session-name test \ --source-identity admin@company.com ```

Step 7: Use IAM Policy Simulator

Test the exact policy evaluation:

```bash # Via AWS Console: # IAM > Policy Simulator # Select user: testuser # Select action: sts:AssumeRole # Specify resource: arn:aws:iam::123456789012:role/MyRole

# Via CLI (simplified): # The simulator shows which policy statements matched # And whether the final result is allowed or denied ```

Step 8: Remove or modify the conflicting deny

Fix the policy that contains the explicit deny:

```bash # If deny is in identity policy, create new version without deny: aws iam create-policy-version \ --policy-arn arn:aws:iam::123456789012:policy/MyPolicy \ --policy-document '{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["sts:AssumeRole"], "Resource": "arn:aws:iam::123456789012:role/MyRole" }] }' \ --set-as-default

# If deny is in permission boundary, update boundary: aws iam put-user-policy \ --user-name testuser \ --policy-name AllowAssumeRole \ --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}' ```

Step 9: Test after changes

Verify the fix works:

```bash # Attempt AssumeRole aws sts assume-role \ --role-arn arn:aws:iam::123456789012:role/MyRole \ --role-session-name test

# Expected successful output: { "Credentials": { "AccessKeyId": "ASIAX...", "SecretAccessKey": "...", "SessionToken": "...", "Expiration": "..." }, "AssumedRoleUser": { "AssumedRoleId": "AROAX...:test", "Arn": "arn:aws:sts::123456789012:assumed-role/MyRole/test" } } ```

Verification

After fixing the policy, verify AssumeRole works correctly:

```bash # Test role assumption aws sts assume-role \ --role-arn arn:aws:iam::123456789012:role/MyRole \ --role-session-name verification-test \ --query 'Credentials.[AccessKeyId,Expiration]'

# Should return credentials without error

# Use assumed role credentials aws sts get-caller-identity --profile assumed-role

# Should show the assumed role ARN # arn:aws:sts::123456789012:assumed-role/MyRole/verification-test ```

Check CloudTrail for successful AssumeRole:

bash
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
  --query 'Events[?contains(CloudTrailEvent, `testuser`)]'

Prevention

To prevent explicit deny AssumeRole failures:

  1. 1.Document all policy layers: Maintain clear documentation of all policies affecting principals.
markdown
## Principal: testuser
- Identity Policies: MyPolicy, ReadOnlyAccess
- Permission Boundary: MyBoundary
- SCP: DenyAllExceptAdmin
- Trust Policy Condition: ExternalId required
  1. 1.Use IAM Policy Simulator regularly: Test policy changes before deployment.
bash
# Always test with simulator before applying policy changes
  1. 1.Avoid broad deny statements: Use specific deny conditions rather than blanket denies.

```json // Better: Specific deny with conditions { "Effect": "Deny", "Action": "sts:AssumeRole", "Resource": "*", "Condition": { "StringNotEquals": {"aws:PrincipalTag/Team": "Admin"} } }

// Avoid: Broad unconditional deny { "Effect": "Deny", "Action": "*", "Resource": "*" } ```

  1. 1.Audit SCPs for unintended side effects: SCP changes affect all principals in the account.
  2. 2.Implement change management for policies: Review all policy layers when making changes.
bash
# Pre-change checklist:
# 1. Check identity policies
# 2. Check permission boundaries
# 3. Check SCPs
# 4. Check trust policy conditions
# 5. Test with Policy Simulator
# 6. Apply change
# 7. Verify with actual test
  1. 1.Use policy conditions for flexibility: Prefer conditions over explicit denies.
  2. 2.Monitor AssumeRole failures: Set up CloudWatch alerts for AssumeRole AccessDenied events.
bash
aws cloudwatch put-metric-alarm \
  --alarm-name AssumeRole-Failures \
  --metric-name CallCount \
  --namespace AWS/STS \
  --statistic Sum \
  --period 300 \
  --threshold 10 \
  --comparison-operator GreaterThanThreshold

Related Articles

  • [AWS troubleshooting: Fix IAM Permission Denied - Complete Tro](fix-iam-permission-denied)
  • [AWS cloud troubleshooting: AWS ACM Certificate Pending Validation Because the](aws-acm-certificate-pending-validation-wrong-route53-zone)
  • [AWS cloud troubleshooting: AWS ALB Returns 502 Because the Target Closed the ](aws-alb-502-target-closed-connection-keepalive-timeout-mismatch)
  • [AWS cloud troubleshooting: Fix AWS ALB CreateListener TargetGroupNotFound Err](aws-alb-createlistener-targetgroupnotfound)
  • [AWS cloud troubleshooting: Fix Aws Alb Lambda 502 Bad Gateway Issue in AWS](aws-alb-lambda-502-bad-gateway)

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "AWS cloud troubleshooting: AWS IAM Role AssumeRole Failed Because an Explicit", "description": "Professional guide to fix AWS IAM Role AssumeRole Failed Because an Explicit Deny Overrode the Allow. AWS cloud troubleshooting with step-by-step solutions. Learn best practices and prevention strategies.", "url": "https://www.fixwikihub.com/aws-iam-role-assume-access-denied-explicit-deny", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2026-01-22T21:56:23.739Z", "dateModified": "2026-01-22T21:56:23.739Z" } </script>