Two Approaches to Cross-Account Amazon S3 Access: Direct Policies vs. AssumeRole
In AWS multi-account environments, it’s common to separate workloads, environments, or business units into different accounts. But sometimes you need to share an Amazon S3 bucket between accounts. For example, Account A might run an application that needs to read and write data into a bucket owned by Account B.
This blog will guide you through two approaches for enabling cross-account access:
- Direct access – Allow a role from Account A directly in Account B’s bucket policy (and KMS key policy if encrypted).
- Recommended access – Create a role in Account B and let Account A assume it securely (with External ID).
We’ll also cover necessary KMS permissions when the bucket uses a customer-managed key.
Scenario
- Account A (111111111111): Workload that needs read/write access.
- Account B (222222222222): Owns S3 bucket
my-shared-bucket
(optionally encrypted with a KMS CMKarn:aws:kms:region:222222222222:key/KEY-ID
).
Shared Prerequisites
- Replace account IDs, ARNs, regions, role names, and bucket names with your own.
- Include
ListBucket
for listing keys and object-level permissions for actual reads/writes. - If the bucket uses a KMS CMK, grant principals KMS permissions (
kms:Encrypt
,kms:Decrypt
,kms:ReEncrypt*
,kms:GenerateDataKey*
,kms:DescribeKey
). - Prefer S3 Object Ownership = Bucket owner enforced to avoid ACL complexities.
Approach 1 — Direct Role Access from Account A
Step 1. Create a Role in Account A
Role: RoleInAccountA
IAM policy attached to RoleInAccountA
:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RWObjects",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-shared-bucket/*"
},
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-shared-bucket"
}
]
}
Step 2. Update Bucket Policy in Account B
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowRoleInAccountAReadWrite",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/RoleInAccountA"
},
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-shared-bucket/*"
},
{
"Sid": "AllowRoleInAccountAList",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/RoleInAccountA"
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-shared-bucket"
}
]
}
Step 3. Update KMS Key Policy in Account B (If Encrypted)
{
"Sid": "AllowRoleInAccountAToUseKey",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/RoleInAccountA"
},
"Action": [
"kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:aws:s3:arn": "arn:aws:s3:::my-shared-bucket"
}
}
}
Approach 2 (Recommended) — Assume a Role in Account B
Why This is Better
- Least-privilege – Permissions live in Account B, closest to the resource.
- Clear auditing –
sts:AssumeRole
calls visible in CloudTrail. - KMS simplicity – Key trusts only a role inside Account B.
- External ID – Protects against confused deputy attacks.
Step 1. Create Role in Account B (S3AccessFromA
)
Permissions policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RWObjects",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-shared-bucket/*"
},
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-shared-bucket"
}
]
}
If KMS-encrypted:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "UseKmsForBucket",
"Effect": "Allow",
"Action": [
"kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:DescribeKey"
],
"Resource": "arn:aws:kms:region:222222222222:key/KEY-ID"
}
]
}
Step 2. Trust Policy for S3AccessFromA
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TrustRoleInAccountAWithExternalId",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/RoleInAccountA"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "YOUR-STRONG-EXTERNAL-ID"
}
}
}
]
}
Step 3. KMS Key Policy in Account B
{
"Sid": "AllowS3AccessFromARoleToUseKey",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::222222222222:role/S3AccessFromA"
},
"Action": [
"kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:aws:s3:arn": "arn:aws:s3:::my-shared-bucket"
}
}
}
Step 4. Bucket Policy in Account B
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowOnlyTargetRoleDataPlane",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::222222222222:role/S3AccessFromA"
},
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-shared-bucket/*"
},
{
"Sid": "AllowOnlyTargetRoleList",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::222222222222:role/S3AccessFromA"
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-shared-bucket"
}
]
}
Step 5. Minimal Policy in Account A to Assume Role
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeCrossAccountRole",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::222222222222:role/S3AccessFromA"
}
]
}
Usage:
aws sts assume-role \
--role-arn arn:aws:iam::222222222222:role/S3AccessFromA \
--role-session-name from-account-a \
--external-id YOUR-STRONG-EXTERNAL-ID \
--profile account-a
Then use temporary credentials to perform S3 operations.
Comparison
Approach | Pros | Cons |
---|---|---|
Direct (Account A role in bucket policy) | Simpler setup | Harder to scale, bucket/key policies trust external principals directly |
Recommended (Assume role in Account B) | Stronger isolation, External ID support, better auditing | Slightly more setup |
Conclusion
By configuring bucket policies, IAM roles, and KMS key policies, you can securely enable cross-account read and write access in S3. While direct bucket policy access works, the recommended approach—assuming a role in Account B—provides stronger isolation, auditability, and long-term maintainability.