
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
AWS S3 static website hosting with CloudFront CDN is the standard architecture for serving high-performance, secure static content globally without managing servers. While S3 provides durable object storage, exposing it directly via the legacy website endpoint creates security risks and latency issues for international users. This guide details the modern production pattern: keeping your bucket private and using CloudFront Origin Access Control (OAC) to serve content securely.
How do you configure AWS S3 static website hosting with CloudFront CDN securely?
The most common mistake I see in audits is teams enabling the "Static Website Hosting" feature on the S3 bucket itself. In 2026, that legacy endpoint should remain disabled for production workloads. Instead, treat S3 purely as a private storage backend and let CloudFront handle all HTTP semantics, TLS termination, and access control. This approach aligns with zero-trust principles and simplifies compliance frameworks like SOC 2 and ISO 27001 because no public ingress exists on the storage layer.
Step 1: Create a private S3 bucket with versioning
Create your bucket with Block Public Access enabled. Enable versioning immediately; this is non-negotiable for rollback capability during failed deployments. If you are managing infrastructure programmatically, refer to my guide on infrastructure as code with Terraform to define these resources declaratively rather than clicking through the console.
resource "aws_s3_bucket" "site" {
bucket = "khimananda-site-assets-prod"
}
resource "aws_s3_bucket_versioning" "site" {
bucket = aws_s3_bucket.site.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "site" {
bucket = aws_s3_bucket.site.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
} Step 2: Configure CloudFront Origin Access Control (OAC)
OAC replaces the older Origin Access Identity (OAI) method. It uses IAM policy conditions tied specifically to the CloudFront distribution ID, preventing unauthorized services from accessing your bucket even if they guess the name. Attach an OAC resource to your distribution and update the S3 bucket policy to allow cloudfront.amazonaws.com as the principal.
resource "aws_cloudfront_origin_access_control" "oac" {
name = "site-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
# Bucket policy allowing only this specific CloudFront distro
data "aws_iam_policy_document" "bucket_policy" {
statement {
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.site.arn}/*"]
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [aws_cloudfront_distribution.site.arn]
}
}
} Step 3: Set cache behaviors and SSL
Configure the default cache behavior to redirect HTTP to HTTPS. Set TTL values appropriate for your content strategy: immutable hashed assets can have a max TTL of one year, while HTML files should use shorter TTLs or rely on cache invalidations. Always assign an ACM certificate in us-east-1 for custom domains; CloudFront requires certificates in this region regardless of where your S3 bucket resides.
Why choose CloudFront over direct S3 website endpoints?
Direct S3 website endpoints lack critical production features. They support only HTTP (no native HTTPS), expose your bucket structure publicly, cannot integrate with WAF, and serve content from a single AWS region. For teams serving users across Nepal and globally, latency from a single-region S3 bucket becomes noticeable. CloudFront caches at edge locations worldwide, reducing time-to-first-byte significantly for distant users.
| Feature | S3 Website Endpoint | CloudFront + Private S3 |
|---|---|---|
| HTTPS Support | No (requires external proxy) | Native ACM integration |
| Access Control | Public read required | Private bucket + OAC |
| Global Latency | Single region origin | 300+ edge locations |
| DDoS Protection | Basic Shield only | WAF + Shield Advanced |
| Custom Headers | Limited metadata | Full header manipulation |
| Cost Model | S3 requests + egress | CloudFront requests + egress (often cheaper) |
Beyond performance, the cost model often favors CloudFront for high-traffic sites. CloudFront’s first 1 TB of data transfer per month is free tier eligible, and egress rates from CloudFront are typically lower than direct S3 egress. When combined with reduced S3 GET requests due to caching, total spend frequently drops compared to direct hosting. For broader optimization strategies, review these cloud cost optimization tactics.
How do you handle SPA routing and cache invalidation?
Single-page applications require special handling because routes like /dashboard don’t correspond to actual S3 objects. Without configuration, CloudFront returns 403/404 errors when users refresh or deep-link. You must implement two complementary patterns: custom error responses and intelligent cache invalidation.
Custom error responses for SPA fallback
Configure CloudFront to intercept 403 and 404 responses from S3 and return /index.html with a 200 status code instead. This allows client-side routers (React, Vue, Angular) to handle path resolution. Set the response page to /index.html, HTTP response code to 200, and cache TTL to 0 or a short duration to avoid stale index files.
Automated cache invalidation in CI/CD
When deploying new versions, invalidate cached paths so users receive updated content immediately. Wildcard invalidations (/*) are convenient but expensive; prefer specific paths or hash-based filenames to minimize invalidation costs. Integrate this into your deployment pipeline — if you use GitLab CI, see this CI/CD pipeline guide for patterns adaptable to static sites.
# Example: Invalidate only changed assets after deploy
aws cloudfront create-invalidation \
--distribution-id E1ABC2DEF3GHIJ \
--paths "/index.html" "/assets/manifest.json" For frameworks generating hashed filenames (Vite, Next.js), invalidating only /index.html and manifest files suffices since all other assets are immutable. This reduces invalidation charges from hundreds of paths to just two or three per deployment.
What monitoring and security controls protect static hosting?
Static doesn’t mean unmonitored. Production deployments need observability for availability, performance, and security events. Enable CloudFront real-time logs delivered to Kinesis Data Streams or S3 for analysis. Standard access logs suffice for basic debugging but lack the granularity needed for incident response or compliance evidence collection.
- WAF Integration: Attach AWS WAF web ACLs to block SQL injection, XSS, and rate-limit abusive IPs. Essential for any public-facing endpoint.
- Geo Restrictions: Limit distribution to specific countries if licensing or compliance requires regional access controls.
- Field-Level Encryption: Encrypt sensitive form data at the edge before forwarding to origins, useful for contact forms on static sites.
- Origin Shield: Consolidate origin requests through a single regional endpoint to reduce S3 GET charges and improve cache hit ratios.
Deploy production-ready AWS S3 static website hosting with CloudFront CDN
Implementing AWS S3 static website hosting with CloudFront CDN correctly requires disciplined configuration: private buckets, OAC authentication, HTTPS enforcement, SPA-aware error handling, and integrated security controls. Skip the legacy website endpoint entirely; the marginal convenience isn’t worth the security debt. Automate every resource definition through Terraform or CDK to ensure reproducibility and audit readiness. If your team needs help architecting compliant static hosting or migrating from legacy configurations, reach out to discuss your infrastructure requirements.