The guide you came from tells you to get a Let's Encrypt certificate because "ACM validation is time-consuming." That was my reasoning at the time. It was wrong on the part that matters, and it cost me a broken site.
What actually went wrong
Let's Encrypt certificates last 90 days. The old guide even says it out loud: "repeat steps 2 to 4 after expiration."
That line is the whole problem. Every 90 days I had to run certbot, add a fresh _acme-challenge TXT record, export fullchain.pem and privkey.pem, re-import them into ACM, and re-point CloudFront. Ten minutes of manual work, four times a year.
Then one cycle I forgot. The certificate expired on May 26. For weeks, aws.domain.com served an expired certificate, and every visitor got a browser security warning. I only found out when I went looking.
Why I skipped ACM the first time
I thought ACM validation was slow and needed a domain email. That's the old email-validation flow. ACM also does DNS validation, and it's just as fast as Let's Encrypt: add one CNAME record to Route 53, ACM reads it back within minutes, done.
But speed was never the real difference. The real difference is renewal.
A validation record is an issuance gate, not an HTTPS switch
This part confused me for a while, so here it is plainly.
A domain-validation record does not turn HTTPS on. It has one job, at one moment. When a CA (Let's Encrypt or ACM) is about to sign a certificate that says "this is aws.domain.com", it first has to confirm you actually control that domain. Otherwise anyone could get a certificate for google.com.
The proof: the CA gives you a random value, you write it into your domain's DNS, the CA reads it back. Only the domain owner can edit that DNS. Proof accepted, certificate signed.
Issuance (happens once):
You: I want a certificate for aws.domain.com
CA: put "abc123" in your DNS
You: (write it to Route 53)
CA: (reads your DNS) sees abc123 -> signs the certificate
At runtime, when a visitor opens your site, nothing reads that DNS record. The browser only checks the certificate the server hands it: is it signed by a CA I trust, does the domain match, is it still valid? That is what the padlock means.
Every visit:
Browser: connect to aws.domain.com:443
CloudFront: here is my certificate, signed by Amazon
Browser: trusted signature? domain matches? not expired? -> padlock
So the DNS record is a gate at signing time, not a switch at connection time.
Why that makes ACM auto-renew and Let's Encrypt not
Certificates expire, so they get re-signed. Re-signing means the CA validates again.
- Let's Encrypt (manual DNS-01): each renewal gives you a new random TXT value. You update
_acme-challengeby hand every 90 days. That is the chore. - ACM: the validation CNAME value never changes. ACM re-reads the same record on every renewal, forever, on its own. You set it once.
That is the entire payoff. The CNAME is a standing proof of "I still control this domain," and ACM keeps pulling it.
If you know OAuth, the CNAME plays the role of a refresh token: a durable thing that silently regenerates the short-lived thing, where the certificate is the access token. Delete it and you are forced back to manual re-validation, the same way losing a refresh token forces a fresh login. One difference: a refresh token is a secret you present, while this CNAME is public and the CA pulls it. Its security comes from "only the owner could have written it," not from being hidden.
The migration
Everything lives in one AWS account. The certificate must be in us-east-1, because CloudFront only reads certificates from that region.
Before you paste, there are only three values you replace with your own:
aws.domain.com-> your domain.<zone-id>-> your Route 53 hosted zone ID (aws route53 list-hosted-zones).<distribution-id>-> your CloudFront distribution ID (aws cloudfront list-distributions).
Everything in UPPERCASE (CERT_ARN, NAME, VALUE, ETAG) is a shell variable the previous command fills in for you. Leave those alone, and run the steps in order in the same terminal so the variables carry over.
1. Request the certificate (DNS validation). CERT_ARN captures the new certificate's ARN so the later steps can point at it:
CERT_ARN=$(aws acm request-certificate \
--domain-name aws.domain.com \
--validation-method DNS \
--region us-east-1 \
--query CertificateArn --output text)2. Write ACM's validation CNAME into Route 53. The first command pulls the record ACM generated in step 1 into NAME and VALUE; the second writes it into your zone:
RR=$(aws acm describe-certificate --certificate-arn "$CERT_ARN" \
--region us-east-1 \
--query "Certificate.DomainValidationOptions[0].ResourceRecord" --output json)
NAME=$(echo "$RR" | jq -r .Name)
VALUE=$(echo "$RR" | jq -r .Value)
aws route53 change-resource-record-sets \
--hosted-zone-id <zone-id> \
--change-batch "{\"Changes\":[{\"Action\":\"UPSERT\",\"ResourceRecordSet\":{\"Name\":\"$NAME\",\"Type\":\"CNAME\",\"TTL\":300,\"ResourceRecords\":[{\"Value\":\"$VALUE\"}]}}]}"3. Wait for validation (minutes). This blocks until ACM has read the CNAME and marked the certificate issued:
aws acm wait certificate-validated --certificate-arn "$CERT_ARN" --region us-east-14. Point CloudFront at the new certificate. ETAG is the current config's version tag; CloudFront requires it on update so you don't overwrite a change someone else made. The jq line swaps only the certificate ARN and leaves the rest of the distribution config untouched:
ETAG=$(aws cloudfront get-distribution-config --id <distribution-id> --query ETag --output text)
aws cloudfront get-distribution-config --id <distribution-id> \
--query DistributionConfig --output json > dist.json
jq --arg arn "$CERT_ARN" \
'.ViewerCertificate.ACMCertificateArn=$arn | .ViewerCertificate.Certificate=$arn' \
dist.json > dist-new.json
aws cloudfront update-distribution --id <distribution-id> \
--if-match "$ETAG" --distribution-config file://dist-new.json5. Delete the old expired Let's Encrypt certificate, after confirming the site serves the new one.
The one rule to remember
Never delete that validation CNAME. It looks like leftover once the certificate is issued. It is not. ACM re-reads it on every auto-renewal. Delete it and the next renewal fails, the certificate expires, and you are back where I started. AWS's own docs say the same: remove the CNAME and managed renewal stops.
The old _acme-challenge TXT record is the opposite. You can delete it, because you are done with Let's Encrypt and nothing will ever read it again.
And that "Don't skip this even though it says optional" warning in the old guide, about pasting the certificate chain? With ACM it is gone. ACM manages the chain itself. Nothing to paste, nothing to forget.