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.
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
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.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.
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=falseAWS Console — S3 bucket created with default settings
Screenshot placeholder
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.Upload Website Files
Organize your site into a conventional static-site folder structure before uploading:
website/
├── index.html
├── error.html
├── css/
│ └── styles.css
├── js/
│ └── main.js
└── assets/
└── images/Sync the local folder to S3, preserving correct content types:
aws s3 sync ./website s3://www.example.com \
--delete \
--cache-control "max-age=86400"--delete flag removes files from the bucket that no longer exist locally, keeping the bucket in sync with your build output on every deploy.Enable Static Website Hosting
Enable the static website hosting feature on the bucket and configure documents:
aws s3 website s3://www.example.com \
--index-document index.html \
--error-document error.htmlWhat each setting does
- Index document — the file served when a visitor requests a directory path (e.g.
/or/blog/). Almost alwaysindex.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.Configure Bucket Policy
Grant CloudFront's Origin Access Control permission to read objects from the bucket, while keeping the bucket itself fully private:
{
"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-17for current policies. - Sid — a human-readable statement identifier, useful when a policy has multiple statements.
- Effect —
Allowgrants 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.
- Action —
s3:GetObjectonly 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.
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.
aws acm request-certificate \
--domain-name www.example.com \
--subject-alternative-names example.com \
--validation-method DNS \
--region us-east-1DNS 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.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.
aws cloudfront create-distribution \
--origin-domain-name www.example.com.s3.amazonaws.com \
--default-root-object index.htmlRoute53 Configuration
Create a hosted zone for your domain if one doesn't already exist:
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.
{
"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.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.
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.
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'sSourceArn 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 toindex.html for directory-style requests.SSL / Certificate errors
Almost always caused by the ACM certificate being requested outsideus-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. Usedig from multiple locations to confirm.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
# 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, immutableCaching 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 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 sensibleContent-Security-Policyvia 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.
Useful Commands
AWS CLI
# 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 --recursiveCloudFront invalidation
aws cloudfront create-invalidation \
--distribution-id EDFDVBD6EXAMPLE \
--paths "/*"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
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.