Quick answer
Check the bucket policy's Principal field — it must use the root ARN of the other account (e.g., arn:aws:iam::111111111111:root), not an IAM role ARN. Also verify the IAM role in the source account has s3:GetObject (or the needed action) and the role trust policy allows the source account's users to assume it.
Why you're getting a 403
You're trying to let an EC2 instance in Account A access a bucket in Account B. You set up a bucket policy in Account B granting access to Account A's IAM role — but you keep hitting 403 AccessDenied. The root cause is almost always one of three things: the bucket policy's Principal is wrong, the IAM role in Account A doesn't have the right S3 actions, or there's a trust policy mismatch. AWS evaluates cross-account S3 requests by checking both the bucket policy and the caller's IAM permissions. Both must allow the action. If you get a 403, it means one of them is blocking the request.
Fix steps
- Verify the bucket policy's Principal — Open the bucket in Account B, go to Permissions > Bucket Policy. The
Principalmust be the root account ARN of Account A, not a specific role. Example:
This grants access to any IAM entity in Account A that has the right permissions. If you set it to a role ARN, it only works if that role is the direct caller — but cross-account requests always use the root principal. You can restrict further with"Principal": { "AWS": "arn:aws:iam::111111111111:root" }aws:SourceArnoraws:SourceAccountconditions. - Check the IAM role in Account A — The role your EC2 instance assumes must have an S3 action policy attached. For example, to read objects:
Don't forget the{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::your-bucket-name/*" } ] }/*on the resource — without it, you can't access objects inside the bucket. This is a common gotcha. - Validate the role trust policy — The role in Account A must allow the EC2 instance (or your IAM user) to assume it. On the role's Trust Relationships tab, ensure it has:
If you're testing from an AWS CLI session logged in as an IAM user, change the principal to that user's ARN.{ "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } - Test with the root principal first — Before adding conditions, simplify the bucket policy to allow all access from Account A to narrow down the issue. Use:
If this works, then start adding conditions like{ "Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::111111111111:root"}, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::your-bucket-name/*" }aws:SourceArnoraws:SourceAccount. If it still fails, the problem is not the bucket policy. - Check for explicit Deny policies — Account A might have a service control policy (SCP) or a permissions boundary that denies S3 access to certain accounts. Also check Account B for bucket ACLs that might block cross-account access. S3 evaluates bucket policies and ACLs together — an ACL with
DENYcan override an Allow in the policy. - Use the AWS CLI to debug — Run this from the EC2 instance in Account A, with the role assumed:
The debug output shows the exact error message and the policies being evaluated. Look for lines like "AccessDenied" or "User: arn:aws:sts::111111111111:assumed-role/your-role/i-xyz". That ARN tells you who AWS thinks is calling. If it's not the role you expect, the trust policy or instance profile is wrong.aws s3 ls s3://your-bucket-name/ --debug
Alternative fixes
If the bucket policy still fails, switch to an IAM-based approach. Don't use a bucket policy at all. Instead, in Account B, create an IAM role that grants s3:GetObject on the bucket. Then give Account A's IAM role permission to sts:AssumeRole into that role. This is more complex but gives you fine-grained control. The downside: you now manage two roles instead of one.
If you're using CloudFront, the origin access identity (OAI) can bypass bucket policies entirely — but that only works for CloudFront, not direct S3 access.
Prevention tip
Always test with the most permissive policy first — root principal, all actions — before locking it down. Use the --debug flag on every S3 CLI call during setup. It'll save you hours. Also, use aws:SourceIp or aws:SourceVpc conditions instead of aws:SourceArn unless you must tie it to a specific resource — this avoids the pain of updating policies when resources change. And always check the IAM role's trust policy separately from the bucket policy — they're independent and both can block you.