Secure, Serverless And Private: Hosting Static Sites with AWS S3 And CloudFront OAC

AWS S3

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).

Goal: A Secure, Serverless, Zero-Exposure Architecture

The architecture needed to satisfy three non-negotiable conditions:

  1. The S3 bucket must block all public access
  2. CloudFront must serve the content securely over HTTPS
  3. 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