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

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

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

LayerOwnerMust allow (cross-account)Must allow (same account)
Caller's IAM policycaller's accountyeseither this or the bucket policy
Bucket policy (or access point policy)bucket's accountyeseither this or the IAM policy
Object Ownershipbucket's accountobject must be owned by the bucket owner for a bucket-policy Allow to reach itsame
Block Public Accessbucket's account / organizationmust not classify the grant as publicsame
KMS key policy (SSE-KMS objects)key's accountkms:Decrypt / kms:GenerateDataKey for the callersame
VPC endpoint policy (request via endpoint)VPC owneryesyes
SCPs (caller's org) / RCPs (bucket's org)organizationsno Deny, Allow in pathsame

Mismatches that bite

  • Bucket ARN vs object ARN. s3:ListBucket is on arn:aws:s3:::<bucket>; object actions are on arn:aws:s3:::<bucket>/*. A policy with only /* cannot list; one with only the bare ARN cannot read.
  • No s3:ListBucket turns 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/s3 keys 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

ConfigurationSymptom
Bucket policy grants the reader; reader's IAM policy does notCross-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-controlBucket owner's own roles get 403 on those objects only
SSE-KMS with aws/s3, reader in another account403 on reads, though S3 policies are correct
Bucket policy statement with "Principal": "*" and "aws:SourceVpc": "vpc-*" alongside a cross-account grant, RestrictPublicBuckets onThe 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 enabledReads 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-object reveals the KMS key; get-bucket-ownership-controls reveals ownership. Then guides/s3/fix-403.md.

Read this document as markdown — the same bytes an agent receives.