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:PrincipalOrgIDoraws:PrincipalOrgPathsto 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.