If you’ve tried to set up cross-account IAM role assumptions and hit “Access Denied” errors, you’re not alone. I’ve debugged dozens of cross-account permission setups, and the issues almost always come down to mistakes in the trust policy (assume role policy document). In this post, I’ll walk through exactly what makes a proper cross-account trust policy and how to fix common mistakes.
The Problem
You’ve created a role in Account B that you want users in Account A to assume. The trust policy looks right to you, but assume-role calls fail:
| Error Type | Error Message |
|---|---|
| AccessDenied | User: arn:aws:iam::111111111111:user/deployer is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::222222222222:role/CrossAccountRole |
You’re confident the trust policy is correct. So why does the assume-role call fail? Almost always, it’s one of five trust policy mistakes.
Why Does This Happen?
- Trust policy uses wrong account ID: The trust policy specifies a different source account ID than where your user actually lives
- Trust policy is too restrictive: It specifies an exact role ARN as the principal instead of allowing all principals in the account
- Principal ARN condition is misconfigured: The policy has a condition on
aws:PrincipalArnbut the syntax or value is wrong - Role session duration exceeds the role’s maximum: You’re requesting a session duration longer than the role permits
- Trust policy document syntax is invalid: JSON formatting or policy structure is malformed, so the policy is silently ignored or partially applied
The Fix
Step 1: Get and Examine the Current Trust Policy
First, retrieve the current trust policy from the role:
aws iam get-role \
--role-name CrossAccountRole \
--query 'Role.AssumeRolePolicyDocument' \
--output text | jq .
This shows you exactly what the role currently trusts. The policy should look like this for a basic cross-account setup:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole"
}
]
}
The key field is Principal.AWS. It specifies who is allowed to assume this role. For cross-account access, it should be the ARN of the source account root, a specific user, or a role.
Step 2: Understand Principal Options
You have three options for the Principal:
Option 1: Allow all principals in the source account (most permissive)
{
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
}
}
This allows any user or role in Account 111 to assume this role. Good for development, risky for production.
Option 2: Allow a specific user or role (most restrictive)
{
"Principal": {
"AWS": "arn:aws:iam::111111111111:user/deployer"
}
}
This allows only the deployer user to assume this role. Most secure, but requires updating the trust policy whenever users/roles change.
Option 3: Allow multiple principals
{
"Principal": {
"AWS": [
"arn:aws:iam::111111111111:user/deployer",
"arn:aws:iam::111111111111:role/DeploymentRole"
]
}
}
This allows both a user and a role to assume the role.
Step 3: Add Conditions for Extra Security
You can add conditions to restrict when the role can be assumed:
Restrict to specific external ID (recommended for third-party access)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "MySecretExternalId123"
}
}
}
]
}
Require MFA
{
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
}
}
}
Restrict to specific IP ranges
{
"Condition": {
"IpAddress": {
"aws:SourceIp": [
"10.0.0.0/8",
"203.0.113.0/24"
]
}
}
}
Restrict to specific principal ARN pattern
{
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::111111111111:role/DeploymentRole*"
}
}
}
Step 4: Update the Trust Policy
If your current policy is wrong, update it:
# Create the corrected policy
cat > trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "MyExternalId123"
}
}
}
]
}
EOF
# Update the role with the new trust policy
aws iam update-assume-role-policy \
--role-name CrossAccountRole \
--policy-document file://trust-policy.json
Step 5: Test the Updated Policy
Verify the assume-role call now works (from the source account):
# If you set an external ID, include it
aws sts assume-role \
--role-arn arn:aws:iam::222222222222:role/CrossAccountRole \
--role-session-name test-session \
--external-id MyExternalId123 \
--duration-seconds 3600 \
--output text
If this works, you see temporary credentials. If it fails, recheck the principal ARN — it’s the most common mistake.
Common Trust Policy Mistakes Checklist
- Wrong account ID in principal → Verify the source account ID is correct:
111111111111 - Exact role ARN instead of account root → Use
arn:aws:iam::111111111111:rootnotarn:aws:iam::111111111111:role/SpecificRole - Missing external ID when required → If you specified an external ID in the condition, you must include it in the assume-role call
- Wrong condition key name → Use
sts:ExternalId, notExternalId; useaws:PrincipalArn, notPrincipalArn - Using StringEquals instead of StringLike for wildcards → Use
StringLikeif your condition value contains*
Is This Safe?
Yes. Reviewing and updating trust policies is part of normal role management. Changes are logged in CloudTrail and can be reverted if needed.
Key Takeaway
Cross-account trust policies need three things: (1) correct source account ID in the Principal, (2) broad enough principal specification (use account root for flexibility), and (3) optional conditions for extra security. The most common mistake is using a specific role ARN as the principal instead of the account root — this locks out other users/roles in the same account from assuming the role. Always start with the account root, then narrow the principal scope if needed.
Have questions or ran into a different IAM issue? Connect with me on LinkedIn or X.