Hosting a static site on AWS is one of the best-value setups there is: pennies of storage, a global CDN, and free HTTPS. But the naive "make the bucket public" version is both insecure and slow. Here is the way that is actually recommended.
Step 1: Private Bucket, No Public Access
Do not enable S3 static website hosting or make the bucket public. Keep it fully private and let CloudFront be the only thing that reads it:
S3 bucket → Block all public access: ON
Upload your built site (index.html, assets, ...) to the bucket
Step 2: Put CloudFront in Front
Create a CloudFront distribution with the bucket as its origin, using Origin Access Control so only CloudFront can read the bucket:
CloudFront → Create distribution
Origin: your-bucket.s3.region.amazonaws.com
Origin access: Origin access control (OAC)
Viewer protocol policy: Redirect HTTP to HTTPS
CloudFront then generates a bucket policy that grants itself read access — copy it onto the bucket. Users hit the CDN edge; the bucket stays private.
Step 3: Handle SPA Routing
A single-page app serves every route from index.html. Without help, a refresh on /dashboard returns a 403. Add a CloudFront custom error response to rewrite those to the app entry point:
Custom error response:
HTTP error code: 403 → Response page path: /index.html → Response code: 200
Step 4: Set Cache Headers and Invalidate on Deploy
Cache hashed assets aggressively and index.html briefly, then invalidate on each deploy so users get the new version:
aws s3 sync ./dist s3://your-bucket --delete aws cloudfront create-invalidation --distribution-id ABC123 --paths "/*"
Summary
Keep the S3 bucket private, front it with CloudFront using Origin Access Control, add HTTPS redirect and a 403→index.html rule for SPA routing, and invalidate the cache on deploy. The result is a globally fast, secure, near-free static site — without ever exposing the bucket to the public internet.