AWS CloudFront SSL and Custom Domains
Serve CloudFront on your own domain over HTTPS: the us-east-1 certificate requirement, DNS validation, alias records, security policies, and origin-side TLS.
CloudFront SSL and Custom Domains
By default a distribution answers on d1234abcd.cloudfront.net with a certificate AWS provides. Putting it on your own domain over HTTPS takes three things: a certificate in the right region, the domain registered on the distribution, and DNS pointing at it.
The one rule to remember before anything else: the ACM certificate must be in us-east-1.
The us-east-1 requirement
CloudFront is a global service whose control plane runs in us-east-1, and it can only attach certificates from that region. This holds regardless of where your bucket, load balancer or anything else lives.
aws acm request-certificate --region us-east-1 \
--domain-name example.com \
--subject-alternative-names 'www.example.com' '*.example.com' \
--validation-method DNS \
--query CertificateArn --output text
What makes this trip so many people is the failure mode: a certificate in the wrong region is perfectly valid, validates successfully, and simply does not appear in CloudFront's list of choices. Nothing reports an error. If you already have one elsewhere for an ALB, leave it and request a second in us-east-1. ACM certificates are free, and there is no downside to holding both.
Request the wildcard alongside the apex. *.example.com covers www and any future subdomain but not example.com itself, which is why both are listed above.
DNS validation
aws acm describe-certificate --region us-east-1 \
--certificate-arn "$CERT_ARN" \
--query 'Certificate.DomainValidationOptions[].[DomainName,ResourceRecord.Name,ResourceRecord.Value]' \
--output table
-------------------------------------------------------------------------
| example.com | _a1b2c3.example.com. | _d4e5f6.acm-validations.aws. |
-------------------------------------------------------------------------
Create that CNAME. In Route 53 one command does it:
aws acm describe-certificate --region us-east-1 --certificate-arn "$CERT_ARN" \
--query 'Certificate.DomainValidationOptions[0].ResourceRecord'
# then create the CNAME with change-resource-record-sets
aws acm wait certificate-validated --region us-east-1 --certificate-arn "$CERT_ARN"
Prefer DNS validation over email. It completes in minutes without anyone reading a mailbox, and, the part that matters long term, ACM renews automatically for as long as the validation record stays in place. Deleting that CNAME after validation is a quiet way to break renewal a year later. Leave it.
A certificate stuck in PENDING_VALIDATION is nearly always a missing trailing dot, a record created in the wrong hosted zone, or a subdomain zone that is delegated elsewhere.
Attaching the domain
Two things must both be set: the alternate domain name (CNAME) and the certificate.
"Aliases": { "Quantity": 2, "Items": ["example.com", "www.example.com"] },
"ViewerCertificate": {
"ACMCertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/…",
"SSLSupportMethod": "sni-only",
"MinimumProtocolVersion": "TLSv1.2_2021",
"CertificateSource": "acm"
}
CloudFront verifies that the certificate covers every alias, and rejects the update otherwise. Every name in Aliases must be in the certificate's subject or SANs.
sni-only is what you want. The alternative, vip, assigns dedicated IP addresses at a substantial monthly cost per distribution, and exists only for clients too old to support SNI. A population that no longer meaningfully exists.
DNS to the distribution
For an apex domain, a CNAME is not allowed by the DNS specification. Route 53's alias record solves this:
aws route53 change-resource-record-sets --hosted-zone-id "$ZONE_ID" --change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "example.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "d1234abcd.cloudfront.net",
"EvaluateTargetHealth": false
}
}
}]
}'
Z2FDTNDATAQYW2 is the fixed hosted zone ID for CloudFront. The same for every distribution in every account. Alias records also cost nothing to query, unlike CNAMEs.
Add an AAAA alias with the same target so IPv6 clients resolve too. Forgetting it produces intermittent failures for exactly the users who have IPv6, which is a confusing report to receive.
Outside Route 53, use a CNAME for subdomains and whatever apex-flattening your provider offers (ALIAS, ANAME, or CNAME flattening) for the apex.
Security policy
"MinimumProtocolVersion": "TLSv1.2_2021"
The minimum protocol version sets which TLS versions and ciphers CloudFront accepts from viewers. TLSv1.2_2021 is a sensible modern floor; TLS 1.0 and 1.1 policies still exist and fail most compliance scans. Raising it drops genuinely ancient clients, which is the trade you are making deliberately.
Note it only applies when you use a custom domain with sni-only. On the default *.cloudfront.net name, AWS controls the policy.
Origin-side TLS
The viewer-to-CloudFront leg and the CloudFront-to-origin leg are configured separately, and leaving the second as plain HTTP is a common oversight:
"CustomOriginConfig": {
"HTTPPort": 80,
"HTTPSPort": 443,
"OriginProtocolPolicy": "https-only",
"OriginSslProtocols": {"Quantity": 1, "Items": ["TLSv1.2"]}
}
https-only is the right choice for any custom origin. match-viewer sounds appealing and means an HTTP viewer request travels to your origin unencrypted, so avoid it.
The origin's certificate must be publicly trusted and match the origin's domain name. CloudFront will not accept a self-signed certificate, and there is no option to skip verification. A 502 immediately after switching to https-only is usually this.
S3 origins do not need any of it: CloudFront always uses HTTPS to S3.
Common errors
InvalidViewerCertificate: The specified SSL certificate doesn't exist,
isn't in us-east-1 region, isn't valid, or doesn't include a valid
certificate chain.
Almost always the region. Re-request in us-east-1.
CNAMEAlreadyExists: One or more of the CNAMEs you provided are already
associated with a different resource
That alias is on another distribution, possibly in a different account. CloudFront enforces global uniqueness of alternate domain names. Remove it from the old distribution first.
InvalidViewerCertificate: ... the certificate does not cover the
alternate domain names
An alias is missing from the certificate's SANs. Note *.example.com does not cover example.com.
ERR_SSL_PROTOCOL_ERROR in the browser
Usually DNS pointing at the distribution before the alias and certificate were attached, so CloudFront answers with its default certificate, which does not match the hostname requested.
Quick reference
| Task | Command |
|---|---|
| Request a cert | aws acm request-certificate --region us-east-1 --validation-method DNS |
| Get validation records | aws acm describe-certificate --query 'Certificate.DomainValidationOptions' |
| Wait for validation | aws acm wait certificate-validated --region us-east-1 |
| List certs in us-east-1 | aws acm list-certificates --region us-east-1 |
| Route 53 alias | AliasTarget.HostedZoneId = Z2FDTNDATAQYW2 |
| Check what is served | curl -sI https://example.com | head -5 |
| Inspect the certificate | openssl s_client -connect example.com:443 -servername example.com |
Key takeaways
- The certificate must be in us-east-1, and one elsewhere silently does not appear.
- Use DNS validation and leave the CNAME in place: it is what enables automatic renewal.
*.example.comdoes not coverexample.com. Request both.- Every alias must be covered by the certificate, or the update is rejected.
sni-only, notvip. Dedicated IPs cost a lot and solve a problem that no longer exists.- Use Route 53 alias records for the apex, with zone ID
Z2FDTNDATAQYW2, plus an AAAA record. - Set
MinimumProtocolVersiontoTLSv1.2_2021or better. - Set the origin policy to
https-only, nevermatch-viewer.
Check your understanding
A browser SSL error on a new domain
DNS for shop.example.com was pointed at a CloudFront distribution, but browsers report ERR_SSL_PROTOCOL_ERROR. The distribution works fine on its cloudfront.net name. What was missed?
A certificate that expires anyway
An ACM certificate validated by DNS a year ago has failed to renew, and the team tidied up "unused" DNS records a few months back. What happened?
Frequently Asked Questions
Why must a CloudFront certificate be in us-east-1?
Because CloudFront is a global service whose control plane lives in that region, and it can only read ACM certificates from there. It applies no matter where the rest of your infrastructure runs. The confusing part is the failure mode: a certificate requested in another region validates normally and is perfectly usable by regional services such as an ALB, but simply never appears in CloudFront's list, with no error explaining why. Request a separate one in us-east-1; ACM certificates are free.
How do I point my apex domain at CloudFront?
The DNS specification forbids a CNAME at the zone apex, so use a Route 53 alias record of type A pointing at the distribution's domain name, with hosted zone ID Z2FDTNDATAQYW2. The same value for every CloudFront distribution. Add an AAAA alias with the same target so IPv6 clients resolve too. Outside Route 53, use whatever apex flattening your DNS provider offers, commonly called ALIAS, ANAME or CNAME flattening.
Why does CloudFront say CNAMEAlreadyExists?
Alternate domain names are globally unique across all of CloudFront, so the hostname is already attached to another distribution. Frequently an old one in the same account, sometimes one in a different account entirely. Remove the alias from the existing distribution first, wait for that change to deploy, then add it to the new one. If the other distribution is not yours, AWS Support can help verify domain ownership and release it.
Does a wildcard certificate cover my apex domain?
No. *.example.com matches www.example.com and any other single-level subdomain, but not example.com itself, and not a.b.example.com. Request both names on one certificate: --domain-name example.com --subject-alternative-names '*.example.com'. CloudFront validates that the certificate covers every entry in Aliases, so a missing apex is rejected at configuration time rather than causing a mysterious runtime error.
Should I use sni-only or a dedicated IP?
sni-only, essentially always. Server Name Indication lets many distributions share addresses by sending the requested hostname during the TLS handshake, and every browser and client in current use supports it. The dedicated IP option (vip) costs a significant monthly fee per distribution and exists only for clients predating SNI, which in practice means Windows XP and similar. There is no performance or security advantage to it.
Do I need HTTPS between CloudFront and my origin?
Yes for a custom origin. Set OriginProtocolPolicy to https-only. Viewer-side and origin-side TLS are configured separately, so a distribution serving HTTPS to browsers can still be fetching over plain HTTP behind the scenes. Avoid match-viewer, which forwards an HTTP viewer request to the origin unencrypted. The origin's certificate must be publicly trusted and match its hostname, since CloudFront offers no way to skip verification; a 502 right after enabling https-only is usually that. S3 origins always use HTTPS and need no configuration.