IAM to S3: every layer that votes on a request
An S3 request is allowed only if every applicable layer agrees — caller's IAM policy, bucket policy, object ownership, Block Public Access, KMS key policy, VPC endpoint policy and organization policies. What each side expects, the mismatches that bite, known-good and known-bad configurations, and how to tell which side refused.
markdownpatternv4RJZR-Y9C1stage: buildtrust: officialservice: iamservice: s3service: kmsinterface: iacIAM S3 permissionsbucket policy cross-accounts3:ListBucketbucket ARN vs object ARNKMS key policy S3cross-account bucket accessaws:PrincipalOrgID bucket policy
IAM to S3
IAM decides who the caller is and what it may attempt; S3 adds the bucket's own policy and three S3-only gates; KMS adds a third policy for encrypted data. Every layer that applies must allow, and any Deny ends it.
What crosses the handoff
| Layer | Owner | Must allow (cross-account) | Must allow (same account) |
|---|---|---|---|
| Caller's IAM policy | caller's account | yes | either this or the bucket policy |
| Bucket policy (or access point policy) | bucket's account | yes | either this or the IAM policy |
| Object Ownership | bucket's account | object must be owned by the bucket owner for a bucket-policy Allow to reach it | same |
| Block Public Access | bucket's account / organization | must not classify the grant as public | same |
| KMS key policy (SSE-KMS objects) | key's account | kms:Decrypt / kms:GenerateDataKey for the caller | same |
| VPC endpoint policy (request via endpoint) | VPC owner | yes | yes |
| SCPs (caller's org) / RCPs (bucket's org) | organizations | no Deny, Allow in path | same |
Mismatches that bite
- Bucket ARN vs object ARN.
s3:ListBucketis onarn:aws:s3:::<bucket>; object actions are onarn:aws:s3:::<bucket>/*. A policy with only/*cannot list; one with only the bare ARN cannot read. - No
s3:ListBucketturns 404 into 403. A missing key looks like a permission error. - Bucket-policy Allow skips foreign-owned objects. With ACLs enabled, objects uploaded by another account are not covered by the bucket owner's Allow (Deny still applies).
- One public statement poisons the policy. With
RestrictPublicBuckets, adding any public statement to a bucket policy cuts off every cross-account grant in it. aws/s3keys don't cross accounts. Data encrypted with the AWS managed key cannot be read by another account; use a customer managed key whose policy names the other account.- Bucket Keys change encryption context to the bucket ARN; key-policy conditions on the object ARN stop matching.
- Detailed 403 messages stop at the organization boundary. Cross-organization callers see only
Access Denied.
Known-good: another account reads one prefix
Bucket policy (bucket account):
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "ListPrefix", "Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::<reader-account>:role/<reader-role>" },
"Action": "s3:ListBucket", "Resource": "arn:aws:s3:::<bucket>",
"Condition": { "StringLike": { "s3:prefix": ["shared/*"] } } },
{ "Sid": "ReadPrefix", "Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::<reader-account>:role/<reader-role>" },
"Action": "s3:GetObject", "Resource": "arn:aws:s3:::<bucket>/shared/*" }
]
}
Reader role's IAM policy (reader account): Allow the same two actions on the same two ARNs. Bucket: Bucket owner enforced. If objects use SSE-KMS: a customer managed key whose key policy allows the reader role kms:Decrypt, plus kms:Decrypt on that key ARN in the reader role's policy. The policy uses a fixed principal, so Block Public Access leaves it alone.
For "anyone in our organization", replace the principal with "AWS": "*" and add "StringEquals": {"aws:PrincipalOrgID": "o-<id>"}: a fixed organization ID keeps the policy non-public.
Known-bad
| Configuration | Symptom |
|---|---|
| Bucket policy grants the reader; reader's IAM policy does not | Cross-account 403; same-account works |
Reader's IAM policy grants s3:GetObject on arn:aws:s3:::<bucket> (no /*) | 403 on every object read |
Objects written by a partner account into a bucket with ACLs enabled, no bucket-owner-full-control | Bucket owner's own roles get 403 on those objects only |
SSE-KMS with aws/s3, reader in another account | 403 on reads, though S3 policies are correct |
Bucket policy statement with "Principal": "*" and "aws:SourceVpc": "vpc-*" alongside a cross-account grant, RestrictPublicBuckets on | The cross-account grant stops working: the wildcard makes the policy public |
| Key policy with an encryption-context condition on the object ARN, then a Bucket Key enabled | Reads and writes of new objects fail on KMS |
Which side refused?
- Same organization: the 403 message names it — identity-based (caller), resource-based (bucket, or
RestrictPublicBuckets/IgnorePublicAcls), service/resource control (organization), VPC endpoint, or the Block Public Access setting by name. - Bare
Access Denied: test from each side. The bucket owner reading the same object proves ownership and KMS; the caller listing the prefix proves the IAM and bucket-policy grants;head-objectreveals the KMS key;get-bucket-ownership-controlsreveals ownership. Thenguides/s3/fix-403.md.