When you manage multiple AWS accounts, you often need resources in one account to access services in another — deploying code, reading S3 buckets, or querying databases across accounts. Cross-account access in AWS with AssumeRole lets you do this without sharing credentials. You create a role in the target account, grant permission to assume it from the source account, and switch contexts using temporary credentials.
The examples here are run on WSL2 Ubuntu in Windows, but the AWS CLI commands work the same on any system.
How AssumeRole Works
The setup involves two accounts:
- Account A (111111111111) — the account that needs access
- Account B (222222222222) — the account that owns the resources
Account B creates an IAM role with a trust policy that says “Account A is allowed to assume this role.” Account A’s users or services then call sts:AssumeRole to get temporary credentials that work in Account B. These credentials expire automatically (default: 1 hour).
Prerequisites
- AWS CLI v2 installed and configured
- IAM admin access in both accounts (or enough permissions to create roles and policies)
Step 1: Create the Role in Account B
In Account B (the account with the resources), create an IAM role that Account A can assume.
Trust policy
Save this as trust-policy.json. It allows Account A’s root principal to assume the role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole"
}
]
}
Using the root principal means any IAM user or role in Account A can assume this role — but only if they also have an sts:AssumeRole permission on their side (Step 2). To restrict to a specific user or role, replace root with the full ARN (e.g., arn:aws:iam::111111111111:user/deployer).
Create the role
aws iam create-role \
--role-name CrossAccountAccessRole \
--assume-role-policy-document file://trust-policy.json \
--profile account-b
Attach permissions
Attach the permissions that the role needs. For example, to grant read-only S3 access:
aws iam attach-role-policy \
--role-name CrossAccountAccessRole \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
--profile account-b
Use the least-privilege principle — only attach the permissions the role actually needs. You can use AWS managed policies or create a custom policy scoped to specific resources.
Step 2: Grant AssumeRole Permission in Account A
In Account A, the user or role that will assume the cross-account role needs permission to call sts:AssumeRole. Create a policy and attach it to the IAM user, group, or role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::222222222222:role/CrossAccountAccessRole"
}
]
}
Save this as assume-role-policy.json and attach it:
aws iam put-user-policy \
--user-name deployer \
--policy-name AllowCrossAccountAccess \
--policy-document file://assume-role-policy.json \
--profile account-a
Only sts:AssumeRole is required here. You don’t need iam:PassRole or iam:ListRoles — those are for different use cases.
Step 3: Assume the Role
There are three ways to assume the role: an AWS CLI profile (recommended for regular use), a one-time CLI command, or boto3 in Python.
Option A: AWS CLI profile (recommended)
Add a profile to ~/.aws/config that automatically assumes the role when you use it:
[profile account-b-cross]
role_arn = arn:aws:iam::222222222222:role/CrossAccountAccessRole
source_profile = account-a
Now any AWS CLI command with --profile account-b-cross assumes the role automatically:
aws s3 ls --profile account-b-cross
role_arn— the role in Account B to assumesource_profile— your Account A credentials profile, used to authenticate the AssumeRole call
The CLI handles the sts:AssumeRole call, caches the temporary credentials, and refreshes them when they expire.
Option B: Manual CLI command with environment variables
For scripts or one-off access, call sts assume-role directly and export the temporary credentials:
OUT=$(aws sts assume-role \
--role-arn "arn:aws:iam::222222222222:role/CrossAccountAccessRole" \
--role-session-name "cross-account-session" \
--profile account-a)
export AWS_ACCESS_KEY_ID=$(echo $OUT | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $OUT | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $OUT | jq -r '.Credentials.SessionToken')
--role-session-name— a label for this session (shows up in CloudTrail logs)- Requires
jqinstalled (sudo apt install -y jq) - These credentials expire after 1 hour by default
After exporting, any AWS CLI command in the same terminal session runs as Account B’s role:
aws s3 ls
aws sts get-caller-identity
To stop using the assumed role, unset the environment variables:
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
Option C: boto3 in Python
For Lambda functions or Python scripts that need cross-account access:
import boto3
sts_client = boto3.client('sts')
response = sts_client.assume_role(
RoleArn='arn:aws:iam::222222222222:role/CrossAccountAccessRole',
RoleSessionName='lambda-cross-account'
)
credentials = response['Credentials']
s3_client = boto3.client(
's3',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken']
)
# Now use s3_client to interact with Account B's S3
buckets = s3_client.list_buckets()
print(buckets)
Adjusting Session Duration
By default, temporary credentials from AssumeRole expire after 1 hour. You can set a longer duration (up to 12 hours) when assuming the role:
aws sts assume-role \
--role-arn "arn:aws:iam::222222222222:role/CrossAccountAccessRole" \
--role-session-name "long-session" \
--duration-seconds 3600 \
--profile account-a
The role’s maximum session duration must also be updated to allow longer sessions. Set it when creating or updating the role:
aws iam update-role \
--role-name CrossAccountAccessRole \
--max-session-duration 43200 \
--profile account-b
Valid values: 3600 (1 hour) to 43200 (12 hours).
Verifying the Setup
After assuming the role, confirm which identity you’re using:
aws sts get-caller-identity --profile account-b-cross
The output should show Account B’s account ID and the assumed role ARN:
{
"UserId": "AROAEXAMPLEID:cross-account-session",
"Account": "222222222222",
"Arn": "arn:aws:sts::222222222222:assumed-role/CrossAccountAccessRole/cross-account-session"
}
Troubleshooting
| Error | Cause |
|---|---|
AccessDenied when calling AssumeRole |
Account A’s user is missing the sts:AssumeRole permission, or Account B’s trust policy doesn’t include Account A |
Not authorized to perform: iam:PassRole |
You’re trying to pass the role to a service (like Lambda or ECS), which requires a separate iam:PassRole permission — this is different from AssumeRole |
| Credentials expire too quickly | Increase --duration-seconds in the assume-role call and update the role’s --max-session-duration |
Wrong account in get-caller-identity |
Environment variables from a previous session may be overriding your profile. Run unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN |
Conclusion
The setup comes down to: create a role with a trust policy in Account B, grant sts:AssumeRole in Account A, and use the CLI profile method to switch contexts automatically. For regular use, the ~/.aws/config profile approach is the cleanest — no scripts, no environment variables.
For specific cross-account use cases, see How to Copy S3 Bucket Objects Across AWS Accounts or How to Access AWS Secrets Manager from Another Account.


