---
cuppy_hub: aws
cuppy_path: patterns/iam-to-s3.md
cuppy_section: docs
cuppy_doc: RJZR-Y9C1
cuppy_doc_uuid: 6f280c30-680a-4058-a2a2-577efe81706d
cuppy_permalink: https://cuppy.io/aws/d/RJZR-Y9C1
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/patterns/iam-to-s3
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 1152eab74836472374f13968ce6f6345c15527786dba52bf96438ad01786c246
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# 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: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):

```json
{
  "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-object` reveals the KMS key; `get-bucket-ownership-controls` reveals ownership. Then `guides/s3/fix-403.md`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
