An S3 Static Site Behind CloudFront
Introduction¶
The modern pattern is: a private S3 bucket (no public access, no website hosting), fronted by CloudFront which authenticates to the bucket with Origin Access Control (OAC). This gets you HTTPS, a CDN, HTTP/2+3, and no publicly listable bucket.
The pieces¶
1. Private bucket¶
aws s3 mb s3://example-com-site
aws s3api put-public-access-block --bucket example-com-site \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Do not enable S3 static website hosting — that requires public access. Upload your build:
aws s3 sync ./dist s3://example-com-site --delete \
--cache-control "public,max-age=31536000,immutable" \
--exclude index.html
aws s3 cp ./dist/index.html s3://example-com-site/index.html \
--cache-control "no-cache"
Hashed assets get a long cache; index.html gets no-cache so deploys are
picked up.
2. ACM certificate — in us-east-1¶
CloudFront only uses certificates from us-east-1, regardless of where your bucket is:
aws acm request-certificate --region us-east-1 \
--domain-name example.com --subject-alternative-names www.example.com \
--validation-method DNS
Add the CNAME validation records to Route 53 (or your DNS).
3. CloudFront distribution¶
- Origin: the bucket's regional domain
(
example-com-site.s3.eu-west-1.amazonaws.com), not the website endpoint. - Origin access: create an OAC (SigV4, S3). CloudFront then signs requests to the bucket.
- Default root object:
index.html. - Viewer protocol policy: redirect HTTP to HTTPS.
- Alternate domain names (CNAMEs):
example.com,www.example.com; attach the ACM cert. - Compress objects automatically: on.
4. Bucket policy for OAC¶
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-com-site/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E1XXXXXXXXXXXX"
}
}
}]
}
5. SPA routing¶
A single-page app needs /any/deep/link to serve index.html. Add a CloudFront
custom error response: HTTP 403 (and 404) → response page path
/index.html, response code 200. (403 because a private bucket returns 403,
not 404, for a missing key.)
Better, if you use CloudFront Functions: rewrite requests without a file
extension to /index.html at the viewer-request stage — keeps real 404s as
404s.
6. DNS¶
Route 53 alias A/AAAA records for example.com and www → the CloudFront
distribution.
Deploying an update¶
aws s3 sync ./dist s3://example-com-site --delete ...
aws cloudfront create-invalidation --distribution-id E1XXXX --paths "/index.html" "/"
Only invalidate what changed (usually just index.html if assets are hashed) —
invalidations beyond the free 1,000/month cost money.
Verification and troubleshooting¶
curl -I https://example.com
curl -I https://example.com/some/spa/route # expect 200 + index.html
dig +short example.com
- 403 AccessDenied from CloudFront — bucket policy doesn't grant the OAC
(wrong distribution ARN in the condition), or the object key doesn't exist
(typo, or
index.htmlnot uploaded), or public access block plus a legacy OAI config. Check the exact key withaws s3 ls. - 403 on every deep link, 200 on
/— SPA custom error response not configured (403→/index.html→200). - Certificate won't attach / "not in us-east-1" — request it again in us-east-1.
- Old content after deploy — no invalidation, or the browser cached
index.html(setno-cacheon it), or a long default TTL. Invalidate/and/index.html. SignatureDoesNotMatch/ origin errors — using the S3 website endpoint as the origin with OAC (OAC needs the REST endpoint), or the bucket is in a different region than the origin domain says.- www doesn't work — missing SAN on the cert, missing CNAME on the distribution, or no DNS record.
- HTTP/3 not showing — enable it on the distribution; it's off by default on older ones.
Related tools and reading¶
- On-site: SSL Certificate Checker, DNS lookup, HTTP Header Analyzer.
- Related posts: Route 53 routing policies, VPC subnets and route tables.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Cloud & AWS
AWS WAF: The Rules That Matter
AWS WAF filters HTTP requests at the ALB/CloudFront/API Gateway edge. Here's a sensible baseline Web ACL, how to tune with count mode, and the false-positive traps.
October 24, 2026CloudWatch Logs and Metric Filters
CloudWatch Logs is cheap to write and expensive to forget about. Here's retention, turning error patterns into metrics and alarms, and Logs Insights for ad-hoc queries.
October 24, 2026ALB vs NLB (and When to Use Each)
ALB is an L7 HTTP router with rich features; NLB is an L4 pass-through with static IPs and raw throughput. Here's the decision, feature by feature.
October 24, 2026