skip to content

How do you configure a CloudFront distribution and an S3 bucket so the bucket's objects are readable only through the distribution, and what does Origin Access Control (OAC) do that the legacy Origin Access Identity (OAI) does not?

level: middleimportance: must knowfreq 66%

answer

  1. bucket stays private, edge fetches for you
  2. REST endpoint, not the website endpoint
  3. OAC signs the origin request
  4. scope the bucket policy to one distribution
  5. the legacy identity cannot do KMS

basics

~20 s

Use the bucket's REST endpoint as the origin, keep Block Public Access on, and attach an Origin Access Control so CloudFront signs each origin request with SigV4; the bucket then grants read only to the CloudFront service principal for that distribution. OAC, unlike OAI, works with SSE-KMS objects and non-GET methods.

solid answer

~50 s

Point the origin at the bucket's REST endpoint (`bucket.s3.<region>.amazonaws.com`), not the static-website endpoint, and leave S3 Block Public Access fully on. Create an **Origin Access Control** with signing behaviour "always" and attach it to that origin — CloudFront then signs every origin request with SigV4 as the CloudFront service. The bucket side grants `s3:GetObject` to the service principal `cloudfront.amazonaws.com`, conditioned on `aws:SourceArn` matching that specific distribution's ARN, so no other distribution — including one in someone else's account — can read it. **OAC replaces OAI** and adds what OAI cannot do: because it signs with SigV4 it can read objects encrypted with SSE-KMS (given a key policy that permits the distribution), it supports methods beyond GET and HEAD such as PUT and DELETE, it works in every region including ones that require SigV4 only, and it is a distribution-scoped construct rather than a long-lived special identity. New distributions should use OAC; OAI exists only for setups built before it.

code

json · 17 lines
json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipalReadOnly",
      "Effect": "Allow",
      "Principal": { "Service": "cloudfront.amazonaws.com" },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::app-assets/*",
      "Condition": {
        "StringEquals": {
          "aws:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1EXAMPLE"
        }
      }
    }
  ]
}

go deeper

for a junior

Recall that the bucket should stay private with Block Public Access on, and that CloudFront reaches it through an origin access control rather than by the bucket being open to the world.

for a middle

Describe the wiring end to end: REST endpoint origin, OAC attached with signing always, bucket policy granting read to the CloudFront service principal scoped to the distribution, and how you would verify the direct URL now fails.

for a senior

Reason about the confused-deputy risk the source-ARN condition removes, the KMS interaction that forces an OAI migration, and how you would roll such a migration out without a window where objects are unreachable.

for a principal

Own the standard: mandate OAC for every new distribution, drive the OAI backlog to zero, and decide how origin-access wiring is enforced and detected across many accounts.

## The problem A CloudFront distribution in front of a public bucket does not protect anything: the bucket's own endpoint is still on the internet, so viewers can skip the CDN, skip your signed URLs and geo rules, and read objects directly. Origin access is the mechanism that closes that door. ## The three moving parts **1. The right origin endpoint.** The origin must be the bucket's REST endpoint. If you instead select the S3 *static website* endpoint, CloudFront treats it as a generic custom origin — that endpoint speaks unauthenticated HTTP and cannot honour a signed request, so the bucket must be public and origin access is impossible. The tradeoff is that the REST endpoint gives you no index-document or redirect handling. **2. Block Public Access on, and no public policy.** Origin access is meaningless if a leftover public statement or ACL still grants `s3:GetObject` to `*`. **3. The OAC plus the matching bucket policy.** Create an OAC of type `s3` with signing behaviour "always", attach it to the origin, and add a bucket policy statement granting read to the CloudFront service principal, scoped to your distribution: ```json { "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::app-assets/*", "Condition": { "StringEquals": { "aws:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1EXAMPLE" } } } ``` The `aws:SourceArn` condition is not optional decoration. Without it the statement trusts the CloudFront *service*, which means any CloudFront distribution anywhere — including one created by a stranger who knows your bucket name — could be pointed at your bucket and read it. This is the confused-deputy problem, and the condition is the fix. ## OAC versus OAI OAI was the original mechanism: a special CloudFront identity, represented in bucket policies as a canonical user, that CloudFront presented when fetching. It works, and existing distributions still use it, but it has hard limits: | | OAI (legacy) | OAC (current) | |---|---|---| | How CloudFront authenticates | a special CloudFront user identity | SigV4-signed request as the CloudFront service | | SSE-KMS encrypted objects | not supported | supported, with a key policy allowing the distribution | | Methods | GET and HEAD | also supports uploads and deletes when enabled | | Regions | not all newer regions | all regions, including SigV4-only ones | | Scoping | one identity reusable across distributions | condition-scoped per distribution | The SSE-KMS gap is the one that bites in practice: teams enable KMS encryption on a bucket that has worked for years behind OAI and every object starts returning an error, because the legacy identity cannot produce a signed request that the KMS integration will accept. Migrating that origin to OAC and adding `kms:Decrypt` for the distribution in the key policy is the fix. ## Verifying it After the change, two checks: a request through the distribution's domain returns the object, and a direct request to `https://bucket.s3.<region>.amazonaws.com/key` returns `AccessDenied`. If the direct request still succeeds, something public remains. ## The 403 that surprises people Because the policy grants only `s3:GetObject` and not `s3:ListBucket`, S3 answers a request for a key that does not exist with **403 AccessDenied**, not 404 — S3 deliberately does not confirm whether an object exists to a caller that lacks list permission. That 403 flows straight through CloudFront to the browser, which is why missing files behind OAC look like permission failures rather than typos.

  • Why must the bucket policy carry an `aws:SourceArn` condition rather than just trusting the CloudFront service principal?
    Without it the policy trusts every CloudFront distribution in existence, so anyone who learns the bucket name could create their own distribution pointed at it and read your objects — a textbook confused-deputy attack. The condition pins the grant to your distribution's ARN, so only that distribution's signed requests are accepted.
  • An existing OAI-based distribution starts returning errors for every object after SSE-KMS is enabled on the bucket. What happened and what is the fix?
    OAI cannot make the SigV4-signed request that KMS-encrypted reads require, so decryption is never authorised. Replace OAI with an Origin Access Control on that origin, update the bucket policy to the service-principal form with `aws:SourceArn`, and allow the distribution `kms:Decrypt` in the key policy.
  • Why does a missing object behind OAC return 403 rather than 404?
    The bucket policy grants `s3:GetObject` but not `s3:ListBucket`. S3 will not reveal whether a key exists to a caller without list permission, so it answers AccessDenied for both a missing object and a forbidden one. CloudFront passes that 403 through, which makes typos look like permission problems.

saying these in an interview costs you the question

  • Makes the bucket public and calls CloudFront the protection
  • Omits the aws:SourceArn condition from the bucket policy
  • Uses the S3 website endpoint and expects origin access to work
  • Says OAI and OAC are interchangeable
  • Assumes OAI can read SSE-KMS encrypted objects

context