How to Make CloudFront Serve Your S3 Website with a Custom Domain and HTTPS
You have a static site sitting in S3, a CloudFront distribution in front of it, and a domain registered in Route 53 — but the moment you try to attach a custom domain with HTTPS, the process branches into ACM certificate validation, CloudFront alternate domain names, and Route 53 alias records, all of which have to be wired together in the right order or the whole thing silently fails.
TL;DR: Custom Domain + HTTPS on CloudFront
| Step | What You Do | Why It Matters |
|---|---|---|
| 1 | Request ACM certificate in us-east-1 | CloudFront only reads ACM certs from us-east-1, regardless of your origin region |
| 2 | Add CNAME validation record to Route 53 | ACM must verify domain ownership before issuing the cert |
| 3 | Attach the issued cert to CloudFront and add the alternate domain name (CNAME) | CloudFront will reject HTTPS requests for domains not listed as CNAMEs |
| 4 | Point Route 53 alias record to the CloudFront distribution | Alias records resolve the CloudFront domain at the DNS layer without extra latency |
How CloudFront Custom Domains and ACM Work Together
CloudFront is a global service, but it has one hard constraint on TLS certificates: it only accepts certificates issued by ACM in the us-east-1 region. This catches engineers off guard when their S3 bucket and other infrastructure live in a different region. The certificate region is non-negotiable — request it anywhere else and CloudFront will not list it as an option.
When a browser connects to your custom domain, Route 53 resolves it to the CloudFront distribution via an alias record. CloudFront then presents the ACM certificate for TLS negotiation. For this to work, the domain on the certificate must match the alternate domain name configured on the distribution. If either side is missing, CloudFront returns an SSL error or a 'Your connection is not private' warning before the request ever reaches your S3 origin.
- Browser resolves
www.example.comvia Route 53 alias → gets the CloudFront distribution domain. - CloudFront presents the ACM certificate (issued in us-east-1) during TLS handshake.
- After TLS, CloudFront forwards the request to the S3 origin (either REST endpoint or OAC-protected bucket).
- ACM validates domain ownership via a CNAME record you add to Route 53 before the certificate is issued.
Step 1: Request an ACM Certificate in us-east-1
This step must happen in us-east-1 — not your default region, not where your S3 bucket lives. The AWS CLI --region flag is mandatory here. If you skip it and your default profile points to another region, you will request a valid certificate that CloudFront cannot see.
aws acm request-certificate \
--domain-name example.com \
--subject-alternative-names www.example.com \
--validation-method DNS \
--region us-east-1
The command returns a CertificateArn. Save it — you will need it in Step 3. The certificate status will be PENDING_VALIDATION until you complete Step 2.
Use --subject-alternative-names to cover both the apex domain and the www subdomain in a single certificate. ACM will generate a separate CNAME validation record for each name.
Step 2: Validate the Certificate via DNS (Route 53)
ACM DNS validation works by requiring you to add a CNAME record to your hosted zone. ACM polls for that record and, once it detects it, issues the certificate. This is where most people get stuck — the validation record name is a long, ACM-generated token, not something predictable like _acme-challenge.
First, retrieve the validation CNAME values ACM generated for your certificate:
aws acm describe-certificate \
--certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
--region us-east-1 \
--query 'Certificate.DomainValidationOptions'
The output will contain one entry per domain name. Each entry includes a ResourceRecord block with a Name, Type (CNAME), and Value. The Name field looks something like _3e5b6a7c8d9f0a1b2c3d4e5f6a7b8c9d.example.com. — it is unique to your certificate and account. Copy both the Name and Value exactly as returned; do not guess or reconstruct them.
Get your Route 53 hosted zone ID:
aws route53 list-hosted-zones-by-name \
--dns-name example.com \
--query 'HostedZones[0].Id' \
--output text
Then create the validation record. Build a change batch file using the exact values from describe-certificate:
🔽 Click to expand: validation-change-batch.json
{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.example.com.",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [
{
"Value": "_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy.acm-validations.aws."
}
]
}
}
]
}
Important: The Name value above (_xxxxxxxx...example.com.) is a placeholder. You must replace it — and the Value field — with the exact strings returned by aws acm describe-certificate. These tokens are unique per certificate and cannot be derived from the domain name alone.
aws route53 change-resource-record-sets \
--hosted-zone-id Z1D633PJN98FT9 \
--change-batch file://validation-change-batch.json
ACM typically detects the record and transitions the certificate to ISSUED within a few minutes, though DNS propagation can extend this. Poll the status:
aws acm describe-certificate \
--certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
--region us-east-1 \
--query 'Certificate.Status'
Do not proceed to Step 3 until the status is ISSUED. CloudFront will not accept a certificate in PENDING_VALIDATION state.
Step 3: Attach the Certificate to CloudFront and Configure the Alternate Domain Name
Two things must happen together here: the ACM certificate ARN gets attached to the distribution, and the custom domain gets added as an alternate domain name (CNAME). CloudFront enforces a strict rule — you cannot serve HTTPS for a domain unless that domain is listed in the distribution's Aliases (alternate domain names). Miss this and CloudFront will either reject the request or serve the default CloudFront certificate, causing a hostname mismatch error in the browser.
Retrieve your current distribution config and its ETag (required for updates):
aws cloudfront get-distribution-config \
--id EDFDVBD6EXAMPLE \
--output json > dist-config.json
The file contains a top-level ETag and a nested DistributionConfig object. You need to edit the DistributionConfig portion — specifically the Aliases and ViewerCertificate blocks — then submit only the DistributionConfig object back with the ETag.
🔽 Click to expand: updated ViewerCertificate and Aliases blocks
"Aliases": {
"Quantity": 2,
"Items": [
"example.com",
"www.example.com"
]
},
"ViewerCertificate": {
"ACMCertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"SSLSupportMethod": "sni-only",
"MinimumProtocolVersion": "TLSv1.2_2021",
"CloudFrontDefaultCertificate": false
}
Extract just the DistributionConfig block into a separate file (the update API does not accept the ETag inside the JSON body), then apply the update:
aws cloudfront update-distribution \
--id EDFDVBD6EXAMPLE \
--distribution-config file://updated-dist-config.json \
--if-match YOUR_ETAG_VALUE
The distribution will enter InProgress deployment state. Changes propagate to all edge locations before taking effect — check status with:
aws cloudfront get-distribution \
--id EDFDVBD6EXAMPLE \
--query 'Distribution.Status'
Step 4: Create Route 53 Alias Records Pointing to CloudFront
With the certificate attached and the alternate domain names configured, the last piece is DNS. Route 53 alias records are the right tool here — they resolve directly to the CloudFront distribution domain at the DNS layer, support the zone apex (bare domain like example.com where a CNAME is not allowed by DNS spec), and do not incur extra query charges for alias resolution to AWS resources.
The CloudFront hosted zone ID for alias records is a fixed value documented by AWS: Z2FDTNDATAQYW2. This is not your Route 53 hosted zone ID — it is the hosted zone ID that represents CloudFront globally for alias record purposes.
🔽 Click to expand: alias-records-change-batch.json
{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "example.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "d111111abcdef8.cloudfront.net.",
"EvaluateTargetHealth": false
}
}
},
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "www.example.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "d111111abcdef8.cloudfront.net.",
"EvaluateTargetHealth": false
}
}
}
]
}
aws route53 change-resource-record-sets \
--hosted-zone-id Z1D633PJN98FT9 \
--change-batch file://alias-records-change-batch.json
Replace d111111abcdef8.cloudfront.net. with your actual distribution domain name, which you can retrieve with:
aws cloudfront get-distribution \
--id EDFDVBD6EXAMPLE \
--query 'Distribution.DomainName' \
--output text
Verifying the Full Chain
Once the CloudFront deployment reaches Deployed status and DNS propagates, verify each layer independently before assuming the setup is correct.
Check that DNS resolves to CloudFront:
dig www.example.com +short
Verify the certificate being served matches your domain:
curl -vI https://www.example.com 2>&1 | grep -E 'subject:|issuer:|SSL connection'
Confirm CloudFront is returning a response (not an S3 direct response):
curl -sI https://www.example.com | grep -i 'x-cache\|server\|via'
A response with X-Cache: Hit from cloudfront or Miss from cloudfront confirms traffic is flowing through the distribution. If you see an S3 error page or a certificate mismatch, work back through the checklist: certificate status, alternate domain name configuration, and alias record target.
A Real Failure Pattern Worth Knowing
The most common failure here looks like this: the certificate shows ISSUED, the CloudFront distribution shows Deployed, but the browser still shows a certificate error. The instinct is to blame DNS propagation and wait. That is usually wrong.
The actual cause in most cases: the alternate domain name was not added to the CloudFront distribution, or it was added with a typo (trailing dot, wrong subdomain). CloudFront will serve its default *.cloudfront.net certificate for any domain not explicitly listed in Aliases, which produces a hostname mismatch against your custom domain — not a 'certificate not found' error, but a 'certificate does not match this hostname' error. The two look similar in a browser but point to completely different fixes.
Think of the CloudFront Aliases list as a bouncer list. The ACM certificate is your ID — valid and government-issued. But if your name is not on the list, the bouncer (CloudFront) still falls back to its default credential, which does not match your domain.
Check the aliases on your distribution before assuming a cert or DNS problem:
aws cloudfront get-distribution-config \
--id EDFDVBD6EXAMPLE \
--query 'DistributionConfig.Aliases'
IAM Permissions Required
The operations in this post require permissions across three services. Below is a least-privilege policy covering the actions used:
🔽 Click to expand: IAM policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ACMCertificateManagement",
"Effect": "Allow",
"Action": [
"acm:RequestCertificate",
"acm:DescribeCertificate",
"acm:ListCertificates"
],
"Resource": "*"
},
{
"Sid": "CloudFrontDistributionUpdate",
"Effect": "Allow",
"Action": [
"cloudfront:GetDistribution",
"cloudfront:GetDistributionConfig",
"cloudfront:UpdateDistribution"
],
"Resource": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
},
{
"Sid": "Route53RecordManagement",
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListHostedZonesByName",
"route53:GetChange"
],
"Resource": "*"
}
]
}
Note: acm:RequestCertificate, acm:DescribeCertificate, and the Route 53 actions require Resource: * — the Service Authorization Reference confirms these actions do not support resource-level restrictions in all contexts. Scope the CloudFront actions to the specific distribution ARN.
Wrap-Up and Next Steps for Your CloudFront Custom Domain Setup
At this point your static S3 site is served over HTTPS through CloudFront on your custom domain. The four-step sequence — ACM certificate in us-east-1, DNS validation, CloudFront alternate domain + cert attachment, Route 53 alias records — is the complete path. Each step has a hard dependency on the previous one, so order matters.
From here, consider:
- Enforcing HTTPS-only by setting the CloudFront viewer protocol policy to
redirect-http-to-httpsorhttps-only. - Locking down the S3 bucket with an Origin Access Control (OAC) policy so the bucket is not publicly accessible directly — only through CloudFront.
- Enabling CloudFront access logging to an S3 bucket for traffic analysis and debugging.
Official references: CloudFront HTTPS requirements, ACM DNS validation, Route 53 alias records for CloudFront.
Glossary
| Term | Definition |
|---|---|
| ACM (AWS Certificate Manager) | AWS service that provisions and manages TLS/SSL certificates. CloudFront requires ACM certificates issued in us-east-1. |
| Alternate Domain Name (CNAME) | A custom domain configured on a CloudFront distribution. CloudFront only serves the attached certificate for domains listed here. |
| Route 53 Alias Record | A Route 53-specific DNS record type that maps a domain to an AWS resource (like a CloudFront distribution) without a standard CNAME. Supports zone apex. |
| SNI (Server Name Indication) | A TLS extension that allows CloudFront to serve multiple certificates on the same IP. sni-only is the standard SSL support method for CloudFront distributions. |
| OAC (Origin Access Control) | A CloudFront mechanism that restricts S3 bucket access to the CloudFront distribution only, replacing the older Origin Access Identity (OAI). |
Related Posts
- 📄 How to Host a Static Website on S3: Step-by-Step Guide
- 📄 CloudFront + S3 Static Website 403 Errors: OAC vs Public Bucket Explained
- 📄 Route 53 Alias vs. CNAME Records: The Definitive Guide for Pointing Domains to an ALB
- 📄 How to Transfer a Domain from Another Registrar to Route 53 (GoDaddy & Others)
- 📄 CloudFront Cache Invalidation: Force-Refresh Stale Edge Content After S3 Updates
Comments
Post a Comment