Amazon S3
Private S3 bucket behind CloudFront (Origin Access Control)
Bucket policy for CloudFront OAC: one distribution may read, the bucket stays private, plus SSE-KMS and OAI migration.
Published · Updated .
The problem
A static site or asset store should be reachable through CloudFront and nowhere else. Making the bucket public or turning on S3 static website hosting works, but it bypasses CloudFront's caching, WAF rules and signed URLs, and it means turning off Block Public Access.
With Origin Access Control (OAC), CloudFront signs its requests to the S3 REST endpoint as the service principal cloudfront.amazonaws.com. The bucket policy grants that principal read access, but only for requests made on behalf of one distribution. The bucket stays private and Block Public Access stays on. OAC replaces the older origin access identity (OAI), which AWS now calls legacy; OAC adds SSE-KMS support, all Regions including newer opt-in Regions, and dynamic requests such as PUT.
The policies
Bucket policy: CloudFront OAC read access
Amazon S3 bucket policy. Attach to: The bucket policy of the origin bucket amzn-s3-demo-bucket, merged with existing statements
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOacRead",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*",
"Condition": {
"StringEquals": {
"aws:SourceArn": "arn:aws:cloudfront::111122223333:distribution/EDFDVBD6EXAMPLE"
}
}
}
]
}Statement by statement
- The principal is the CloudFront service, which every CloudFront customer's distributions share. The aws:SourceArn condition is what ties the grant to your distribution EDFDVBD6EXAMPLE; without it, a distribution in any other account could be pointed at your bucket and read it. Distribution ARNs have no Region field, hence the double colon. s3:ListBucket is deliberately left out; see the pitfall about 403 and 404.
During an OAI-to-OAC migration: allow both for a short time
Amazon S3 bucket policy. Attach to: The same bucket policy, temporarily, while the distribution switches from OAI to OAC
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOacRead",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*",
"Condition": {
"StringEquals": {
"aws:SourceArn": "arn:aws:cloudfront::111122223333:distribution/EDFDVBD6EXAMPLE"
}
}
},
{
"Sid": "AllowLegacyOaiReadUntilCutover",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <oai-id>"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
}
]
}The form generators on this site don't edit bucket policy documents; copy it, then check it with IAM Access Analyzer as shown below.
Statement by statement
- The OAC grant from the main policy, added first so that edge locations which already have the new configuration can read.
- The existing OAI grant, kept until the distribution update has fully deployed, because edge locations pick up the new configuration gradually. Replace <oai-id> with the ID shown under Origin access, Identities (legacy). Remove this statement once the distribution status is Deployed and traffic is healthy.
Before you start
- A CloudFront distribution whose origin is the bucket's REST endpoint (amzn-s3-demo-bucket.s3.<region>.amazonaws.com), not the S3 website endpoint.
- S3 Object Ownership set to Bucket owner enforced (the default for new buckets), which AWS requires for OAC unless you still need ACLs.
- If objects use SSE-KMS: permission to edit the KMS key policy. If the bucket already has a policy (for example the HTTPS and organization denies), merge rather than replace; both are compatible with OAC.
Deploy it step by step
- Add the bucket policy before switching the origin. AWS recommends granting access first and attaching the OAC second, so the distribution never sends signed requests that the bucket refuses. Merge the statement into the current policy, validate, apply.
Commands (replace <placeholders>) aws s3api get-bucket-policy --bucket amzn-s3-demo-bucket --query Policy --output text > current.json aws accessanalyzer validate-policy --policy-type RESOURCE_POLICY --validate-policy-resource-type AWS::S3::Bucket --policy-document file://bucket-policy.json aws s3api put-bucket-policy --bucket amzn-s3-demo-bucket --policy file://bucket-policy.json - Let CloudFront use the KMS key (SSE-KMS only). S3 decrypts SSE-KMS objects on CloudFront's behalf, so the key policy (not the bucket policy) must allow the CloudFront service principal. Add a statement with Principal {"Service": "cloudfront.amazonaws.com"}, Action kms:Decrypt, Resource "*" (in a key policy that means this key) and the same StringEquals aws:SourceArn condition on the distribution ARN. AWS's documented statement also lists kms:Encrypt and kms:GenerateDataKey*, which matter only if viewers upload through CloudFront. SSE-S3 objects need nothing here.
Commands (replace <placeholders>) aws kms get-key-policy --key-id <key-id> --policy-name default --output text > key-policy.json aws kms put-key-policy --key-id <key-id> --policy-name default --policy file://key-policy.json - Create the OAC and attach it to the origin. Create the OAC with signing behavior always (the console's Sign requests option), then edit the distribution config: set the origin's OriginAccessControlId, clear OriginAccessIdentity if one is set, and rename ETag to IfMatch as the CLI requires. When migrating from OAI, apply the temporary two-statement policy before this step and return to the single-statement policy once the distribution is deployed.
Commands (replace <placeholders>) aws cloudfront create-origin-access-control --generate-cli-skeleton yaml-input > origin-access-control.yaml aws cloudfront create-origin-access-control --cli-input-yaml file://origin-access-control.yaml aws cloudfront get-distribution-config --id EDFDVBD6EXAMPLE --output yaml > dist-config.yaml aws cloudfront update-distribution --id EDFDVBD6EXAMPLE --cli-input-yaml file://dist-config.yaml - Set a default root object. Set DefaultRootObject (for example index.html) in the distribution settings. It only applies to the root URL; CloudFront does not serve index.html for /docs/ the way an S3 website endpoint would, so sites with directory-style URLs need a CloudFront Function or Lambda@Edge to rewrite them.
Verify it without changing anything
These checks only read state, simulate, or use dry runs, so they are safe to run in any account. Exact error wording varies by service and over time.
Confirm the origin configuration
These read-only calls show whether the distribution is actually using OAC against the REST endpoint, and whether it has finished deploying.
aws cloudfront get-distribution-config --id EDFDVBD6EXAMPLE --query 'DistributionConfig.Origins.Items[].[Id,DomainName,OriginAccessControlId,S3OriginConfig.OriginAccessIdentity]' --output table
aws cloudfront get-origin-access-control --id <oac-id> --query 'OriginAccessControl.OriginAccessControlConfig.[SigningBehavior,OriginAccessControlOriginType]'
aws cloudfront get-distribution --id EDFDVBD6EXAMPLE --query Distribution.StatusExpected: The origin's domain ends in .s3.<region>.amazonaws.com (not s3-website), OriginAccessControlId is set, OriginAccessIdentity is empty; signing behavior always and origin type s3; status Deployed.
Compare the CloudFront URL with the direct S3 URL
Request the same existing object both ways, then request a key that doesn't exist. The direct request is anonymous, so its 403 only proves the object isn't public, which is exactly the property you want; the CloudFront request proves the grant works.
curl -sI https://<distribution-domain>.cloudfront.net/index.html
curl -sI https://amzn-s3-demo-bucket.s3.<region>.amazonaws.com/index.html
curl -sI https://<distribution-domain>.cloudfront.net/does-not-exist.htmlExpected: HTTP 200 with an x-cache header (for example Miss from cloudfront) for the first; 403 for the direct S3 URL; 403, not 404, for the missing key, because CloudFront has no s3:ListBucket. A 403 on the first request means the aws:SourceArn or OAC attachment is wrong, or, if only KMS-encrypted objects fail, the key policy.
Confirm S3 doesn't consider the bucket public
A service principal scoped by aws:SourceArn is not public access, so Block Public Access can stay on. These reads confirm both.
aws s3api get-bucket-policy-status --bucket amzn-s3-demo-bucket
aws s3api get-public-access-block --bucket amzn-s3-demo-bucketExpected: PolicyStatus.IsPublic is false, and all four Block Public Access settings are true.
Common mistakes and troubleshooting
Using the S3 website endpoint as the origin
If the origin domain contains s3-website, CloudFront treats it as a custom origin and OAC can't be used. Website endpoints only serve public content, so the bucket would have to be public. Use the REST endpoint and handle index documents in CloudFront.
Missing files return 403, not 404
Without s3:ListBucket, S3 answers a request for a missing key with 403, and CloudFront passes it on. You can map 403 to a custom error page, or grant the CloudFront principal s3:ListBucket on the bucket ARN with the same SourceArn condition to get real 404s. If you grant ListBucket, set a default root object; AWS warns that without one, a request for the root can return a listing of the bucket.
Copying the policy to a second distribution
Each distribution has its own ARN. A staging distribution pointed at the same bucket needs its ARN added to the aws:SourceArn value (StringEquals accepts a list), or it gets 403.
Removing the OAI grant too early
Edge locations switch from OAI to OAC as the update propagates. Dropping the OAI statement before the status is Deployed causes intermittent 403s from some locations.
Limitations and production considerations
- The policy controls how CloudFront reaches the bucket, not who can reach CloudFront. Anything the distribution serves is public unless you add signed URLs or cookies, WAF rules or geographic restrictions.
- The grant covers every object in the bucket. Keep only publishable files in the origin bucket, or scope the Resource to a prefix such as amzn-s3-demo-bucket/site/*.
- OAC doesn't support origin redirects from Lambda@Edge, and S3 Multi-Region Access Point origins need a different OAC setup; check the CloudFront guide before using either.
Official AWS documentation
Account IDs, bucket and organization names are AWS documentation placeholders. Test in a non-production account before use (see the methodology).