Modern platforms demand architectures that are not only fast and scalable but also impossible to attack at the storage layer. A static website might seem simple, but hosting it securely on AWS—without exposing S3 to the public internet—requires careful design.
As part of my DevOps journey, I was given a straightforward but strict objective:
Host a static site that loads globally fast, while keeping the S3 bucket 100% private. No public access. No website hosting mode. No shortcuts.
This blog covers the Proof of Concept (PoC) I executed using Amazon S3, CloudFront, and the modern Origin Access Control (OAC), the secure successor to the legacy Origin Access Identity (OAI).
Table of Contents
Goal: A Secure, Serverless, Zero-Exposure Architecture
The architecture needed to satisfy three non-negotiable conditions:
- The S3 bucket must block all public access
- CloudFront must serve the content securely over HTTPS
- Only CloudFront should be able to read from S3, enforced by cryptographic OAC signing
In short: Global speed on the front, private storage at the back.
Designing secure, zero-exposure architectures like this is often easier and faster when working with a certified AWS Partner who understands compliance, scalability, and modern cloud best practices.
Architecture Overview
Below is the exact flow the PoC implements:
| Component | Purpose | Security Mechanism |
|---|---|---|
| S3 Bucket | Stores static files | Block Public Access + Restrictive Bucket Policy |
| CloudFront CDN | Serves users globally | HTTPS enforced |
| Origin Access Control (OAC) | Authenticates CloudFront → S3 | Cryptographic signing + SourceArn restriction |
The key upgrade here is OAC, which AWS now recommends over the older OAI method because it works across all S3 regions and supports full SSE-KMS compatibility.
Phase 1 – Private Storage Setup (S3)
Challenge
Static website hosting mode requires public read access—even if you don’t want it.
Solution
I created an S3 bucket (example: s3-cdn-ninja-bucket) with:
- Block All Public Access → ON (mandatory)
- SSE-S3 encryption
- Uploaded a basic
index.html
Even with a misconfigured policy, “Block Public Access” guarantees that the bucket stays private.
Why This Matters
This ensures the storage layer is fully isolated from the public internet, satisfying zero-exposure compliance requirements.
Phase 2 – CDN Setup Using CloudFront (New Wizard Flow)
Challenge
How do you let CloudFront access a private S3 bucket without exposing credentials or making the bucket public?
Solution – Using OAC (Origin Access Control)
AWS recently updated the CloudFront setup wizard with a new “Recommended” option that:
- Creates the OAC automatically
- Links it to the S3 origin
- Simplifies configuring the bucket policy
My distribution configuration included:
| Setting | Value |
|---|---|
| Origin Domain | Private S3 bucket |
| Origin Access | OAC (Recommended) |
| Viewer Protocol | Redirect HTTP → HTTPS |
| Cache Policy | CachingOptimized |
| Default Root Object | index.html |

The Gotcha I Encountered
At first, https://xyz.cloudfront.net returned 403 Access Denied.
The root cause? CloudFront didn’t know which file to serve.
Setting Default Root Object = index.html fixed it instantly.
Why OAC Works Better Than OAI
- No regional limitations
- Works seamlessly with SSE-KMS
- Uses the CloudFront service principal + SourceArn condition
- More secure and future-proof

Phase 3 – The Security Handshake (Bucket Policy)
Even with OAC, CloudFront still needs explicit permission to fetch objects.
Here is the restrictive policy I applied:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-private-site-intern-task-2024/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
}
}
}
]
}
Why This Works
CloudFront sends a signed request → S3 verifies the signature → grants access only if the request came from the exact distribution we configured.
No public access. No backdoors.

Phase 4 – Verification (Trust, but Verify)
I validated the setup using two tests:
Test 1 – Direct S3 Access (Expected Failure)
URL:
https://[bucket].s3.amazonaws.com/index.html
Result: 403 Forbidden
- Confirms the bucket is private.
Test 2 – CloudFront Distribution (Expected Success)
URL:
https://[distribution].cloudfront.net
Result: 200 OK
- Page loads successfully
- Confirms OAC + bucket policy is correctly implemented

Final Outcome
With this PoC, I achieved a fully secure, global static hosting setup that meets modern AWS compliance expectations:
| Objective | Result |
|---|---|
| S3 Block Public Access | ✔ Enforced |
| CloudFront HTTPS Delivery | ✔ Enabled |
| OAC + Bucket Policy | ✔ Locked-down |
| Zero S3 Exposure | ✔ Validated |
Related Searches