Skip to content

Certbot DNS-01 Challenge: Wildcard Certificates & TXT Record Setup

ACME DNS-01 Challenge: Complete Setup & Troubleshooting Guide

Section titled “ACME DNS-01 Challenge: Complete Setup & Troubleshooting Guide”

DNS-01 challenge validation is the most reliable way to issue wildcard and OV/EV certificates with Certbot and other ACME clients. This guide shows the exact validation flow, TXT record setup, rate-limit handling and troubleshooting steps that actually work in production enterprise environments. With a section on secure credential management.

Need help with ACME? Ask Axel Axelspire AI bot with own augmented memory for all ACME/certbot.

DNS-01 is the ONLY way to get wildcard certificates from Let’s Encrypt. HTTP-01 and TLS-ALPN-01 cannot issue wildcards.

Single command for wildcard certificate:

Terminal window
certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d example.com \
-d *.example.com

This certificate covers:

  • example.com (apex domain)
  • *.example.com (all subdomains: www, api, blog, mail, etc.)
  • Works for 100+ subdomains with one certificate
  • Renews automatically without HTTP server access

Why wildcards need DNS-01: Let’s Encrypt must validate you control the DNS zone to issue *.example.com. HTTP-01 would require placing validation files on infinite subdomains—impossible. DNS-01 proves zone control with a single TXT record.

Every DNS-01 validation requires a TXT record at _acme-challenge:

FieldValueExample
Record Name_acme-challenge.example.com_acme-challenge.api.company.com
Record TypeTXTTXT
Record ValueToken from Certbot"9G8F7K3LmN2pQ1rS5tU8vW0x4yZ6..."
TTL60 seconds (recommended)60

For wildcard certificates, add TXT record for the domain itself:

_acme-challenge.example.com TXT "token-value"

For specific subdomain certificates, add TXT record for that subdomain:

_acme-challenge.api.example.com TXT "token-value"

Manual TXT record creation (if not using DNS plugin):

Terminal window
# 1. Start certificate request
certbot certonly --manual --preferred-challenges dns -d example.com -d *.example.com
# 2. Certbot shows the TXT record to create:
# "Please deploy a DNS TXT record under the name
# _acme-challenge.example.com with the following value:
# 9G8F7K3LmN2pQ1rS5tU8vW0x4yZ6..."
# 3. Add the TXT record in your DNS provider's control panel
# 4. Verify record is live:
dig TXT _acme-challenge.example.com +short
# 5. Press Enter in Certbot to continue validation

Certbot supports 50+ DNS providers through official plugins. Most popular:

ProviderCertbot PluginInstallationCredential Setup
Cloudflarecertbot-dns-cloudflaresnap install certbot-dns-cloudflareAPI token (scoped to zone)
AWS Route53certbot-dns-route53snap install certbot-dns-route53IAM role or AWS credentials
Google Cloud DNScertbot-dns-googlesnap install certbot-dns-googleService account JSON
Azure DNScertbot-dns-azuresnap install certbot-dns-azureManaged identity or service principal
DigitalOceancertbot-dns-digitaloceansnap install certbot-dns-digitaloceanAPI token
OVHcertbot-dns-ovhsnap install certbot-dns-ovhApplication credentials
GoDaddycertbot-dns-godaddysnap install certbot-dns-godaddyAPI key + secret
Namecheapacme.sh (use instead)Not officially supported by CertbotAPI key

Full list: Certbot DNS Plugins Documentation

Example: Cloudflare setup:

Terminal window
# 1. Install plugin
snap install certbot-dns-cloudflare
# 2. Create credentials file
mkdir -p ~/.secrets
chmod 700 ~/.secrets
cat > ~/.secrets/cloudflare.ini << EOF
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
chmod 600 ~/.secrets/cloudflare.ini
# 3. Issue certificate
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d example.com \
-d *.example.com

Example: Route53 setup (using IAM role):

Terminal window
# 1. Install plugin
snap install certbot-dns-route53
# 2. Configure IAM role with Route53 permissions (no credential file needed)
# 3. Issue certificate
certbot certonly \
--dns-route53 \
-d example.com \
-d *.example.com

Overview: Why DNS-01 Enables Advanced ACME Use Cases

Section titled “Overview: Why DNS-01 Enables Advanced ACME Use Cases”

DNS-01 challenge validation unlocks ACME capabilities that HTTP-01 cannot provide. While HTTP-01 requires publicly accessible web servers on port 80, DNS-01 proves domain ownership through DNS infrastructure—making it the only ACME challenge method that supports wildcard certificates (*.example.com) and works in air-gapped environments, behind corporate firewalls, and for services without web servers.

The DNS-01 advantage: Organizations operating in complex network environments—multi-cloud architectures, private networks, IoT deployments—need certificates for systems that cannot expose HTTP endpoints to the internet. DNS-01 moves the validation boundary from HTTP infrastructure to DNS infrastructure, which organizations already manage centrally.

Why This Belongs in ACME Client Operations

Section titled “Why This Belongs in ACME Client Operations”

The ACME Protocol defines DNS-01 specification (RFC 8555 Section 8.4); this guide addresses DNS-01 operations. Understanding the protocol doesn’t prepare you for:

  • DNS provider integration: Each DNS provider (Cloudflare, Route53, Azure DNS, Google Cloud DNS) has different API authentication, rate limits, and propagation characteristics
  • Propagation delays: DNS changes aren’t instantaneous; validation timing must account for DNS TTL, nameserver propagation, and ACME CA polling intervals
  • Multi-domain wildcards: Issuing certificates for example.com AND *.example.com requires careful DNS record coordination
  • Credential security: DNS API credentials grant zone modification powers—compromise enables domain hijacking, not just certificate issuance
  • Split-horizon DNS: Internal and external DNS views complicate validation in enterprise environments

Real-world scenario: Your organization needs a wildcard certificate for *.internal.company.com to cover 50+ internal services. HTTP-01 won’t work (internal domain, no public access). TLS-ALPN-01 won’t work (requires TLS server on port 443 for each validation). DNS-01 is your only option—but your corporate DNS is managed by a separate team with strict change control procedures.

Use DNS-01 when you need:

  • Wildcard certificates: *.example.com, *.api.example.com (only DNS-01 supports wildcards)
  • Private network certificates: Internal services without internet access
  • Non-HTTP services: Mail servers, VPN endpoints, IoT devices, APIs without web servers
  • Multi-cloud deployments: Centralized certificate issuance regardless of infrastructure location
  • Firewall-restricted environments: Systems behind NAT/firewalls that block HTTP-01 validation

Use HTTP-01 instead when:

  • You already have public web servers running
  • You don’t need wildcard certificates
  • You want faster validation (no DNS propagation delay)
  • You want simpler automation (no DNS provider API integration)

This page is part of the Operating ACME Clients section:

For DNS and infrastructure conundefined:

For protocol understanding:


Challenge: Traditional HTTP-01 validation fails when:

  • Servers operate behind firewalls/NAT without port 80/443 exposure to the internet
  • Multiple servers share the same domain (load balancers, CDN origins)
  • Wildcard certificates are required (*.example.com, *.api.example.com)
  • Mail servers (SMTP, IMAP) or internal applications need certificates without running web servers
  • Air-gapped or private network environments cannot receive HTTP-01 challenges
  • Rate limiting makes per-server HTTP-01 validation impractical at scale

Solution: DNS-01 validation proves domain control through DNS infrastructure rather than HTTP endpoints. The ACME CA verifies you can create specific TXT records in your domain’s DNS zone—demonstrating authoritative control over the domain.

Trade-offs: DNS-01 requires DNS provider API access (security consideration), tolerates DNS propagation delays (60-300 seconds typical), and demands careful credential management. For most organizations, these trade-offs are worthwhile for wildcard and private network use cases.

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ACME Client │────1───▶│ ACME CA │ │ DNS │
│ (Certbot) │ │ (Let's │ │ Provider │
└─────────────┘ │ Encrypt) │ │ (Cloudflare)│
│ └─────────────┘ └─────────────┘
│ │ ▲
│ │ │
2. Get TXT value │ │
│ │ │
▼ │ │
┌─────────────┐ │ │
│ DNS API │────3───────────┼────────────────────────┘
│ Integration │ Create TXT record
└─────────────┘ │
4. Verify TXT record
┌─────────────┐
│ DNS Lookup │
│ dig TXT │
│ _acme-chal │
└─────────────┘
5. Certificate issued
Certificate delivered

Flow Steps:

  1. ACME client requests certificate, receives DNS-01 challenge
  2. Client extracts TXT record value from challenge
  3. Client uses DNS provider API to create _acme-challenge.example.com TXT "validation-token"
  4. ACME CA queries DNS to verify TXT record exists
  5. Upon successful verification, CA issues certificate

ACME Client:

  • certbot with DNS plugins (dns-cloudflare, dns-route53, dns-azure, etc.)
  • acme.sh with 50+ DNS provider integrations
  • lego (Go-based ACME client with extensive DNS support)
  • dehydrated (Bash-based ACME client)

DNS Provider Requirements:

  • API for automated TXT record creation/deletion
  • Reasonable API rate limits (100+ requests/hour minimum)
  • Fast DNS propagation (< 300 seconds ideal)
  • Support for DNSSEC (optional but recommended)

Challenge Record Format:

_acme-challenge.example.com. 300 IN TXT "validation-token"

Validation Window:

  • DNS TTL: Typically 60-300 seconds for challenge records
  • CA polling interval: Let’s Encrypt checks every 5-10 seconds for up to 60 seconds
  • Total validation time: 1-5 minutes typical (DNS propagation + CA verification)

Single Domain (Interactive)

Terminal window
# Basic manual challenge
sudo certbot certonly \
--manual \
--preferred-challenges dns \
-d example.com

Process:

  1. Certbot displays: Please deploy a DNS TXT record under the name:
    _acme-challenge.example.com with the following value:
    XYZ123abc...
  2. Manually create DNS record in your DNS provider’s console
  3. Verify propagation: dig TXT _acme-challenge.example.com @8.8.8.8
  4. Press Enter in Certbot after DNS propagation
  5. Certificate issued to /etc/letsencrypt/live/example.com/

Wildcard Certificate (Multiple Domains)

Terminal window
# Wildcard + apex domain
sudo certbot certonly \
--manual \
--preferred-challenges dns \
-d example.com \
-d *.example.com \
-d www.example.com

Important: Wildcard certificates require separate TXT record for *.example.com

Installation

Terminal window
# Install Cloudflare DNS plugin
sudo apt update
sudo apt install python3-certbot-dns-cloudflare
# Or via pip
pip install certbot-dns-cloudflare

Credential Configuration

Terminal window
# Create credentials file
sudo mkdir -p /etc/letsencrypt/cloudflare
sudo tee /etc/letsencrypt/cloudflare/credentials.ini << 'EOF'
# Cloudflare API token (recommended)
dns_cloudflare_api_token = your-cloudflare-api-token-here
# Or legacy API key (less secure)
# dns_cloudflare_email = user@example.com
# dns_cloudflare_api_key = your-cloudflare-global-api-key
EOF
# Secure credentials
sudo chmod 600 /etc/letsencrypt/cloudflare/credentials.ini
sudo chown root:root /etc/letsencrypt/cloudflare/credentials.ini

Obtaining Cloudflare API Token (Scoped Permissions):

  1. Cloudflare Dashboard → My Profile → API Tokens
  2. Create Token → Edit Zone DNS template
  3. Permissions: Zone:DNS:Edit for specific zones
  4. Copy token (only shown once)

Certificate Issuance

Terminal window
# Single domain
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d example.com
# Wildcard certificate
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
--dns-cloudflare-propagation-seconds 30 \
-d example.com \
-d *.example.com

Propagation Tuning:

Terminal window
# Cloudflare DNS propagates quickly (15-30 seconds typical)
--dns-cloudflare-propagation-seconds 30
# For slower DNS providers, increase wait time
--dns-cloudflare-propagation-seconds 120

Installation

Terminal window
pip install certbot-dns-route53

IAM Policy for DNS-01

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:GetChange"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/ZXXXXXXXXXXXXX"
}
]
}

Authentication Methods:

Terminal window
# Method 1: IAM instance profile (recommended for EC2)
sudo certbot certonly --dns-route53 -d example.com
# Method 2: AWS credentials file
export AWS_CONFIG_FILE=/etc/letsencrypt/aws/config
sudo certbot certonly --dns-route53 -d example.com
# Method 3: Environment variables
export AWS_ACCESS_KEY_ID=AKIAXXXXXXXX
export AWS_SECRET_ACCESS_KEY=xxxxx
sudo -E certbot certonly --dns-route53 -d example.com

Multi-Account Route53

Terminal window
# Specify profile for cross-account DNS
AWS_PROFILE=dns-account certbot certonly \
--dns-route53 \
-d example.com

Installation

Terminal window
pip install certbot-dns-azure

Service Principal Setup

Terminal window
# Create service principal
az ad sp create-for-rbac \
--name certbot-dns-azure \
--role "DNS Zone Contributor" \
--scopes /subscriptions/SUBSCRIPTION_ID/resourceGroups/RG_NAME/providers/Microsoft.Network/dnszones/example.com
# Output provides:
# - appId (client_id)
# - password (client_secret)
# - tenant

Configuration

/etc/letsencrypt/azure/credentials.ini
dns_azure_sp_client_id = xxxxx-xxxx-xxxx-xxxx-xxxxx
dns_azure_sp_client_secret = your-client-secret
dns_azure_tenant_id = xxxxx-xxxx-xxxx-xxxx-xxxxx
dns_azure_subscription_id = xxxxx-xxxx-xxxx-xxxx-xxxxx
dns_azure_resource_group = your-resource-group

Certificate Issuance

Terminal window
sudo certbot certonly \
--dns-azure \
--dns-azure-credentials /etc/letsencrypt/azure/credentials.ini \
-d example.com -d *.example.com

Installation

Terminal window
pip install certbot-dns-google

Service Account Setup

Terminal window
# Create service account
gcloud iam service-accounts create certbot-dns \
--display-name "Certbot DNS-01 Challenge"
# Grant DNS admin role
gcloud projects add-iam-policy-binding PROJECT_ID \
--member serviceAccount:certbot-dns@PROJECT_ID.iam.gserviceaccount.com \
--role roles/dns.admin
# Create key file
gcloud iam service-accounts keys create /etc/letsencrypt/gcp/credentials.json \
--iam-account certbot-dns@PROJECT_ID.iam.gserviceaccount.com

Certificate Issuance

Terminal window
sudo certbot certonly \
--dns-google \
--dns-google-credentials /etc/letsencrypt/gcp/credentials.json \
-d example.com -d *.example.com

acme.sh supports 50+ DNS providers with unified interface

Terminal window
# Install acme.sh
curl https://get.acme.sh | sh -s email=admin@example.com
source ~/.acme.sh/acme.sh.env
# Example: Namecheap
export NAMECHEAP_USERNAME="your-username"
export NAMECHEAP_API_KEY="your-api-key"
export NAMECHEAP_SOURCEIP="your-server-ip"
acme.sh --issue --dns dns_namecheap \
-d example.com -d *.example.com
# Example: DigitalOcean
export DO_API_KEY="your-digitalocean-api-token"
acme.sh --issue --dns dns_dgon \
-d example.com -d *.example.com
# List all supported DNS providers
acme.sh --help | grep "dns_"

Multi-Domain Automation Script

#!/bin/bash
set -euo pipefail
# Enterprise DNS-01 automation script
DOMAINS_FILE="/etc/ssl-automation/domains.conf"
DNS_PROVIDER="cloudflare"
CERT_DIR="/etc/letsencrypt/live"
LOG_FILE="/var/log/certbot/dns01-automation.log"
exec > >(tee -a "$LOG_FILE") 2>&1
echo "=== DNS-01 Certificate Automation Started: $(date) ==="
# Read domains from configuration file
# Format: domain_name|cert_name|additional_sans
while IFS='|' read -r domain cert_name sans; do
echo "Processing: $domain"
# Build domain arguments
domain_args="-d $domain"
if [ -n "$sans" ]; then
for san in ${sans//,/ }; do
domain_args="$domain_args -d $san"
done
fi
# Issue/renew certificate
if certbot certonly \
--non-interactive \
--agree-tos \
--email security@company.com \
--dns-${DNS_PROVIDER} \
--dns-${DNS_PROVIDER}-credentials /etc/ssl-automation/dns-credentials.ini \
--dns-${DNS_PROVIDER}-propagation-seconds 60 \
$domain_args \
--cert-name "${cert_name}" \
--deploy-hook "/etc/ssl-automation/deploy-${cert_name}.sh"; then
echo "Success: $domain"
else
echo "Failed: $domain"
# Send alert
echo "Certificate issuance failed for $domain" | \
mail -s "DNS-01 Certificate Failure" ops@company.com
fi
# Rate limit: space out requests
sleep 5
done < "$DOMAINS_FILE"
echo "=== DNS-01 Certificate Automation Completed: $(date) ==="

Domains Configuration (/etc/ssl-automation/domains.conf)

example.com|example-wildcard|*.example.com,www.example.com
api.company.com|api-wildcard|*.api.company.com
internal.corp.net|internal-services|*.internal.corp.net,vpn.internal.corp.net

Terraform DNS Provider Integration

# Terraform configuration for DNS-01 prerequisites
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
resource "cloudflare_zone" "example" {
zone = "example.com"
}
# API token for Certbot with limited scope
resource "cloudflare_api_token" "certbot_dns" {
name = "Certbot DNS-01 Challenge"
policy {
permission_groups = [
data.cloudflare_api_token_permission_groups.all.zone["DNS Write"],
]
resources = {
"com.cloudflare.api.account.zone.${cloudflare_zone.example.id}" = "*"
}
}
}
output "certbot_dns_token" {
value = cloudflare_api_token.certbot_dns.value
sensitive = true
}

Ansible Playbook for Multi-Server Deployment

---
- name: Issue certificates via DNS-01 across environments
hosts: certificate_servers
become: yes
vars:
ssl_certificates:
- name: production-wildcard
domain: "*.prod.example.com"
sans: "prod.example.com"
env: production
- name: staging-wildcard
domain: "*.staging.example.com"
sans: "staging.example.com"
env: staging
tasks:
- name: Install Certbot DNS plugin
apt:
name: python3-certbot-dns-cloudflare
state: present
- name: Deploy DNS credentials
template:
src: cloudflare-credentials.ini.j2
dest: /etc/letsencrypt/cloudflare/credentials.ini
mode: '0600'
owner: root
group: root
- name: Issue certificates
command: >
certbot certonly
--dns-cloudflare
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini
--dns-cloudflare-propagation-seconds 30
-d {{ item.domain }}
-d {{ item.sans }}
--cert-name {{ item.name }}
--non-interactive
--agree-tos
--email ops@example.com
loop: "{{ ssl_certificates }}"
when: inventory_hostname == groups['certificate_servers'][0]
- name: Deploy certificates to application servers
synchronize:
src: "/etc/letsencrypt/live/{{ item.name }}/"
dest: "/etc/ssl/{{ item.name }}/"
mode: push
delegate_to: "{{ groups['certificate_servers'][0] }}"
loop: "{{ ssl_certificates }}"

ClusterIssuer with DNS-01

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns01
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: k8s-certs@example.com
privateKeySecretRef:
name: letsencrypt-dns01-account-key
solvers:
# Cloudflare DNS-01 solver
- dns01:
cloudflare:
email: cloudflare-account@example.com
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- 'example.com'
- '*.example.com'
# Route53 DNS-01 solver for different domain
- dns01:
route53:
region: us-east-1
hostedZoneID: Z1234567890ABC
selector:
dnsZones:
- 'aws-hosted.example.com'

Wildcard Certificate Resource

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-com
namespace: default
spec:
secretName: wildcard-example-com-tls
issuerRef:
name: letsencrypt-dns01
kind: ClusterIssuer
dnsNames:
- '*.example.com'
- 'example.com'

Problem: ACME validation fails because DNS changes haven’t propagated globally

Terminal window
# WRONG - insufficient propagation wait time
certbot certonly --dns-cloudflare \
--dns-cloudflare-propagation-seconds 10 # Too short!
# Error: Incorrect TXT record

Solution: Verify DNS propagation before validation

Terminal window
# Check DNS propagation across multiple nameservers
dig TXT _acme-challenge.example.com @8.8.8.8 # Google DNS
dig TXT _acme-challenge.example.com @1.1.1.1 # Cloudflare DNS
dig TXT _acme-challenge.example.com @208.67.222.222 # OpenDNS
# Use appropriate propagation delay for your DNS provider
# Cloudflare: 20-30 seconds
# Route53: 30-60 seconds
# Traditional DNS: 60-120 seconds
certbot certonly --dns-cloudflare \
--dns-cloudflare-propagation-seconds 60 # Safe default

DNS Propagation Check Script

#!/bin/bash
RECORD="_acme-challenge.example.com"
EXPECTED_VALUE="validation-token"
NAMESERVERS=("8.8.8.8" "1.1.1.1" "208.67.222.222")
for ns in "${NAMESERVERS[@]}"; do
result=$(dig +short TXT "$RECORD" @"$ns")
if [ "$result" == "\"$EXPECTED_VALUE\"" ]; then
echo "✓ Propagated to $ns"
else
echo "✗ Not yet on $ns (got: $result)"
fi
done

2. DNS Plugin Installation and Version Conflicts

Section titled “2. DNS Plugin Installation and Version Conflicts”

Problem: Missing or incompatible DNS plugin versions

Terminal window
# Check installed plugins
certbot plugins
# Output: No DNS plugins found
# Common issue: Plugin not installed
sudo certbot certonly --dns-cloudflare ...
# Error: certbot: error: unrecognized arguments: --dns-cloudflare

Solution: Install and verify DNS plugins

Terminal window
# Install specific plugin
sudo apt install python3-certbot-dns-cloudflare
# Or via pip (for latest version)
pip install --upgrade pip
pip install certbot-dns-cloudflare --force-reinstall
# Verify plugin is available
certbot plugins | grep cloudflare
# Output: dns-cloudflare
# Check plugin version
pip show certbot-dns-cloudflare

Version Compatibility Matrix

Certbot 2.x → certbot-dns-* 2.x
Certbot 1.x → certbot-dns-* 1.x
Never mix major versions

3. DNS API Permission and Credential Issues

Section titled “3. DNS API Permission and Credential Issues”

Problem: DNS API credentials lack necessary permissions

Terminal window
# Error messages indicating permission issues:
# - "Authentication failed"
# - "Forbidden: You do not have permission"
# - "Access denied to zone"

Solution: Verify and scope DNS API permissions correctly

Cloudflare API Token Permissions:

Required:
- Zone:DNS:Edit (for specific zones)
Optional but recommended:
- Zone:Zone:Read (to list zones)

AWS Route53 IAM Policy (Minimum permissions):

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:GetChange",
"route53:ListHostedZones"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/ZXXXXX"
}
]
}

Credential File Security:

Terminal window
# WRONG - world-readable credentials
chmod 644 /etc/letsencrypt/dns-credentials.ini
# Security risk: Any user can read DNS API credentials
# CORRECT - restricted permissions
chmod 600 /etc/letsencrypt/dns-credentials.ini
chown root:root /etc/letsencrypt/dns-credentials.ini
# Verify permissions
ls -la /etc/letsencrypt/dns-credentials.ini
# -rw------- 1 root root ... dns-credentials.ini

Problem: DNS provider API rate limits exceeded

Terminal window
# Cloudflare: 1200 requests/5 minutes per zone
# Route53: 5 API requests/second (steady state)
# Google Cloud DNS: 400 write requests/minute per project

Solution: Implement rate limiting and backoff

#!/bin/bash
# Rate-limited certificate issuance
DOMAINS=("site1.example.com" "site2.example.com" "site3.example.com")
DELAY_BETWEEN_REQUESTS=10 # seconds
for domain in "${DOMAINS[@]}"; do
echo "Issuing certificate for $domain"
certbot certonly --dns-cloudflare ... -d "$domain"
# Wait between requests to avoid rate limits
sleep $DELAY_BETWEEN_REQUESTS
done

Problem: Internal DNS returns different TXT records than external DNS

Scenario:

  • Internal DNS: Used by internal applications
  • External DNS: Authoritative for internet
  • ACME CA validates against external DNS
  • Internal systems may see stale or different records

Solution: Ensure ACME challenge records propagate to external DNS

Terminal window
# Verify external DNS resolution
dig TXT _acme-challenge.example.com @8.8.8.8 # External resolver
# If using split-horizon, ensure challenge records exist in BOTH:
# 1. External zone (for ACME validation)
# 2. Internal zone (for consistency)
# Or configure internal DNS to forward _acme-challenge queries externally

Problem: Exceeding Let’s Encrypt rate limits during testing

Rate Limits (per domain per week):
- 50 certificates per registered domain
- 5 duplicate certificates (same exact SANs)

Solution: Use staging environment for testing

Terminal window
# WRONG - testing against production
for i in {1..10}; do
certbot certonly --dns-cloudflare -d test$i.example.com
done
# Risk: Approaching rate limit of 50 certs/week
# CORRECT - test with staging first
certbot certonly \
--staging \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d test.example.com
# Staging server URL (manual specification)
--server https://acme-staging-v02.api.letsencrypt.org/directory
# Once validated, switch to production
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d production.example.com

Problem: Misunderstanding wildcard certificate scope

Terminal window
# Common misconception:
# "*.example.com" covers both "example.com" AND all subdomains
# Reality:
# "*.example.com" covers: foo.example.com, bar.example.com
# "*.example.com" does NOT cover: example.com (apex domain)

Solution: Explicitly request both wildcard and apex

Terminal window
# WRONG - apex domain not covered
certbot certonly --dns-cloudflare -d *.example.com
# CORRECT - explicitly include both
certbot certonly --dns-cloudflare \
-d example.com \ # Apex domain
-d *.example.com # Wildcard for all subdomains
# Also works for multiple levels
certbot certonly --dns-cloudflare \
-d api.example.com \
-d *.api.example.com # Covers foo.api.example.com but not api.example.com

Principle of Least Privilege for DNS API Access

Terminal window
# WRONG - global API key with full account access
dns_cloudflare_api_key = your-global-api-key
# CORRECT - scoped API token for specific zones only
dns_cloudflare_api_token = token-with-dns-edit-for-example-com-only

Cloudflare Scoped Token:

  1. Create token with ONLY Zone:DNS:Edit permission
  2. Scope to specific zones: example.com, api.example.com
  3. Set IP restrictions if possible (certificate server IPs)
  4. Set expiration date (rotate annually)

AWS Route53 Resource-Based Policy:

{
"Resource": "arn:aws:route53:::hostedzone/Z123SPECIFICZONE"
}

Credential Rotation

Terminal window
# Rotate DNS API credentials quarterly
# 1. Generate new API token
# 2. Update credential files
# 3. Test certificate renewal
# 4. Revoke old token
# 5. Document rotation in audit log

Audit Trail for DNS Changes

Terminal window
# Log all DNS-01 operations
logger -t certbot-dns01 "Certificate requested for $DOMAIN by $USER"
# Enable DNS provider audit logging
# Cloudflare: Audit Logs in Dashboard
# Route53: CloudTrail for route53:ChangeResourceRecordSets
# Azure DNS: Activity Log for Microsoft.Network/dnszones/TXT/write

Robust Renewal Script with Error Handling

#!/bin/bash
set -euo pipefail
# Production DNS-01 renewal automation
LOG_FILE="/var/log/certbot/dns01-renewal-$(date +%Y%m%d).log"
ERROR_EMAIL="ops@example.com"
SUCCESS_WEBHOOK="https://monitoring.example.com/webhook/cert-renewal"
exec > >(tee -a "$LOG_FILE") 2>&1
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
log "Starting DNS-01 certificate renewal"
# Pre-flight checks
if ! command -v certbot &> /dev/null; then
log "ERROR: certbot not found"
exit 1
fi
if ! certbot plugins | grep -q dns-cloudflare; then
log "ERROR: dns-cloudflare plugin not installed"
exit 1
fi
if [ ! -f /etc/letsencrypt/cloudflare/credentials.ini ]; then
log "ERROR: DNS credentials file missing"
exit 1
fi
# Attempt renewal
if certbot renew \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
--deploy-hook "/usr/local/bin/certificate-deploy.sh" \
--quiet; then
log "Renewal successful"
# Notify monitoring system
curl -X POST "$SUCCESS_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"status\":\"success\",\"timestamp\":\"$(date -Iseconds)\"}"
else
log "ERROR: Renewal failed"
# Send alert email
echo "DNS-01 certificate renewal failed. Check logs: $LOG_FILE" | \
mail -s "ALERT: Certificate Renewal Failed" "$ERROR_EMAIL"
# Page on-call engineer
curl -X POST "https://pagerduty.example.com/api/incidents" \
-H "Authorization: Token token=$PAGERDUTY_TOKEN" \
-d "{\"incident\":{\"type\":\"incident\",\"title\":\"DNS-01 renewal failure\"}}"
exit 1
fi
log "DNS-01 renewal completed successfully"

Idempotent Renewal Automation

Terminal window
# Design renewals to be idempotent (safe to run multiple times)
# Certbot automatically skips certificates >30 days from expiry
# Safe to run frequently
0 */12 * * * /usr/local/bin/certbot-dns01-renewal.sh

Certificate Expiration Monitoring

#!/bin/bash
# Monitor certificate expiration
DOMAINS=(
"example.com"
"*.example.com"
"api.example.com"
)
for domain in "${DOMAINS[@]}"; do
cert_path="/etc/letsencrypt/live/${domain/\*./wildcard.}/cert.pem"
if [ -f "$cert_path" ]; then
expiry=$(openssl x509 -in "$cert_path" -noout -enddate | cut -d= -f2)
expiry_epoch=$(date -d "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( ($expiry_epoch - $now_epoch) / 86400 ))
echo "$domain: $days_left days until expiry"
if [ $days_left -lt 30 ]; then
echo "WARNING: $domain expires in $days_left days"
# Trigger alert
fi
else
echo "WARNING: Certificate not found for $domain"
fi
done

Prometheus Metrics Export

Terminal window
# Export certificate metrics for Prometheus
cat > /var/lib/node_exporter/textfile_collector/certificates.prom << EOF
# HELP ssl_certificate_expiry_days Days until SSL certificate expires
# TYPE ssl_certificate_expiry_days gauge
ssl_certificate_expiry_days{domain="example.com",type="dns01"} 89
ssl_certificate_expiry_days{domain="*.example.com",type="dns01"} 89
EOF

Grafana Dashboard Queries

# Alert when certificates expire within 7 days
ssl_certificate_expiry_days{type="dns01"} < 7
# Renewal success rate
rate(certbot_renewal_success_total[1h]) /
rate(certbot_renewal_attempts_total[1h])

Evaluation Matrix:

ProviderAPI QualityPropagation SpeedRate LimitsCostEnterprise Features
CloudflareExcellent15-30s1200 req/5minFreeDNSSEC, API tokens
Route53Excellent30-60s5 req/s~$0.50/zone/moIAM integration
Google DNSGood30-60s400 req/min$0.20/zone/moGCP integration
Azure DNSGood60-120s500 req/5min$0.50/zone/moAD integration
Traditional DNSVaries120-300sVariesVariesOften limited APIs

Selection Criteria:

  1. API Reliability: 99.9%+ uptime for API endpoints
  2. Propagation Speed: < 60 seconds preferred for automated workflows
  3. Rate Limits: Support for expected certificate volume
  4. Security Features: API token scoping, audit logs, DNSSEC
  5. Cost: Free tier availability for small deployments

Multi-Provider Strategy:

Terminal window
# Primary: Cloudflare (fast, reliable)
certbot certonly --dns-cloudflare -d primary.example.com
# Backup: Route53 (if Cloudflare unavailable)
certbot certonly --dns-route53 -d backup.example.com
# Document failover procedures in runbook

1. Test in Staging Environment First

Terminal window
# Stage 1: Staging server with Let's Encrypt staging
certbot certonly \
--staging \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials-staging.ini \
-d staging.example.com
# Stage 2: Production server with Let's Encrypt staging (validate DNS setup)
certbot certonly \
--staging \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d production.example.com
# Stage 3: Production server with Let's Encrypt production
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d production.example.com

2. Gradual Rollout to Non-Production First

Terminal window
# Week 1: Development environment
# Week 2: Staging environment
# Week 3: Production (canary - 10% of domains)
# Week 4: Production (full rollout)

3. Rollback Plan

#!/bin/bash
# Certificate rollback procedure
DOMAIN="$1"
BACKUP_DIR="/etc/letsencrypt/backup-$(date +%Y%m%d)"
# Backup current certificate before renewal
cp -r /etc/letsencrypt/live/$DOMAIN $BACKUP_DIR/
# If renewal fails or causes issues, restore:
# cp -r $BACKUP_DIR/$DOMAIN /etc/letsencrypt/live/
# systemctl reload nginx

4. Documentation and Runbooks

# DNS-01 Renewal Runbook
## Normal Operations
- Automated renewal via cron (daily at 2 AM)
- DNS-01 challenge via Cloudflare API
- Certificates deployed to /etc/letsencrypt/live/
- Services auto-reloaded via deploy hooks
## Manual Renewal (Emergency)
1. SSH to cert-server.example.com
2. Run: sudo /usr/local/bin/certbot-dns01-manual.sh example.com
3. Verify DNS propagation: dig TXT _acme-challenge.example.com @8.8.8.8
4. Validate certificate: openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -text
## Troubleshooting
### DNS Propagation Issues
- Check Cloudflare DNS dashboard for TXT records
- Query multiple nameservers (8.8.8.8, 1.1.1.1, 208.67.222.222)
- Increase --dns-cloudflare-propagation-seconds to 120
### API Authentication Failures
- Verify API token in /etc/letsencrypt/cloudflare/credentials.ini
- Check token permissions in Cloudflare dashboard
- Rotate token if compromised
### Rate Limit Exceeded
- Wait 1 week for rate limit reset
- Use staging server for testing
- Review automation frequency
## Escalation
- Primary: ops@example.com
- Secondary: Platform team Slack channel
- PagerDuty: DNS-01 renewal failure alerts

Before deploying DNS-01 automation to production:

  • Select DNS provider with reliable API and fast propagation
  • Create scoped API credentials (minimum required permissions)
  • Install appropriate Certbot DNS plugin or acme.sh
  • Test manual DNS-01 challenge with single domain
  • Verify DNS propagation across multiple nameservers
  • Test wildcard certificate issuance (if needed)
  • Configure appropriate propagation delay for DNS provider
  • Secure credential files (chmod 600, root ownership)
  • Implement automated renewal with error handling
  • Set up monitoring for certificate expiration
  • Document DNS provider API limits and behavior
  • Create rollback procedures for failed renewals
  • Test renewal in staging environment first
  • Configure alerting for renewal failures
  • Document emergency manual renewal procedures
  • Implement credential rotation schedule
  • Enable DNS provider audit logging
  • Test rate limiting behavior
  • Validate certificates after issuance
  • Update runbook with DNS-01 specific troubleshooting

ACME Operations:

Broader Operations:

Implementation:

Protocol:

Troubleshooting:


This comprehensive guide provides production-ready DNS-01 challenge implementation patterns for wildcard certificates, private networks, and enterprise automation scenarios across diverse DNS providers and infrastructure environments.