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:PrincipalArn but 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

  1. Wrong account ID in principal → Verify the source account ID is correct: 111111111111
  2. Exact role ARN instead of account root → Use arn:aws:iam::111111111111:root not arn:aws:iam::111111111111:role/SpecificRole
  3. Missing external ID when required → If you specified an external ID in the condition, you must include it in the assume-role call
  4. Wrong condition key name → Use sts:ExternalId, not ExternalId; use aws:PrincipalArn, not PrincipalArn
  5. Using StringEquals instead of StringLike for wildcards → Use StringLike if 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.