Secure Static Website Hosting on AWS: CloudFront + Private S3 Bucket with Origin Access Control

Published: (January 17, 2026 at 01:59 AM EST)
2 min read
Source: Dev.to

Source: Dev.to

End State

A static website hosted on Amazon S3, served globally via Amazon CloudFront with private access.

Browser / curl

CloudFront Distribution (HTTPS)

Origin Access Control (OAC)

Private S3 bucket

index.html

Final working URL (will not work until the tutorial is completed):

https://d11v1nvbkt9lgm.cloudfront.net/

Result after everything is set up:

  • HTTP 200
  • Content served from S3
  • Cached by CloudFront
  • Bucket not public

Step‑by‑Step What I Did

1️⃣ Create an S3 bucket

  • Bucket name: devops-bucket-alok
  • Uploaded index.html
  • Enabled Static Website Hosting

Note: S3 static website hosting S3 REST API endpoint.

2️⃣ Create a CloudFront distribution

  • Origin: the S3 bucket created above
  • Default behavior: *
  • Viewer policy: Redirect HTTP → HTTPS
  • Allowed methods: GET, HEAD

CloudFront domain created: d11v1nvbkt9lgm.cloudfront.net

❌ Problem 1 – 403 AccessDenied from CloudFront

Symptoms

  • Browser displayed an XML error page.

  • curl output:

    HTTP/2 403
    server: AmazonS3
    x-cache: Error from cloudfront
  • CloudFront could reach S3, but S3 refused the request because the bucket had no permission for CloudFront.

Root cause

  • CloudFront Origin Access Control (OAC) does not work with S3 website endpoints (those require public access).
  • OAC works only with the S3 REST endpoint.

Correct decision – use the bucket’s REST endpoint.

Created an Origin Access Control named devops-day0-oac and attached it to the origin, but still received 403.

OAC is not magic; CloudFront signs requests, but S3 still needs a bucket policy that allows those signed requests.

❌ Problem 4 – “Where do I check if it’s attached?”

The most frustrating part was that CloudFront tells you:

“You must allow access using this policy statement”

AWS does not auto‑apply the required bucket policy.

3️⃣ Add S3 Bucket Policy (Key Step)

  1. In the S3 console: devops-bucket-alok → Permissions → Bucket policy.

  2. Paste the CloudFront‑generated policy that:

    • Allows cloudfront.amazonaws.com
    • Restricts access to your distribution ARN
    • Grants s3:GetObject

After applying the policy, the request flow succeeded:

  • CloudFront → S3
  • Browser received HTTP/2 200, x-cache: Miss from cloudfront
## Hello from S3 + CloudFront!

This confirmed that CloudFront is front‑ending the request, S3 is the origin, access control is correct, and HTTPS works.

What I Learned

Conceptual

  • Difference between object storage and server hosting.
  • CDN vs. origin responsibilities.
  • Why CloudFront exists.
  • Why S3 buckets should not be public.
  • A 403 error is not a networking issue.

AWS‑specific

  • S3 website endpoint vs. REST endpoint.
  • OAC (modern) vs. OAI (legacy).
  • A bucket policy is mandatory when using OAC.
  • Many CloudFront errors actually originate from S3 permissions.

DevOps reality

  • The AWS UI hides critical steps (e.g., attaching the bucket policy).
  • Error messages are technically correct but often unhelpful.
  • Troubleshooting is mostly about tracing permissions.
  • Most time is lost due to a single missing policy statement.
Back to Blog

Related posts

Read more »