Feature

S3 Static Website Hosting: Use CloudFront and a Private Bucket

Choose an S3 static-site architecture, configure CloudFront with OAC and HTTPS, deploy safely, and test routing, caching and rollback.

Impetuous · · 6 Min Read

“S3 static website” commonly describes two different architectures:

  1. A public bucket served from an S3 website endpoint.
  2. A private S3 bucket used as the origin for CloudFront.

For a production publishing site, use the second as the baseline: CloudFront in front of a private S3 bucket, with origin access control (OAC). This provides HTTPS on the public domain, preserves S3 Block Public Access and prevents readers from bypassing CloudFront. You do not enable S3 website hosting in this design; CloudFront uses the bucket’s regular S3 endpoint.

AWS’s direct S3 hosting tutorial requires public bucket access, and S3 website endpoints support only HTTP. AWS recommends retaining Block Public Access and using CloudFront OAC for a secure static site (S3 documentation).

Choose the architecture first

Requirement Public S3 website endpoint CloudFront with private S3 origin
HTTPS on a custom domain Not directly; requires CloudFront or another proxy Yes
S3 Block Public Access retained No Yes
S3 website redirects and directory indexes Yes Not automatically
Edge caching No Yes
Sensible production default Usually no Yes

Do not combine the designs accidentally. If CloudFront uses an S3 website endpoint, AWS treats it as a custom origin and OAC cannot be used. OAC requires a regular S3 bucket origin (CloudFront OAC documentation). CloudFront can still provide viewer-facing HTTPS in front of a website endpoint, but the endpoint is public and the origin connection is HTTP.

That leaves a deliberate trade-off:

  • Choose the website endpoint when its native redirect and directory-index behavior is essential and a public origin is acceptable.
  • Choose the private origin for the stronger production baseline, then implement required URL rewriting at build time or at the edge.

Production setup

1. Build a complete static release

Generate the site into a release directory such as dist/. It should contain every object needed at runtime: HTML, CSS, JavaScript, images, fonts, feeds, sitemaps and error pages.

Before upload, verify that:

  • index.html exists at the release root.
  • Internal links use the intended production hostname and path format.
  • Asset paths use the correct letter case; S3 object keys are case-sensitive.
  • The deployment process assigns the intended content type to each file.
  • The release contains no credentials, environment files or unpublished material. Source maps should appear only when their public release is intentional.

If this replaces an existing site, preserve its URL behavior rather than treating upload as the whole migration. The static website migration checklist covers redirects, DNS, rollback and post-cutover checks.

2. Create a private S3 bucket

Create a general-purpose bucket in the chosen Region and leave all four Block Public Access settings enabled. Do not add a public-read ACL or a policy with a wildcard principal.

The bucket is the object store, not the public web server. Its name does not need to match the domain when CloudFront is in front. The matching-name constraint applies to the direct Route 53 alias-to-S3-website pattern (Route 53 documentation).

Enable S3 Versioning if retained object history fits the recovery and cost policy. It can make overwritten objects recoverable, but it does not replace a tested release rollback.

3. Put CloudFront in front of the bucket

Create a CloudFront distribution with the regular S3 bucket endpoint as its origin. Create an OAC, select Sign requests (recommended) and authorize the CloudFront service principal for s3:GetObject only when the request comes from that distribution. AWS documents the distribution-scoped AWS:SourceArn condition for this purpose (CloudFront OAC guide).

Set the viewer protocol policy to redirect HTTP to HTTPS. Set index.html as the default root object.

There is a routing trap: the default root object applies to /, not automatically to every subdirectory. A request for /guides/ does not automatically return /guides/index.html from a regular S3 origin (default root object documentation). Choose an explicit policy:

  • Link to files ending in .html.
  • Generate /path/index.html and rewrite /path/ to that key at the edge.
  • Generate extensionless objects whose keys exactly match the public paths.

For a client-side application, define fallback routing separately. Do not apply a blanket 200 index.html response to a content site; that can make missing pages and assets look successful.

4. Attach the domain and certificate

Request or import the ACM viewer certificate in us-east-1. The certificate must cover every alternate domain name attached to the distribution, either exactly or with a valid wildcard (CloudFront certificate requirements).

Add the production hostnames to CloudFront, then point DNS at the distribution. Choose either the apex or www hostname as canonical. Implement the other hostname as a real redirect—for example, with a separate redirect distribution or an edge function—rather than expecting DNS itself to redirect HTTP requests.

5. Deploy assets before documents

Use two cache classes:

  • Content-hashed assets, such as app.4f91c2.js: long-lived public caching with immutable.
  • HTML, feeds, sitemaps and other mutable publishing files: a short lifetime or revalidation policy.

Upload new hashed assets before HTML that refers to them. Do not assign a one-year immutable policy to every non-HTML file; feeds, sitemaps and stable-name images may need to change at the same URL.

For a build that puts all fingerprinted files under dist/assets/, a proposed deployment pattern is:

aws s3 sync dist/assets/ s3://YOUR_BUCKET/assets/ \
  --cache-control "public,max-age=31536000,immutable"

aws s3 sync dist/ s3://YOUR_BUCKET/ \
--exclude "assets/*" \
  --cache-control "public,max-age=0,must-revalidate"

Adapt the filters to the actual build output and run both commands with --dryrun first. The AWS CLI supports filters, --dryrun, --delete, --content-type and --cache-control on s3 sync (CLI reference). It also notes that metadata is applied only to files transferred by a sync; changing the command’s cache policy does not update unchanged objects. Use an explicit metadata-rewrite step when policy changes must affect existing keys.

Keep deletion out of the routine publish step until the new release is verified. If cleanup is required, review a separate --dryrun --delete operation and retain old hashed assets long enough that cached HTML cannot refer to deleted files.

Prefer content-hashed names over repeatedly invalidating stable asset names. AWS recommends versioned file or directory names because CloudFront can fetch the new key without waiting for an old cached object to expire (CloudFront versioning guidance). Invalidate HTML only when its configured cache behavior cannot meet the release requirement.

Test URL semantics and rollback

Before changing DNS, test the CloudFront hostname. After cutover, test the production hostname from a clean client:

curl -I https://example.com/
curl -I https://example.com/an-existing-page/
curl -I https://example.com/a-missing-page
curl -I http://example.com/

Confirm that:

  • HTTP redirects to HTTPS.
  • The non-canonical hostname redirects to the canonical one.
  • Existing pages return the intended status and content type.
  • Missing pages produce the intended public 404 behavior rather than 200.
  • Cache headers differ appropriately between mutable documents and fingerprinted assets.
  • A direct, unsigned S3 object request is denied.
  • Deploying one changed page does not remove unrelated objects.

A private S3 REST origin needs special attention for missing objects. S3 returns 404 for an absent key when the requester has s3:ListBucket, but 403 when it does not (S3 GetObject API). Do not broaden the CloudFront bucket policy merely to obtain a prettier status. If the public site requires a custom 404, configure and test CloudFront error handling carefully so a genuine origin-permission failure is not silently presented as an ordinary missing page.

Finally, retain the previous release manifest and a documented rollback command. S3 makes object storage straightforward; dependable static hosting comes from the surrounding decisions about origin privacy, URL semantics, cache metadata, deployment order and verification.