Cross-Account Access
Most real companies do not run everything in one AWS account. They split workloads into separate accounts — one for production, one for staging, one for security logs, maybe one per team. This isolation is great for safety and billing, but it creates a new problem: how does a user or service in Account A reach an S3 bucket or database in Account B? Cross-account access is the set of patterns AWS gives you to do this securely, without ever sharing long-lived passwords or access keys. This page explains the two main patterns and the single most common mistake people hit.
Two ways to share across accounts
There are exactly two patterns for cross-account access, and picking the right one matters.
The first pattern is assume a role in the target account. The account that owns the resource (the target) creates an IAM role (IAM stands for Identity and Access Management). That role trusts the other account. A principal (a user or service) in the source account then calls AWS Security Token Service (STS, the service that hands out temporary credentials) to “assume” that role and gets short-lived keys that act as if they belong to the target account.
The second pattern is a resource-based policy that names the other account directly. Some AWS services let you attach a policy to the resource itself (an S3 bucket, an SQS queue, a KMS key) and say “Account B is allowed to use me.” No role assumption happens — the foreign principal just calls the resource and AWS checks the resource’s own policy.
| Aspect | Assume-role (IAM role) | Resource-based policy |
|---|---|---|
| Where the grant lives | A role in the target account | The resource itself in the target account |
| Works for | Any service, any number of resources | Only services that support it (S3, SQS, SNS, KMS, Lambda, Secrets Manager, etc.) |
| Credentials | Temporary, swapped via STS | None — caller uses their own identity |
| Best when | Broad or ongoing access, many resources, auditing matters | One specific resource shared with one or two accounts |
| Audit trail | Clean (AssumeRole shows in CloudTrail) | Caller’s identity appears directly |
When to use which: Reach for roles when access is broad, long-running, or spans many resources — it scales and audits cleanly. Reach for a resource-based policy when you just want to hand one bucket or one queue to one partner account. Do not use resource policies for sprawling access; they become impossible to track.
The assume-role flow, step by step
Say the source account is 111111111111 (where your engineers log in) and the target account is 222222222222 (where a production S3 bucket lives). You want source-account users to read that bucket.
The flow requires two halves, one in each account:
- In the target account, create a role with a trust policy that allows the source account to assume it, plus a permissions policy describing what the role can do.
- In the source account, grant the user permission to call
sts:AssumeRoleon that role.
Half 1 — the role in the target account (Console)
- Sign in to the target account (
222222222222) and open the IAM console. - Go to Roles then Create role.
- For trusted entity, choose AWS account then Another AWS account, and enter the source account ID
111111111111. - (Recommended) tick Require external ID and enter a secret string — this blocks the “confused deputy” attack when a third party assumes the role for you.
- Attach a permissions policy, for example
AmazonS3ReadOnlyAccess, then name the roleCrossAccountS3Readerand create it.
The resulting trust policy looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111111111111:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "my-secret-123" }
}
}
]
}
arn:aws:iam::111111111111:root does not mean the root user — it means “any principal in account 111111111111 that also has IAM permission to assume this role.”
Half 2 — the permission in the source account
The user in the source account needs an identity policy that allows the assume call:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::222222222222:role/CrossAccountS3Reader"
}
]
}
Switching roles in the Console
- While signed in to the source account, click your account name (top-right) then Switch role.
- Enter target account
222222222222and role nameCrossAccountS3Reader. - Click Switch Role. The console banner changes color and you are now operating inside the target account with the role’s permissions.
Assuming the role with the CLI
Using AWS CLI v2 (the current 2026 version):
aws sts assume-role \
--role-arn arn:aws:iam::222222222222:role/CrossAccountS3Reader \
--role-session-name dev-session \
--external-id my-secret-123
Output:
{
"Credentials": {
"AccessKeyId": "ASIA0A1B2C3D4E5F6G7H",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"SessionToken": "FQoGZXIvYXdzEJ...truncated...",
"Expiration": "2026-06-15T13:45:00+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROA...:dev-session",
"Arn": "arn:aws:sts::222222222222:assumed-role/CrossAccountS3Reader/dev-session"
}
}
Export those three values as AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN, and subsequent commands run as the target-account role. Easier still, define a profile in ~/.aws/config and let the CLI do it automatically:
[profile prod-reader]
role_arn = arn:aws:iam::222222222222:role/CrossAccountS3Reader
source_profile = default
external_id = my-secret-123
Then just run aws s3 ls --profile prod-reader.
The gotcha: both sides must agree
This trips up nearly everyone. Cross-account assume-role needs permission on both sides:
- The target account’s role must trust the source (the trust policy).
- The source account’s user must be allowed to call
sts:AssumeRole(an identity policy).
If either half is missing, STS returns the same vague error and never tells you which side failed:
An error occurred (AccessDenied) when calling the AssumeRole operation:
User: arn:aws:iam::111111111111:user/alice is not authorized to perform:
sts:AssumeRole on resource: arn:aws:iam::222222222222:role/CrossAccountS3Reader
Gotcha: That message is identical whether the source user lacks
sts:AssumeRoleor the target trust policy omits the source account. Always check both policies. If you supplied an external ID, a mismatch produces the sameAccessDeniedtoo.
There is no extra charge for AssumeRole calls or cross-account access itself — you only pay for the resources you actually use (the S3 requests, data transfer, etc.).
Best practices
- Grant the role only the permissions it needs (least privilege) — never attach
AdministratorAccessto a cross-account role for convenience. - Always set an external ID when a third-party vendor assumes your role, to prevent confused-deputy attacks.
- Prefer roles over sharing access keys; temporary STS credentials expire automatically (default one hour).
- Use a unique, descriptive
--role-session-nameso CloudTrail logs show who assumed the role. - Add
Conditionkeys (such asaws:PrincipalOrgID) to restrict trust to accounts inside your AWS Organization. - Review trust policies regularly and remove accounts that no longer need access.