Amazon S3
Cross-account S3 access with IAM roles and bucket policies
Cross-account access needs an Allow on both sides. Compare granting a remote role in the bucket policy with having the remote workload assume a role in the bucket's account, and verify each with the CLI.
Published · Updated .
A data platform in account 111122223333 owns a bucket; an analytics workload in account 444455556666 needs to read part of it. There are two sound ways to do this with IAM roles, and one outdated way (long-term access keys for an IAM user in the bucket account) that you should avoid. This guide builds both role-based patterns, explains what each costs you, and shows how to verify and troubleshoot them.
Pattern A: grant the external role in the bucket policy
The bucket owner adds a statement naming the analytics role. The analytics account then grants its role the matching S3 permissions.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AnalyticsRoleListsCurated",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:role/AnalyticsReader"
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket",
"Condition": {
"StringLike": {
"s3:prefix": "curated/*"
}
}
},
{
"Sid": "AnalyticsRoleReadsCurated",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:role/AnalyticsReader"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/curated/*"
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListPartnerBucketCurated",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket",
"Condition": {
"StringLike": {
"s3:prefix": "curated/*"
}
}
},
{
"Sid": "ReadPartnerBucketCurated",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/curated/*"
}
]
}What to know about pattern A
- The bucket policy names a specific role, not
arn:aws:iam::444455556666:root, so the bucket owner controls exactly which identity can read. - If the role is deleted and recreated, the bucket policy will show an internal principal ID instead of the ARN and stop matching. Recreating a role does not silently regain access.
- Writes from the other account: with Object Ownership set to bucket owner enforced (the default for new buckets since April 2023), the bucket owner owns every object, whoever uploaded it. On older buckets with ACLs enabled, objects uploaded by another account are owned by the uploader, a frequent cause of “I can't read objects in my own bucket”.
Pattern B: assume a role in the bucket's account
The bucket owner creates a role (here CuratedDataReader) in 111122223333 with an ordinary identity policy for the bucket, and lets the analytics role assume it. The bucket policy does not change at all, because access becomes same-account access after the role is assumed.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TrustAnalyticsReader",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:role/AnalyticsReader"
},
"Action": "sts:AssumeRole"
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeCuratedDataReader",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::111122223333:role/CuratedDataReader"
}
]
}The reader role's own permissions are a normal prefix-scoped identity policy, like the prefix example. Reusable versions of these trust and caller policies are in the example library: trust policy and caller permission.
What to know about pattern B
- The bucket account owns every permission decision; nothing about the bucket is exposed to the other account's policies.
- CloudTrail in the bucket account records the assumed-role session, including the session name you choose.
- The workload must handle temporary credentials (the SDKs do this through profiles or explicit
AssumeRolecalls). Sessions are limited by the role's maximum session duration; for role chaining, AWS limits sessions to one hour.
Choosing between them
| Consideration | A: bucket policy grant | B: assume a role |
|---|---|---|
| Changes needed | Bucket policy + caller identity policy | New role + trust + caller identity policy |
| Credentials in workload | Its own role, unchanged | Must assume a second role |
| Who controls effective access | Shared: both accounts | Bucket account (through the role) |
| Good fit | Few consumers, simple read access | Many resources, or access beyond S3 |
If the objects are encrypted with SSE-KMS
The two patterns differ here. In pattern A the caller is in another account, and the key policy of an AWS managed key such as aws/s3 cannot be changed to admit it, so objects must be encrypted with a customer managed key whose key policy allows the external role. The caller's identity policy must also allow kms:Decrypt for reads (plus kms:GenerateDataKey for writes) on that key. In pattern B the reader role belongs to the bucket's account, so this is ordinary same-account access: aws/s3 works, and a customer managed key works if its key policy allows the account's IAM policies to grant access (the default key policy does) and the reader role's policy grants the KMS actions.
Verification
# Pattern A: call S3 directly with the AnalyticsReader role's credentials
aws s3 ls s3://amzn-s3-demo-bucket/curated/
aws s3 cp s3://amzn-s3-demo-bucket/curated/<key> ./
aws s3 ls s3://amzn-s3-demo-bucket/raw/ # expect AccessDenied
# Pattern B: assume the reader role first
aws sts assume-role --role-arn arn:aws:iam::111122223333:role/CuratedDataReader --role-session-name analytics-test
aws sts get-caller-identity # with the returned credentials: shows the assumed-role ARNTroubleshooting AccessDenied
- Read the message. Where S3 includes the reason, the access-denied message names the policy type that blocked the request (for example “no identity-based policy allows the s3:GetObject action” or “explicit deny in a resource-based policy”), which tells you which side to fix. S3 adds this detail only for callers in the bucket owner's account or organization; others get a plain AccessDenied, and CloudTrail in the bucket owner's account is the place to look.
- 403 on a missing object. Without
s3:ListBucket, S3 returns 403 rather than 404 for keys that do not exist. - KMS errors. A message mentioning
kms:Decryptmeans the S3 permissions are fine and the key is the problem. - Guardrails. Organization policies can block a request that both identity and resource policies allow, but which ones apply depends on the pattern. SCPs only limit principals in the accounts they are attached to. In pattern A that means SCPs on the caller's account (
444455556666); the bucket's account constrains outside callers with a resource control policy (RCP) or an organization-perimeter deny in the bucket policy. In pattern B the request is made by a role in the bucket's account, so that account's SCPs apply to it, along with the caller account's SCPs on the initialsts:AssumeRole.