Home / AWS

AWSTroubleshootingAdvanced

Cross-account sts:AssumeRole AccessDenied: a two-sided checklist (trust policy, identity policy, SCPs, external ID)

Fix "is not authorized to perform sts:AssumeRole" between AWS accounts by checking both sides - the caller's permissions and SCPs, the target role's trust policy and conditions, external IDs, session duration and role chaining limits.

Deepak Bhardwaj4 min read

Applies toAWS IAM, AWS STS, AWS Organizations SCPs, AWS CLI v2

Symptom

An error occurred (AccessDenied) when calling the AssumeRole operation:
User: arn:aws:sts::111111111111:assumed-role/ci-runner/i-0abc is not authorized to perform:
sts:AssumeRole on resource: arn:aws:iam::222222222222:role/deploy-role

The message is the same whether the problem is in the source account or the target account. You have to check both.

The two sides

Side Must allow Where
Source (111111111111) sts:AssumeRole on the target role ARN Caller's identity policy, permission boundary, SCPs
Target (222222222222) The caller as principal, with conditions met Role trust policy, SCPs/RCPs in the target org

Step 1: who is really calling?

aws sts get-caller-identity

Compare the ARN with what the trust policy expects. Common mismatch: the trust policy names arn:aws:iam::111111111111:role/ci-runner and that's correct, but the call is made by a different role (a default profile, instance role instead of the pipeline role).

Step 2: source side - can the caller assume it?

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::111111111111:role/ci-runner \
  --action-names sts:AssumeRole \
  --resource-arns arn:aws:iam::222222222222:role/deploy-role \
  --query "EvaluationResults[].{decision:EvalDecision,denied:MatchedStatements}"

implicitDeny means no identity policy allows it. Add:

{ "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "arn:aws:iam::222222222222:role/deploy-role" }

The simulator doesn't evaluate SCPs from your org for every case. If it says allowed but the call fails, check SCPs on the source account for statements like "deny sts:AssumeRole outside the org" (aws:ResourceOrgID conditions) and permission boundaries on the role.

Step 3: target side - does the trust policy match?

aws iam get-role --role-name deploy-role --query Role.AssumeRolePolicyDocument

Check each of these:

Principal. Prefer the exact role ARN:

"Principal": { "AWS": "arn:aws:iam::111111111111:role/ci-runner" }

If you see something like "AWS": "AROAEXAMPLEID123" instead of an ARN, the source role was deleted and recreated. The trust policy holds the old role's unique ID, which never matches the new role. Edit the trust policy and put the ARN back.

Conditions. Every condition must match:

Condition Typical failure
sts:ExternalId Caller didn't pass --external-id, or passed the wrong value
aws:PrincipalOrgID Source account is in a different organization
aws:MultiFactorAuthPresent Automation can't do MFA
aws:SourceIp Call comes via NAT/VPC endpoint with a different IP
sts:RoleSessionName pattern Session name doesn't match the required pattern

Test with the exact parameters the trust expects:

aws sts assume-role --role-arn arn:aws:iam::222222222222:role/deploy-role \
  --role-session-name ci-$(date +%s) --external-id "<value>" \
  --query Credentials.Expiration

Step 4: look at CloudTrail in the target account

Failed AssumeRole calls are logged in the target account (and the source). They show the caller, the error and the request parameters:

aws cloudtrail lookup-events --region us-east-1 \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
  --max-results 20 --query "Events[].CloudTrailEvent" --output text \
  | jq -r 'select(.errorCode != null) | [.eventTime, .userIdentity.arn, .errorCode, .errorMessage] | @tsv'

STS events can land in the region of the STS endpoint used. If you use regional endpoints, look in that region.

Step 5: errors that aren't AccessDenied

Error Cause Fix
ValidationError: The requested DurationSeconds exceeds the MaxSessionDuration Asked for longer than the role allows Raise MaxSessionDuration on the role (up to 12 h) or request less
Session limited to 1 hour despite a larger max Role chaining (assumed role assumes another role) is capped at 1 hour Re-design the chain or refresh more often
RegionDisabledException Using an STS regional endpoint in a region not enabled for the account Enable the region or use another STS endpoint
ExpiredToken on the source credentials Caller's own session expired Refresh the source credentials

Prevent it next time

  • Name principals by ARN in trust policies and manage both sides in the same IaC change.
  • Use aws:PrincipalOrgID or aws:PrincipalOrgPaths to trust "my org" instead of listing every account, where that fits your security model.
  • For third parties, always require sts:ExternalId.
  • Related: workload identity options on AWS, cross-account KMS access denied.