You have an S3 bucket with static site files and an ACM certificate in us-east-1. Your job is to put CloudFront in front of everything: create a distribution, lock down the bucket with Origin Access Control, configure SPA routing, and attach your certificate. By the end, you should have a globally distributed, HTTPS-secured frontend that serves your SPA correctly on all routes.
Why It Matters
Without CloudFront, your site is a single-region S3 bucket with no HTTPS, no edge caching, and no security headers. With CloudFront, you have a globally distributed CDN that serves content from edge locations close to your users, enforces HTTPS, handles SPA routing, and adds security headers—the same infrastructure Vercel and Netlify give you out of the box. This exercise is the bridge from “files in a bucket” to “production deployment.”
If the console or CLI output shifts while you’re doing this, keep the CloudFront Developer Guide and the OAC setup guide open.
Prerequisites
Before you start, make sure you have:
- An S3 bucket (
my-frontend-app-assets) with at least anindex.htmlfile uploaded. See Uploading and Organizing Files if you need to set this up. - An ACM certificate in
us-east-1with statusISSUED. See Requesting a Certificate in ACM if you need one. - A domain you control, with DNS already lined up in Route 53 or ready for the final alias-record step.
- The AWS CLI v2 configured with credentials that have CloudFront, S3, and ACM permissions.
Create an Origin Access Control
Create an OAC that CloudFront will use to authenticate requests to your S3 bucket.
- Use
aws cloudfront create-origin-access-controlwith a JSON config. - Set
SigningProtocoltosigv4,SigningBehaviortoalways, andOriginAccessControlOriginTypetos3. - Save the OAC
Idfrom the response—you need it for the distribution config.
Checkpoint
Run aws cloudfront list-origin-access-controls --region us-east-1 --output json and confirm your OAC appears in the list.
Create the Distribution
Create a CloudFront distribution with these settings:
- Origin: Your S3 bucket (
my-frontend-app-assets.s3.us-east-1.amazonaws.com), with the OAC ID attached. - Default root object:
index.html - Price class:
PriceClass_100 - Cache policy: Use the managed
CachingOptimizedpolicy (658327ea-f89d-4fab-a63d-7e88639e58f6). - Viewer protocol policy:
redirect-to-https - Compression: Enabled
- HTTP version:
http2and3 - Viewer certificate: Use your ACM certificate ARN,
sni-only,TLSv1.2_2021. - Aliases: Your custom domain (e.g.,
example.comandwww.example.com). - Custom error responses: Map both
403and404to/index.htmlwith response code200and an error caching TTL of10seconds.
Write the full distribution config JSON and use aws cloudfront create-distribution --distribution-config file://distribution-config.json.
Checkpoint
The create-distribution command returns a distribution Id and DomainName. Save both. The Status should be "InProgress". Wait for deployment:
aws cloudfront wait distribution-deployed \
--id YOUR_DISTRIBUTION_ID \
--region us-east-1Update the S3 Bucket Policy
Replace your bucket’s current policy with one that allows only CloudFront to read from it:
- Principal:
cloudfront.amazonaws.comservice principal. - Action:
s3:GetObject. - Resource:
arn:aws:s3:::my-frontend-app-assets/*. - Condition:
StringEqualsonAWS:SourceArnmatching your distribution’s ARN (arn:aws:cloudfront::123456789012:distribution/YOUR_DISTRIBUTION_ID).
Apply the policy with aws s3api put-bucket-policy.
Checkpoint
Test that CloudFront serves your content:
curl -I https://YOUR_DISTRIBUTION_DOMAIN/index.htmlYou should get 200 OK. Now test that direct S3 access is blocked:
curl -I https://my-frontend-app-assets.s3.us-east-1.amazonaws.com/index.htmlYou should get 403 Forbidden.
Re-enable Block Public Access
Your bucket no longer needs to be publicly accessible. Re-enable all four Block Public Access settings:
aws s3api put-public-access-block \
--bucket my-frontend-app-assets \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" \
--region us-east-1Checkpoint
Verify that CloudFront still works after re-enabling Block Public Access:
curl -I https://YOUR_DISTRIBUTION_DOMAIN/index.htmlStill 200 OK. The CloudFront service principal policy isn’t affected by Block Public Access.
Test SPA Routing
Navigate to a path that doesn’t correspond to a real file in your S3 bucket:
curl -I https://YOUR_DISTRIBUTION_DOMAIN/dashboard/settingsYou should get 200 OK with content-type: text/html. This confirms that the custom error response is working—CloudFront is serving index.html for missing paths.
Checkpoint
Open your distribution’s domain in a browser and navigate to a SPA route. The page should load correctly, and refreshing shouldn’t produce a 403 or 404 error.
Attach a Response Headers Policy
Attach the managed SecurityHeadersPolicy (67f7725c-6f97-4210-82d7-5512b31e9d03) to your default cache behavior:
- Fetch the current distribution config with
aws cloudfront get-distribution-config. - Add
ResponseHeadersPolicyIdto theDefaultCacheBehavior. - Update the distribution with
aws cloudfront update-distribution, using theETagfrom the fetch.
Checkpoint
After the distribution deploys, verify the security headers:
curl -I https://YOUR_DISTRIBUTION_DOMAIN/index.htmlYou should see:
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
x-frame-options: SAMEORIGINFinal Verification
At this point, your CloudFront distribution should have:
- An S3 origin with Origin Access Control (no public bucket access).
- The
CachingOptimizedmanaged cache policy. - HTTPS via your ACM certificate with
redirect-to-https. - Custom error responses for SPA routing (403 and 404 to
/index.htmlwith 200). - Security headers via a response headers policy.
Run a final check:
# Distribution details
aws cloudfront get-distribution \
--id YOUR_DISTRIBUTION_ID \
--region us-east-1 \
--output json \
--query "Distribution.{Id:Id,Domain:DomainName,Status:Status}"
# Verify HTTPS works
curl -I https://YOUR_DISTRIBUTION_DOMAIN
# Verify SPA routing
curl -I https://YOUR_DISTRIBUTION_DOMAIN/any/spa/route
# Verify S3 is locked down
curl -I https://my-frontend-app-assets.s3.us-east-1.amazonaws.com/index.htmlFailure Diagnosis
- CloudFront returns
403 Forbiddenfor files that exist: The S3 bucket policy does not trust your distribution’s Origin Access Control, or the origin is still configured to use the bucket incorrectly. /dashboard/settingsreturns a 403 or 404 instead of your app shell: The custom error responses for SPA routing are missing or point at the wrong path.- Direct S3 access still works after CloudFront is configured: Re-enable Block Public Access and make sure the bucket policy grants read access only to CloudFront, not to
Principal: "*"anymore.
Stretch Goals
- Custom response headers policy: Create a custom policy with HSTS
max-ageof 2 years,includeSubDomains, andpreload. Add aPermissions-Policyheader that disables camera, microphone, and geolocation. - Cache-Control headers on S3 objects: Re-upload your assets with differentiated
Cache-Controlheaders: short TTL forindex.html, long TTL withimmutablefor hashed assets. - Targeted invalidation: Deploy a change to
index.html, then create an invalidation for only/index.htmlinstead of/*. Verify that the old version is still cached for other paths.