Skip to content

Static Website Hosting using AWS & Cloudflare

Deploy a secure, high-performance static website using AWS and Cloudflare with HTTPS, CDN and DNS.

Route53Certificate ManagerCloudFrontS3Cloudflare
7 min readBeginner
Section 01

Introduction

Static website hosting serves pre-built HTML, CSS, JavaScript, and media files directly to visitors with no server-side processing. Because there is no application server to patch, scale, or keep online, static hosting is the fastest, cheapest, and most resilient way to publish a portfolio, marketing site, or documentation site.

This guide deploys a static site on Amazon S3 as the origin storage layer, distributes it globally through Amazon CloudFront, secures it with a free TLS certificate from AWS Certificate Manager (ACM), routes traffic with Route 53, and places Cloudflare in front for an additional layer of caching, DDoS protection, and a Web Application Firewall.

Why static hosting

  • No servers to patch, scale, or monitor — S3 handles availability and durability.
  • Extremely low cost — pay only for storage and data transfer, no idle compute cost.
  • Near-instant global delivery via CloudFront's edge network.
  • Naturally resistant to most attack vectors since there is no server-side runtime.

Benefits of this architecture

  • S3 provides 99.999999999% (11 nines) durability for your website files.
  • CloudFront caches content at edge locations close to visitors, cutting latency dramatically.
  • ACM issues and auto-renews a free TLS certificate — no manual certificate management.
  • Cloudflare adds a WAF, additional caching, and hides your origin behind its network.

Architecture overview

A visitor's request first hits Cloudflare, which resolves DNS and proxies the request. Route 53 (or Cloudflare, depending on which layer owns DNS) resolves the CloudFront distribution's domain, CloudFront serves from its edge cache or fetches from the S3 origin on a cache miss, and the response flows back through the same chain.

Section 02

Architecture Diagram

The request flow below shows how traffic moves from the visitor's browser through Cloudflare, DNS resolution, CloudFront's edge cache, and finally to the S3 origin bucket.

User → Cloudflare → Route 53 → CloudFront → S3

Section 03

Prerequisites

Before starting, make sure you have the following in place:

  • An active AWS account with billing enabled and IAM access.
  • A registered domain name (purchased through Route 53, Cloudflare, or any registrar).
  • A free or paid Cloudflare account with the domain added.
  • Basic IAM knowledge — creating a user/role with scoped S3 and CloudFront permissions.

IAM least privilege

Avoid using your AWS root account for daily operations. Create an IAM user with only the permissions needed for S3, CloudFront, ACM, and Route 53, and enable MFA.
Section 04

Create S3 Bucket

Create a bucket whose name exactly matches your domain (e.g. www.example.com) — this convention makes it easy to trace which bucket serves which site.

terminal
aws s3api create-bucket \
  --bucket www.example.com \
  --region us-east-1

# Block all public access at the account level is fine —
# CloudFront will access the bucket via Origin Access Control (OAC),
# not public bucket policy, so public access stays blocked.
aws s3api put-public-access-block \
  --bucket www.example.com \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=false,RestrictPublicBuckets=false

Bucket naming is global

S3 bucket names are globally unique across all AWS accounts. If the exact domain name is taken, prefix or suffix it (e.g. example-com-site) and reference that name consistently in later steps.
Section 05

Upload Website Files

Organize your site into a conventional static-site folder structure before uploading:

folder structure
website/
├── index.html
├── error.html
├── css/
│   └── styles.css
├── js/
│   └── main.js
└── assets/
    └── images/

Sync the local folder to S3, preserving correct content types:

terminal
aws s3 sync ./website s3://www.example.com \
  --delete \
  --cache-control "max-age=86400"
The --delete flag removes files from the bucket that no longer exist locally, keeping the bucket in sync with your build output on every deploy.
Section 06

Enable Static Website Hosting

Enable the static website hosting feature on the bucket and configure documents:

terminal
aws s3 website s3://www.example.com \
  --index-document index.html \
  --error-document error.html

What each setting does

  • Index document — the file served when a visitor requests a directory path (e.g. / or /blog/). Almost always index.html.
  • Error document— served when a requested object doesn't exist (HTTP 404 from S3's perspective). Use a friendly branded 404 page here.

This is optional with CloudFront + OAC

When CloudFront uses Origin Access Control against the S3 REST API endpoint (recommended, used in this guide), the bucket's own static-website-hosting endpoint isn't actually used for serving — CloudFront handles index/error document behavior instead. It's still useful to enable for direct-to-S3 testing before CloudFront is wired up.
Section 07

Configure Bucket Policy

Grant CloudFront's Origin Access Control permission to read objects from the bucket, while keeping the bucket itself fully private:

bucket-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipalReadOnly",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::www.example.com/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
        }
      }
    }
  ]
}

Line-by-line explanation

  • Version — the IAM policy language version; always 2012-10-17 for current policies.
  • Sid — a human-readable statement identifier, useful when a policy has multiple statements.
  • EffectAllow grants the action; the absence of any other statement means everything else stays denied by default.
  • Principal — restricts this permission to the CloudFront service itself, not any AWS account or the public.
  • Actions3:GetObject only allows reading objects, never writing or deleting.
  • Resource — scopes the permission to objects inside this bucket only.
  • Condition— ties the permission to one specific CloudFront distribution ARN, preventing any other AWS customer's CloudFront distribution from reading your bucket.
Section 08

Create ACM Certificate

Request a public certificate in us-east-1 — CloudFront only accepts ACM certificates issued in that region, regardless of where your other resources live.

terminal
aws acm request-certificate \
  --domain-name www.example.com \
  --subject-alternative-names example.com \
  --validation-method DNS \
  --region us-east-1

DNS validation

ACM returns a CNAME record you must publish in DNS to prove domain ownership. Add it in Route 53 (or Cloudflare, if it owns your authoritative DNS) exactly as provided — validation typically completes within a few minutes.

  • Request the certificate in us-east-1, never another region, for CloudFront use.
  • Include both the apex domain and the www subdomain as alternative names.
  • Publish the CNAME validation record exactly as returned by ACM.
  • Wait for the certificate status to change from Pending validation to Issued.

Best practice

ACM certificates auto-renew for free as long as the validation DNS record stays in place — never delete it after issuance.
Section 09

Configure CloudFront

Create a distribution with the S3 bucket as its origin:

Origin

Set the origin to the bucket's REST API endpoint (www.example.com.s3.amazonaws.com), not the static-website-hosting endpoint, and attach an Origin Access Control so only CloudFront can read from it.

Behavior

  • Viewer protocol policy: Redirect HTTP to HTTPS.
  • Allowed methods: GET, HEAD (static content only).
  • Cache policy: CachingOptimized managed policy.

Compression

Enable automatic compression — CloudFront serves Brotli or Gzip based on the visitor'sAccept-Encoding header, shrinking HTML/CSS/JS transfer size significantly.

Caching

Use the managed CachingOptimized policy for a strong default, and override with a shorter TTL for HTML if you deploy frequently.

HTTPS

Attach the ACM certificate created above under "Custom SSL certificate", and set the minimum TLS version to TLSv1.2_2021 or newer.

terminal
aws cloudfront create-distribution \
  --origin-domain-name www.example.com.s3.amazonaws.com \
  --default-root-object index.html
Section 10

Route53 Configuration

Create a hosted zone for your domain if one doesn't already exist:

terminal
aws route53 create-hosted-zone \
  --name example.com \
  --caller-reference $(date +%s)

A Record (Alias)

Instead of a plain A record with a static IP (CloudFront has none), create an alias recordpointing at your CloudFront distribution's domain name. Alias records resolve at the DNS layer with no extra latency and are free of charge for AWS-to-AWS resolution.

route53-change-batch.json
{
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "www.example.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d111111abcdef8.cloudfront.net",
          "EvaluateTargetHealth": false
        }
      }
    }
  ]
}
Z2FDTNDATAQYW2is CloudFront's fixed, global hosted zone ID — it is the same value for every CloudFront distribution and every AWS account.
Section 11

Cloudflare Configuration

Nameservers

Point your domain registrar's nameservers to the two nameservers Cloudflare assigns after adding the site, so Cloudflare becomes authoritative for DNS.

SSL Mode — Full (Strict)

Set the SSL/TLS encryption mode to Full (strict). This requires CloudFront to present a valid, trusted certificate (which ACM provides) and ensures the connection between Cloudflare and CloudFront is encrypted end-to-end, not just browser-to-Cloudflare.

Caching

Set the caching level to Standard and enable Tiered Cacheto reduce redundant requests reaching CloudFront from Cloudflare's many edge locations.

Security

  • Enable Always Use HTTPS to redirect any stray HTTP request.
  • Turn on the Web Application Firewall (WAF) managed ruleset.
  • Enable Bot Fight Mode on the free plan for basic bot mitigation.
Section 12

Verify Deployment

Run through this checklist before considering the deployment complete:

  • https://www.example.com loads the site with a valid padlock/certificate.
  • http:// requests redirect to https:// automatically.
  • The apex domain (example.com) redirects or resolves correctly to www.
  • A deliberately broken URL shows the custom error page, not a raw AWS error.
  • CloudFront cache headers (x-cache: Hit from cloudfront) appear on repeat requests.
  • Cloudflare's own headers (cf-ray) are present, confirming traffic flows through it.
Troubleshooting

Common Issues

403 Forbidden

Usually means the bucket policy or Origin Access Control isn't correctly scoped to the CloudFront distribution's ARN, or the object genuinely doesn't exist with that exact key/case. Re-check the bucket policy's SourceArn condition.

404 Not Found

Confirm the requested key exists in S3 with the exact same casing, and that CloudFront's default root object is set to index.html for directory-style requests.

SSL / Certificate errors

Almost always caused by the ACM certificate being requested outside us-east-1, or Cloudflare's SSL mode being set to Flexible instead of Full (strict) while the origin also enforces HTTPS-only.

Stale CloudFront cache

Changes not appearing after a deploy usually mean the old objects are still cached at the edge. Create a cache invalidation (see Useful Commands) or lower the TTL on your cache policy for actively-changing files.

DNS propagation delay

DNS changes can take anywhere from a few minutes up to 48 hours to fully propagate globally, though Cloudflare and Route 53 both typically resolve within minutes. Use dig from multiple locations to confirm.
Optimization

Performance Optimization

Compression

Ensure CloudFront's automatic compression is enabled so text-based assets ship as Brotli/Gzip.

Image optimization

Serve modern formats (WebP/AVIF) and appropriately sized images. Consider a build-time image pipeline rather than shipping unoptimized originals.

HTTP headers

recommended cache-control
# HTML — revalidate often
Cache-Control: max-age=60, must-revalidate

# Hashed/versioned assets (css/js with content hash in filename)
Cache-Control: public, max-age=31536000, immutable

Caching strategy

Use long-lived, immutable caching for fingerprinted static assets, and short TTLs for HTML so deploys become visible quickly without needing an invalidation every time.

Security

Security Hardening

  • Cloudflare WAF — enable the managed ruleset to block common exploit patterns (SQLi, XSS payloads) before they ever reach AWS.
  • Security headers — add Strict-Transport-Security, X-Content-Type-Options: nosniff, and a sensible Content-Security-Policy via a CloudFront response headers policy.
  • HTTPS everywhere— enforce HTTPS at both Cloudflare and CloudFront, and keep the S3 bucket fully private, reachable only via CloudFront's OAC.
Reference

Useful Commands

AWS CLI

terminal
# Sync a fresh build to S3
aws s3 sync ./dist s3://www.example.com --delete

# List bucket contents
aws s3 ls s3://www.example.com --recursive

CloudFront invalidation

terminal
aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/*"
Best Practices

Best Practices

  • Keep the S3 bucket fully private — never enable public bucket access.
  • Automate deploys with a CI pipeline running aws s3 sync + invalidation.
  • Version fingerprinted assets so long cache TTLs never serve stale files.
  • Monitor CloudFront metrics (4xx/5xx rate, cache hit ratio) in CloudWatch.
  • Rotate IAM access keys regularly and prefer OIDC-based CI credentials over long-lived keys.
Summary

Summary

This deployment gives you a static site backed by S3's durability, distributed globally through CloudFront, secured end-to-end with a free ACM certificate, and shielded by Cloudflare's caching and WAF. The result is a fast, low-cost, low-maintenance hosting setup with no servers to manage and near-zero ongoing operational overhead.