CloudFront to S3: OAC, bucket policy and KMS

CloudFront reads a private S3 bucket by signing requests with origin access control. What each side expects — REST endpoint, signing behaviour, the bucket policy and KMS key policy conditioned on the distribution ARN, Object Ownership — a known-good setup, and known-bad ones with the 403 or 502 each produces.

markdownpatternv47P24-6VX9stage: buildtrust: officialservice: cloudfrontservice: s3interface: iacinterface: cliCloudFront S3 handoffOAC bucket policyAWS:SourceArn distributioncloudfront.amazonaws.com principalSSE-KMS CloudFrontS3 website endpoint CloudFrontOAI to OAC migrationBlock Public Access CloudFrontCloudFront 403 S3

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

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

SideProvides / expects
CloudFrontAn 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 GETs, plus PUT/DELETE if the behaviour allows those methods.
S3A 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):
{ "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>" } } }
  1. KMS key-policy statement (SSE-KMS only), with the same principal and condition.
  2. Viewer protocol policy set to Redirect to HTTPS. With signing always, CloudFront always uses HTTPS to S3.
  3. 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

ConfigurationSymptom
Origin uses the website endpoint (s3-website-…) with OACDoesn'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 statement403 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 statement403 for encrypted objects only; unencrypted ones work
Objects under the AWS managed aws/s3 key403. That key's policy can't grant CloudFront (UNVERIFIED in the collected sources)
Old OAI statement removed before the origin switched to OACOutage during migration. Keep both statements until deployment completes
Viewer Host header forwarded to the S3 origin403
Origin is another CloudFront distribution403 (stacked distributions aren't supported)
SigningBehavior: never on a private bucket403: requests arrive unsigned
Bucket ACLs required (Object Ownership Object writer)Objects written by other accounts stay unreadable to CloudFront
Bucket deleted before the distributionDelete 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).

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