Introduction

Cross-account S3 access fails when bucket policies contain explicit deny statements, when the requesting IAM principal lacks proper permissions, or when Service Control Policies (SCPs) at the organization level block the action. The error manifests as 403 AccessDenied even when both the bucket policy and IAM policy appear to grant access.

Symptoms

  • An error occurred (AccessDenied) when calling the GetObject operation: Access Denied
  • S3 access logs show 403 AccessDenied with the requester's account ID
  • aws s3 ls s3://bucket-name/ --profile cross-account fails but works with root account
  • CloudTrail shows s3:GetObject with errorCode: AccessDenied

Common Causes

  • Explicit Deny in bucket policy overrides Allow statements
  • Missing s3:ListBucket permission on the bucket resource (separate from object permission)
  • SCP at organization level blocks S3 actions from external accounts
  • Missing aws:PrincipalArn condition in bucket policy
  • VPC endpoint policy restricting S3 access for specific principals

Step-by-Step Fix

  1. 1.Check bucket policy for explicit denies:
  2. 2.```bash
  3. 3.aws s3api get-bucket-policy --bucket my-bucket --query 'Policy' --output text | python3 -m json.tool
  4. 4.`
  5. 5.Search for "Effect": "Deny" statements that may match the cross-account principal.
  6. 6.Verify IAM role permissions in the requesting account:
  7. 7.```bash
  8. 8.aws iam get-role-policy --role-name CrossAccountS3Role --policy-name S3Access
  9. 9.`
  10. 10.The role needs both s3:GetObject on arn:aws:s3:::bucket/* AND s3:ListBucket on arn:aws:s3:::bucket.
  11. 11.Check organization SCPs:
  12. 12.```bash
  13. 13.aws organizations list-policies --filter SERVICE_CONTROL_POLICY
  14. 14.aws organizations describe-policy --policy-id p-example
  15. 15.`
  16. 16.Look for SCPs that deny s3:* or specific S3 actions for the requesting account.
  17. 17.Update bucket policy for cross-account access:
  18. 18.```json
  19. 19.{
  20. 20."Version": "2012-10-17",
  21. 21."Statement": [
  22. 22.{
  23. 23."Sid": "CrossAccountAccess",
  24. 24."Effect": "Allow",
  25. 25."Principal": {"AWS": "arn:aws:iam::EXTERNAL-ACCOUNT:role/CrossAccountS3Role"},
  26. 26."Action": ["s3:GetObject", "s3:ListBucket"],
  27. 27."Resource": [
  28. 28."arn:aws:s3:::my-bucket",
  29. 29."arn:aws:s3:::my-bucket/*"
  30. 30.]
  31. 31.}
  32. 32.]
  33. 33.}
  34. 34.`
  35. 35.```bash
  36. 36.aws s3api put-bucket-policy --bucket my-bucket --policy file://policy.json
  37. 37.`
  38. 38.Verify access from the requesting account:
  39. 39.```bash
  40. 40.aws sts assume-role --role-arn arn:aws:iam::BUCKET-ACCOUNT:role/S3AccessRole \
  41. 41.--role-session-name test
  42. 42.aws s3 ls s3://my-bucket/ --profile assumed-role
  43. 43.`

Prevention

  • Use AWS RAM for structured cross-account resource sharing
  • Test bucket policy changes in the IAM policy simulator before applying
  • Document all cross-account access patterns in a central registry
  • Enable S3 Block Public Access to prevent accidental public exposure
  • Use VPC endpoints with proper policies for controlled access