DEV Community

Sohana Akbar
Sohana Akbar

Posted on

AWS S3 + CloudFront + OAC: The Correct Way to Host a SPA

So you've built a slick single-page application with React, Vue, or Angular. Now comes the crucial question—how do you host it securely on AWS without making your S3 bucket the wild west of the internet? Let's dive into what I consider the correct way: using S3 with CloudFront and Origin Access Control (OAC).

Why the Old Way Isn't Good Enough
I've seen countless tutorials that recommend enabling static website hosting on an S3 bucket and making it public. This always raises a red flag for me .

The problem? Public S3 buckets are security liabilities. Even if your bucket only contains static files today, exposing it directly to the internet creates risks. AWS themselves recommend blocking all public access . Plus, serving content directly from S3 means your users might experience higher latency, especially if they're far from your bucket's region .

The Three Pillars of Secure SPA Hosting

  1. Keep Your S3 Bucket Private First things first—your S3 bucket should remain completely private with all public access blocked . This might seem counterintuitive for a website, but trust me, it's the foundation of a secure setup.

When creating your bucket, disable all public access and ensure your bucket policy doesn't allow public reads. This forces traffic to flow through CloudFront, which becomes your single entry point .

  1. Use Origin Access Control (OAC) Here's where the magic happens. OAC is the modern way to let CloudFront securely access your private S3 bucket . It's the successor to the older Origin Access Identity (OAI) and provides better security with more granular permissions .

Configuring OAC:

In CloudFront, create a new OAC setting

Select "S3" as the origin type and keep the signing behavior as default

In your distribution, set the origin to use this OAC

Important: When configuring your S3 origin in CloudFront, use the REST API endpoint (e.g., your-bucket.s3.amazonaws.com), not the static website endpoint . The static website endpoint and OAC are incompatible .

Update Your Bucket Policy:

Your S3 bucket policy should look like this to allow CloudFront access:

json
{
"Version": "2012-10-17",
"Statement": {
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
}
}
}
}
Replace the placeholders with your actual bucket name, account ID, and distribution ID .

  1. Handle SPA Routing Properly SPAs use client-side routing, meaning paths like /dashboard or /about are handled by JavaScript, not by the server. If a user refreshes the page or enters a deep link directly, CloudFront will look for a file that doesn't exist and return a 404.

The fix? Configure CloudFront's custom error responses to route 403 and 404 errors back to index.html with a 200 status code :

In your CloudFront distribution, go to the Error Pages tab

Create custom error responses for both 403: Forbidden and 404: Not Found

Set the response page path to /index.html and the response code to 200: OK

This tells CloudFront to serve index.html when a file isn't found, allowing your SPA router to take over and handle the navigation.

Alternative Approach: You can also use a CloudFront Function to rewrite requests:

javascript
function handler(event) {
const request = event.request;
const uri = request.uri;

if (uri.endsWith('/') || !uri.includes('.')) {
    request.uri = '/index.html';
}
return request;
Enter fullscreen mode Exit fullscreen mode

}
This function rewrites requests without file extensions or trailing slashes to index.html .

Optimizing Performance with Caching
Cache Static Assets Aggressively
Your JavaScript and CSS files typically include unique hashes in their filenames (e.g., main-abc123.js), making them immutable. You can cache these aggressively with CloudFront .

Create a separate cache behavior for your static assets. For React, the pattern might be static/, while other frameworks might use assets/:

Go to your CloudFront distribution's Behaviors tab

Create a behavior with a path pattern matching your static assets

Use the managed CachingOptimized policy or create a custom one with long TTLs

You'll see the benefit immediately—CloudFront will serve cached assets with an x-cache: Hit from cloudfront header, dramatically reducing latency for returning visitors .

Don't Cache index.html
Your index.html should have a short cache duration or no caching at all. This ensures users get the latest version of your app without clearing their cache . Consider using max-age=60 with stale-while-revalidate for a balance of performance and freshness .

Custom Domain and SSL
Once your setup is working, you'll want to add a custom domain. You'll need to:

Request an SSL certificate through ACM (AWS Certificate Manager) in the us-east-1 region

Add your domain as an alternate domain name in CloudFront

Create a CNAME record in your DNS provider pointing to your CloudFront distribution domain name

CloudFront will handle SSL termination, providing HTTPS for your custom domain.

The Bottom Line
This architecture—private S3 bucket + CloudFront with OAC—gives you security, performance, and cost-effectiveness. Your S3 bucket stays private, your content gets delivered globally through CloudFront's edge network, and your SPA routes work flawlessly .

Skip the outdated tutorials that make your bucket public. Do it the right way with OAC, and your future self (and your users) will thank you.

Top comments (0)