---
cuppy_hub: aws
cuppy_path: patterns/cloudfront-to-s3.md
cuppy_section: docs
cuppy_doc: 7P24-6VX9
cuppy_doc_uuid: 12ef8267-e125-4e1f-aaae-e638e9f1379c
cuppy_permalink: https://cuppy.io/aws/d/7P24-6VX9
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/patterns/cloudfront-to-s3
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 7673e979608133783011ef5f7f9b606888b289fe380b00a40c8010f190078b8d
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# CloudFront to S3

**The bucket stays private. CloudFront signs each origin request as the service principal `cloudfront.amazonaws.com`, and the bucket policy and any KMS key policy allow that principal only when `AWS:SourceArn` is this distribution.** Nothing about the bucket needs to be public, and Block Public Access stays on, because a policy scoped to one service principal and one distribution isn't public (UNVERIFIED against the BPA "public" rules; consistent with `reference/s3/access-control.md`).

## What crosses the handoff

| Side | Provides / expects |
|---|---|
| **CloudFront** | An origin with domain `<bucket>.s3.<region>.amazonaws.com` (the REST endpoint), an attached OAC with `SigningBehavior: always`, `SigningProtocol: sigv4`, type `s3`. It sends SigV4-signed `GET`s, plus `PUT`/`DELETE` if the behaviour allows those methods. |
| **S3** | A bucket policy granting `s3:GetObject` (and `s3:PutObject` for uploads) on `arn:aws:s3:::<bucket>/*` to `cloudfront.amazonaws.com`, with `StringEquals AWS:SourceArn arn:aws:cloudfront::<account>:distribution/<id>`. Object Ownership set to **Bucket owner enforced** (the default), or Bucket owner preferred if you need ACLs. SSE-KMS keys need a key-policy statement for the same principal and condition (`kms:Decrypt`, `kms:Encrypt`, `kms:GenerateDataKey*`). |

## Known-good

1. Bucket: private, BPA on, Bucket owner enforced, default encryption SSE-S3 or SSE-KMS with a customer managed key.
2. OAC created and attached to the origin; no OAI on the origin.
3. Bucket policy (one statement per distribution, or a list of ARNs in the condition):

```json
{ "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" },
  "Action": "s3:GetObject", "Resource": "arn:aws:s3:::<bucket>/*",
  "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::<account>:distribution/<id>" } } }
```

4. KMS key-policy statement (SSE-KMS only), with the same principal and condition.
5. Viewer protocol policy set to Redirect to HTTPS. With signing `always`, CloudFront always uses HTTPS to S3.
6. Optional: `s3:ListBucket` on `arn:aws:s3:::<bucket>`, so a missing key returns 404 instead of 403 (and is cacheable as 404).

The full walkthrough is in `guides/cloudfront/serve-s3-privately.md`.

## Known-bad

| Configuration | Symptom |
|---|---|
| Origin uses the **website endpoint** (`s3-website-…`) with OAC | Doesn't work: website endpoints are custom origins and OAC/OAI don't apply. Bucket must be public, or switch to the REST endpoint |
| OAC attached, but the bucket policy has no `cloudfront.amazonaws.com` statement | 403 for every object |
| `AWS:SourceArn` names another distribution (a copy-paste from staging) | 403 on this distribution only |
| SSE-KMS bucket; key policy lacks the CloudFront statement | 403 for encrypted objects only; unencrypted ones work |
| Objects under the AWS managed `aws/s3` key | 403. That key's policy can't grant CloudFront (UNVERIFIED in the collected sources) |
| Old OAI statement removed before the origin switched to OAC | Outage during migration. Keep both statements until deployment completes |
| Viewer `Host` header forwarded to the S3 origin | 403 |
| Origin is another CloudFront distribution | 403 (stacked distributions aren't supported) |
| `SigningBehavior: never` on a private bucket | 403: requests arrive unsigned |
| Bucket ACLs required (Object Ownership Object writer) | Objects written by other accounts stay unreadable to CloudFront |
| Bucket deleted before the distribution | Delete the distribution first, or remove the OAC |

## Which side is wrong?

- **403 on everything** → the S3 side: the bucket-policy statement or its distribution ARN, or the OAC not attached (on the CloudFront side).
- **403 only on some objects** → KMS on those objects, wrong keys or paths, or objects owned by another account.
- **403 that persists after fixing the bucket** → CloudFront's cached error. Invalidate the path, or wait out the error-caching TTL (UNVERIFIED default).
- **502** → not S3 access: the origin domain is wrong or a function is broken (`reference/cloudfront/errors.md`).

Check from the S3 side with `aws s3api get-bucket-policy --bucket <bucket>` and CloudTrail (data events). S3 server access logs show requests from CloudFront (UNVERIFIED field values).


---
<!-- 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 -->
