Introduction

An IAM role can have a perfectly valid trust policy and still reject sts:AssumeRole. The reason is that AWS evaluation does not stop at the trust relationship. Any explicit deny in the caller path, permission boundary, session policy, or organization SCP overrides the allow. That is why these incidents are frustrating: the role looks right, but the request is denied anyway.

Symptoms

  • AssumeRole returns AccessDenied
  • The trust policy appears correct when reviewed in isolation
  • One principal can assume the role while another cannot
  • The failure started after an SCP, permissions boundary, or tag condition change

Common Causes

  • The caller has an explicit deny on sts:AssumeRole
  • A permissions boundary blocks role assumption even though the attached policy allows it
  • An organization SCP denies the action for that account or OU
  • The trust policy requires conditions such as ExternalId, MFA, or session tags that the caller is not providing

Step-by-Step Fix

  1. 1.Verify the trust policy is not the only thing under review
  2. 2.Start by acknowledging that the trust policy is necessary but not sufficient for a successful assume-role path.
  3. 3.Inspect the caller-side policies and boundaries for explicit deny
  4. 4.Search for Effect: Deny on sts:AssumeRole or broader statements that cover the target role path.
bash
aws iam list-attached-user-policies --user-name deploy-user
aws iam list-attached-role-policies --role-name ci-runner-role
  1. 1.Check organization SCPs and required trust policy conditions
  2. 2.If the role is cross-account or guarded by ExternalId, MFA, or tag conditions, verify the caller request actually satisfies them.
json
{
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::123456789012:root" },
  "Action": "sts:AssumeRole",
  "Condition": {
    "StringEquals": {
      "sts:ExternalId": "deploy-prod"
    }
  }
}
  1. 1.Retest with the exact caller context after removing the conflicting deny
  2. 2.Use the same profile, principal, and tags that failed originally so you know the fix applies to the real path rather than a different admin identity.

Prevention

  • Review AssumeRole failures across trust policy, caller policy, permissions boundary, and SCP together
  • Document required conditions such as ExternalId, MFA, and session tags for every cross-account role
  • Avoid broad deny statements that unintentionally catch sts:AssumeRole
  • Test role assumption paths after organization policy changes, not just after trust policy edits