---
cuppy_hub: aws
cuppy_path: guides/cloudfront/serve-s3-privately.md
cuppy_section: docs
cuppy_doc: T3FM-F7ZG
cuppy_doc_uuid: 6e6acfe8-b0bc-4583-a9b2-f96d461d954d
cuppy_permalink: https://cuppy.io/aws/d/T3FM-F7ZG
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/guides/cloudfront/serve-s3-privately
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: a1a9a5bb64da6df9e0fa9144da9a920bdec6669602ff0cb4c9fa29ac2b0d4b58
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Serve a private S3 bucket through CloudFront with OAC

**Keep the bucket private, with Block Public Access on and Object Ownership set to Bucket owner enforced. Give the distribution an origin access control (OAC) that signs every request, and grant `cloudfront.amazonaws.com` `s3:GetObject` in the bucket policy, conditioned on that one distribution's ARN.** Use the bucket's REST endpoint as the origin, not its static-website endpoint.

## When OAC does not apply

- **S3 static website endpoint** (`<bucket>.s3-website-<region>.amazonaws.com`): it must be configured as a *custom* origin, and neither OAC nor OAI works with it. If you need website features such as index documents in subfolders or redirects, use the REST endpoint plus a CloudFront Function, or accept a public website bucket (UNVERIFIED alternatives).
- **Origin redirects with Lambda@Edge** aren't supported with OAC.
- **Multi-Region Access Points** need a different OAC configuration.

OAC replaces the legacy **origin access identity** (OAI), which doesn't support SSE-KMS, `PUT`/`DELETE`, or Regions launched since early 2023.

## 1. Create the OAC

```ts
import { CloudFrontClient, CreateOriginAccessControlCommand } from "@aws-sdk/client-cloudfront";

const cf = new CloudFrontClient({ region: "us-east-1" });

const oac = await cf.send(new CreateOriginAccessControlCommand({
  OriginAccessControlConfig: {
    Name: "<bucket>-oac",
    OriginAccessControlOriginType: "s3",
    SigningBehavior: "always",
    SigningProtocol: "sigv4",
  },
}));
console.log(oac.OriginAccessControl?.Id); // put this in the origin's OriginAccessControlId
```

CLI: `aws cloudfront create-origin-access-control --cli-input-yaml file://oac.yaml`. Generate the skeleton with `--generate-cli-skeleton yaml-input` and set `SigningBehavior: always`.

`SigningBehavior`:

- **`always`** is recommended. It is also the only setting that guarantees HTTPS between CloudFront and S3.
- `never` turns signing off; the bucket must then be public.
- `no-override` passes a viewer's `Authorization` header through, and you must add that header to the cache policy.

The SDK's type also lists `always-amz-auth`, which the page doesn't describe; don't use it without its documentation.

## 2. Attach it to the origin

For an existing distribution:

```
aws cloudfront get-distribution-config --id <dist-id> --output yaml > dist.yaml
# edit: Origins → set OriginAccessControlId: <oac-id>; clear OriginAccessIdentity; rename ETag → IfMatch
aws cloudfront update-distribution --id <dist-id> --cli-input-yaml file://dist.yaml
```

The origin domain is the REST endpoint, `<bucket>.s3.<region>.amazonaws.com`. The change deploys to all edge locations (UNVERIFIED duration).

## 3. Bucket policy (the bucket owner's account)

```json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "AllowCloudFrontOAC",
    "Effect": "Allow",
    "Principal": { "Service": "cloudfront.amazonaws.com" },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::<bucket>/*",
    "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::<account>:distribution/<dist-id>" } }
  }]
}
```

`aws s3api put-bucket-policy --bucket <bucket> --policy file://policy.json`. Add `s3:PutObject` only if viewers upload through CloudFront. The `AWS:SourceArn` condition is what stops *other* distributions from using the grant. Without `s3:ListBucket`, a missing key returns 403, not 404.

## 4. SSE-KMS objects: also the key policy

```json
{
  "Sid": "AllowCloudFrontOACKms",
  "Effect": "Allow",
  "Principal": { "Service": "cloudfront.amazonaws.com" },
  "Action": ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey*"],
  "Resource": "*",
  "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::<account>:distribution/<dist-id>" } }
}
```

Add it to the customer managed key's policy. Objects under the AWS managed `aws/s3` key can't be served this way, because an AWS managed key's policy can't be modified (stated in SQS's key-management guide; consistent with `reference/s3/encryption.md` and `reference/kms/key-policies-and-errors.md`).

## 5. Custom domain

1. Request or import the certificate in ACM in **us-east-1** (for viewer HTTPS, whatever Region the bucket is in). It must cover the domain in its SAN field, by exact name or a wildcard at the same level.
2. Add the domain as an **alternate domain name** on the distribution. A DNS record pointing at CloudFront without this returns 403.
3. Create a Route 53 alias record to the distribution (`guides/route53/point-domain-at-aws.md`).

## Migrating from OAI without downtime

1. Add a second bucket-policy statement for the OAC principal, keeping the OAI statement.
2. Switch the origin to the OAC and remove `OriginAccessIdentity`.
3. After the distribution has fully deployed, remove the OAI statement.

## Deleting

Delete the distribution before the bucket, or remove the OAC first.

## When it returns 403

The usual causes are a bucket policy without the OAC statement or with the wrong distribution ARN; a wrong path or missing object; the KMS key policy; `Host` header forwarding to the REST endpoint; or Restrict Viewer Access without signed URLs. See `reference/cloudfront/errors.md`, then `guides/s3/fix-403.md` for the S3 side.


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