ACM: Request and Validate a Public TLS Certificate via DNS¶
TLS certificate provisioning — Requests a public certificate from ACM for a domain, injects the DNS validation CNAME record into the authoritative DNS provider, and waits for ACM to confirm ownership and issue the certificate. Once issued, ACM auto-renews the certificate before expiry without further intervention.
Prerequisite: AWS CLI configured with credentials that carry acm:RequestCertificate, acm:DescribeCertificate, and route53:ChangeResourceRecordSets permissions. The domain must resolve publicly — NS delegation must be active before ACM can validate.
Workflow Overview¶
This runbook covers the following steps in order:
- Request the ACM certificate for the target domain
- Retrieve the CNAME name and value ACM generates for DNS validation
- Add the CNAME record to the authoritative DNS provider (Route 53 or Cloudflare)
- Wait for ACM to detect the record and issue the certificate
- Confirm the issued status
The DNS provider step varies. Follow the Route 53 path if the hosted zone lives in Route 53. Follow the Cloudflare path if the domain is managed on Cloudflare. The CNAME Name field is handled differently between the two providers.
Set Variables¶
DOMAIN="ibtisam.qzz.io"
REGION="us-east-1" # Use us-east-1 for CloudFront; match service region for ALB
Region matters for CloudFront
CloudFront only accepts ACM certificates from us-east-1. For ALB or API Gateway, request the certificate in the same region as the service. Requesting in the wrong region means the certificate will not appear in the service's certificate picker and cannot be attached.
If the validation record will be injected into Route 53, capture the hosted zone ID now.
HOSTED_ZONE_ID=$(aws route53 list-hosted-zones \
--query "HostedZones[?Name=='${DOMAIN}.'].Id" \
--output text | cut -d'/' -f3)
echo "Hosted Zone ID: $HOSTED_ZONE_ID"
Skip the HOSTED_ZONE_ID step entirely if using Cloudflare.
Request the Certificate¶
Request a certificate that covers the apex domain and all first-level subdomains.
CERT_ARN=$(aws acm request-certificate \
--domain-name "$DOMAIN" \
--subject-alternative-names "*.${DOMAIN}" \
--validation-method DNS \
--region "$REGION" \
--query "CertificateArn" \
--output text)
echo "Certificate ARN: $CERT_ARN"
The certificate is created in PENDING_VALIDATION state. It stays there until the DNS CNAME record is added and ACM can verify it.
One CNAME covers both apex and wildcard
ACM issues a single CNAME validation record that satisfies both ibtisam.qzz.io and *.ibtisam.qzz.io. Only one record needs to be added to the DNS provider.
Retrieve the Validation CNAME¶
Wait a few seconds after the request, then fetch the CNAME name and value ACM generated for this certificate.
sleep 10
aws acm describe-certificate \
--certificate-arn "$CERT_ARN" \
--region "$REGION" \
--query "Certificate.DomainValidationOptions[*].{Domain:DomainName,Name:ResourceRecord.Name,Value:ResourceRecord.Value,Type:ResourceRecord.Type}" \
--output table
Capture Name and Value into variables.
CNAME_NAME=$(aws acm describe-certificate \
--certificate-arn "$CERT_ARN" \
--region "$REGION" \
--query "Certificate.DomainValidationOptions[0].ResourceRecord.Name" \
--output text)
CNAME_VALUE=$(aws acm describe-certificate \
--certificate-arn "$CERT_ARN" \
--region "$REGION" \
--query "Certificate.DomainValidationOptions[0].ResourceRecord.Value" \
--output text)
echo "CNAME Name: $CNAME_NAME"
echo "CNAME Value: $CNAME_VALUE"
ACM always returns both values with a trailing dot. Example output from a real certificate request for ibtisam.qzz.io:
CNAME Name: _2dfb6fc829ca40151f533feb5aab4c86.ibtisam.qzz.io.
CNAME Value: _cb393cd72cee9292c19b9bbc2b510185.jkddzztszm.acm-validations.aws.
What ACM returns for apex vs subdomain
For an apex domain ibtisam.qzz.io:
For a subdomain rank.ibtisam.qzz.io:
The structure is identical. What to do with the trailing dot and domain suffix depends entirely on which DNS provider receives the record.
describe-certificate returns null for ResourceRecord
ACM has not yet generated the validation record. Wait 15 to 30 seconds and retry. This can happen when describe-certificate is called immediately after request-certificate.
Add the Validation CNAME to DNS¶
The CNAME record must be added to whichever DNS provider is currently authoritative for the domain. Choose the path that applies.
Option A: Route 53¶
Inject the CNAME directly into the hosted zone using the AWS CLI.
cat > /tmp/acm-validation.json <<EOF
{
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "${CNAME_NAME}",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [
{ "Value": "${CNAME_VALUE}" }
]
}
}
]
}
EOF
aws route53 change-resource-record-sets \
--hosted-zone-id "$HOSTED_ZONE_ID" \
--change-batch file:///tmp/acm-validation.json
Route 53 accepts the trailing dot as-is
Pass $CNAME_NAME and $CNAME_VALUE directly into the JSON with no modification. Route 53 accepts the full FQDN including the trailing dot for both apex domains and subdomains. No trimming of any kind is required.
Option B: Cloudflare¶
Log in to the Cloudflare dashboard, navigate to the domain, and go to DNS > Records > Add record.
Cloudflare does not auto-strip anything. Every character entered is stored exactly as typed. The table below shows what ACM returns and what must be entered manually in the Cloudflare UI for each case.
Apex domain (ibtisam.qzz.io) | Subdomain (rank.ibtisam.qzz.io) | |
|---|---|---|
| ACM returns (Name) | _abc123.ibtisam.qzz.io. | _abc123.rank.ibtisam.qzz.io. |
| Enter in Cloudflare Name field | _abc123.ibtisam.qzz.io (remove trailing dot manually) | _abc123.rank (remove .ibtisam.qzz.io. suffix and trailing dot manually) |
| ACM returns (Value) | _xyz789.acm-validations.aws. | _xyz789.acm-validations.aws. |
| Enter in Cloudflare Target field | _xyz789.acm-validations.aws (remove trailing dot manually) | _xyz789.acm-validations.aws (remove trailing dot manually) |
| Proxy status | DNS only (grey cloud) | DNS only (grey cloud) |
Grey cloud is mandatory for the validation CNAME
Set proxy status to DNS only on this record. If the record is proxied (orange cloud), Cloudflare rewrites the DNS response and ACM cannot read the actual CNAME value. The certificate stays in PENDING_VALIDATION indefinitely.
Why Route 53 and Cloudflare differ
Route 53 is operated via the AWS CLI which handles FQDN notation natively. Cloudflare's dashboard UI stores exactly what is typed — nothing is stripped or appended automatically. For a subdomain, Cloudflare also appends the zone root internally, so the apex domain suffix in the Name must be removed before saving, otherwise the record is created with the full domain duplicated.
Wait for Issuance¶
Block until ACM confirms ownership and issues the certificate.
aws acm wait certificate-validated \
--certificate-arn "$CERT_ARN" \
--region "$REGION"
echo "Certificate issued."
The waiter polls every 60 seconds with a maximum of 40 attempts (40 minutes total). In practice, issuance takes 2 to 5 minutes once the CNAME is present and NS delegation is fully propagated.
Confirm the final status.
aws acm describe-certificate \
--certificate-arn "$CERT_ARN" \
--region "$REGION" \
--query "Certificate.{Status:Status,NotAfter:NotAfter,DomainName:DomainName}" \
--output table
A Status of ISSUED and a NotAfter date 13 months out confirms the certificate is valid and ready to attach.
Auto-Renewal¶
ACM renews DNS-validated certificates automatically. The CNAME record added to the DNS provider remains in place permanently. ACM queries it 60 days before expiry to renew without any manual step.
Do not delete the validation CNAME
Removing the validation CNAME breaks auto-renewal. The next renewal attempt will fail and the certificate will eventually expire. Leave the record in place for the lifetime of the certificate — on both Route 53 and Cloudflare.
Troubleshooting¶
Certificate stays in PENDING_VALIDATION after 30 minutes
Check whether NS delegation is complete.
If the old nameservers are still returned, propagation has not finished. ACM cannot find the CNAME because Route 53 is not yet authoritative.
Verify the CNAME record is present in Route 53.
aws route53 list-resource-record-sets \
--hosted-zone-id "$HOSTED_ZONE_ID" \
--query "ResourceRecordSets[?Type=='CNAME']" \
--output table
Confirm the CNAME resolves publicly.
For Cloudflare: confirm the record is set to DNS only (grey cloud), not proxied (orange cloud). Also confirm no trailing dot and no apex suffix appears in the stored Name field.
wait certificate-validated exits with a non-zero code
The waiter timed out (40 minutes elapsed). Check NS delegation and CNAME presence using the steps above, then re-run the waiter.
Quick Reference¶
Copy and run the setup block. Then choose the DNS injection block that matches the authoritative provider. Finish with the verification block.
# Set variables
DOMAIN="ibtisam.qzz.io"
REGION="us-east-1"
# Request the certificate
CERT_ARN=$(aws acm request-certificate \
--domain-name "$DOMAIN" \
--subject-alternative-names "*.${DOMAIN}" \
--validation-method DNS \
--region "$REGION" \
--query "CertificateArn" \
--output text)
echo "Certificate ARN: $CERT_ARN"
# Retrieve the validation CNAME name and value (ACM returns both with a trailing dot)
sleep 10
CNAME_NAME=$(aws acm describe-certificate \
--certificate-arn "$CERT_ARN" \
--region "$REGION" \
--query "Certificate.DomainValidationOptions[0].ResourceRecord.Name" \
--output text)
CNAME_VALUE=$(aws acm describe-certificate \
--certificate-arn "$CERT_ARN" \
--region "$REGION" \
--query "Certificate.DomainValidationOptions[0].ResourceRecord.Value" \
--output text)
echo "CNAME Name: $CNAME_NAME"
echo "CNAME Value: $CNAME_VALUE"
If DNS is on Route 53, pass values as-is. Route 53 accepts the trailing dot natively — no trimming required for apex or subdomain.
# Capture hosted zone ID (Route 53 only)
HOSTED_ZONE_ID=$(aws route53 list-hosted-zones \
--query "HostedZones[?Name=='${DOMAIN}.'].Id" \
--output text | cut -d'/' -f3)
# Inject the validation CNAME into Route 53 (trailing dot in $CNAME_NAME is fine)
cat > /tmp/acm-validation.json <<EOF
{
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "${CNAME_NAME}",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [ { "Value": "${CNAME_VALUE}" } ]
}
}
]
}
EOF
aws route53 change-resource-record-sets \
--hosted-zone-id "$HOSTED_ZONE_ID" \
--change-batch file:///tmp/acm-validation.json
If DNS is on Cloudflare, add the record manually in the dashboard (DNS only, grey cloud). Remove the trailing dot from both Name and Target. For a subdomain certificate, also remove the apex domain suffix from the Name field.
# Wait for ACM to detect the CNAME and issue the certificate
aws acm wait certificate-validated \
--certificate-arn "$CERT_ARN" \
--region "$REGION"
echo "Certificate issued."
# Confirm issued status
aws acm describe-certificate \
--certificate-arn "$CERT_ARN" \
--region "$REGION" \
--query "Certificate.{Status:Status,NotAfter:NotAfter,DomainName:DomainName}" \
--output table