Automated SSL with Let us Encrypt and Cloudflare DNS Challenge
Set up fully automated SSL certificates using Let us Encrypt with Cloudflare DNS validation. Covers wildcard certificates, Traefik integration, certbot...
Why DNS Challenge?
The standard HTTP challenge requires port 80 to be publicly accessible. The DNS challenge validates domain ownership by creating a DNS TXT record — no open ports needed. This is perfect for:
<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 150" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="150" rx="12" fill="#1a1a2e"/><rect x="30" y="40" width="100" height="55" rx="6" fill="none" stroke="#3b82f6" stroke-width="1.5"/><text x="80" y="60" text-anchor="middle" fill="#3b82f6" font-size="10" font-family="monospace">Hello World</text><text x="80" y="80" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">Plaintext</text><rect x="175" y="30" width="90" height="75" rx="8" fill="#6366f1" opacity="0.85"/><text x="220" y="55" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Encrypt</text><text x="220" y="72" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">AES-256</text><text x="220" y="92" text-anchor="middle" fill="#f59e0b" font-size="20" font-family="system-ui">🔑</text><rect x="310" y="40" width="100" height="55" rx="6" fill="none" stroke="#a855f7" stroke-width="1.5"/><text x="360" y="60" text-anchor="middle" fill="#a855f7" font-size="10" font-family="monospace">x8f2...k9z</text><text x="360" y="80" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">Ciphertext</text><rect x="455" y="30" width="90" height="75" rx="8" fill="#2dd4bf" opacity="0.85"/><text x="500" y="55" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Decrypt</text><text x="500" y="72" text-anchor="middle" fill="#1a1a2e" font-size="9" font-family="system-ui">AES-256</text><text x="500" y="92" text-anchor="middle" fill="#f59e0b" font-size="20" font-family="system-ui">🔑</text><defs><marker id="arrow6" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="132" y1="67" x2="173" y2="67" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow6)"/><line x1="267" y1="67" x2="308" y2="67" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow6)"/><line x1="412" y1="67" x2="453" y2="67" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow6)"/><text x="300" y="130" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui">Symmetric Encryption: same key encrypts and decrypts</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Encryption transforms readable plaintext into unreadable ciphertext, reversible only with the correct key.</p></div>
Method 1: Traefik + Cloudflare (Recommended)
If you use Traefik as your reverse proxy, SSL is automatic:
# docker-compose.yml
services:
traefik:
image: traefik:v3
command:
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.cloudflare.acme.dnschallenge=true"
- "--certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare"
- "[email protected]"
- "--certificatesresolvers.cloudflare.acme.storage=/letsencrypt/acme.json"
- "--providers.docker=true"
environment:
CF_DNS_API_TOKEN: your-cloudflare-api-token
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencryptThen any service with Docker labels automatically gets SSL:
services:
myapp:
image: myapp:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
- "traefik.http.routers.myapp.tls.certresolver=cloudflare"Wildcard Certificate
labels:
- "traefik.http.routers.wildcard.rule=HostRegexp(`{subdomain:.+}.example.com`)"
- "traefik.http.routers.wildcard.tls.certresolver=cloudflare"
- "traefik.http.routers.wildcard.tls.domains[0].main=example.com"
- "traefik.http.routers.wildcard.tls.domains[0].sans=*.example.com"Method 2: Certbot + Cloudflare Plugin
For non-Traefik setups, use certbot with the Cloudflare DNS plugin:
# Install certbot and Cloudflare plugin
sudo apt install certbot python3-certbot-dns-cloudflare
# Create Cloudflare credentials file
cat > /etc/letsencrypt/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = your-cloudflare-api-token
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini
# Obtain a certificate
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d example.com \
-d "*.example.com" \
--preferred-challenges dns-01
# Auto-renewal is set up automatically via systemd timer
systemctl list-timers | grep certbotRenewal Hook (Reload Services)
# /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh
#!/bin/bash
systemctl reload nginx
# Or for Docker:
docker kill --signal=HUP traefik<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 170" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="170" rx="12" fill="#1a1a2e"/><text x="60" y="30" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui">Internet</text><circle cx="60" cy="60" r="25" fill="none" stroke="#3b82f6" stroke-width="1.5"/><text x="60" y="57" text-anchor="middle" fill="#3b82f6" font-size="18" font-family="system-ui">🌐</text><rect x="155" y="25" width="120" height="120" rx="10" fill="#6366f1" opacity="0.15"/><rect x="155" y="25" width="120" height="120" rx="10" fill="none" stroke="#6366f1" stroke-width="1.5"/><text x="215" y="50" text-anchor="middle" fill="#6366f1" font-size="11" font-family="system-ui" font-weight="bold">Reverse</text><text x="215" y="65" text-anchor="middle" fill="#6366f1" font-size="11" font-family="system-ui" font-weight="bold">Proxy</text><text x="215" y="85" text-anchor="middle" fill="#94a3b8" font-size="8" font-family="system-ui">TLS termination</text><text x="215" y="98" text-anchor="middle" fill="#94a3b8" font-size="8" font-family="system-ui">Load balancing</text><text x="215" y="111" text-anchor="middle" fill="#94a3b8" font-size="8" font-family="system-ui">Path routing</text><text x="215" y="124" text-anchor="middle" fill="#94a3b8" font-size="8" font-family="system-ui">Rate limiting</text><rect x="350" y="20" width="110" height="35" rx="6" fill="#2dd4bf" opacity="0.8"/><text x="405" y="42" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">app.example.com</text><rect x="350" y="65" width="110" height="35" rx="6" fill="#a855f7" opacity="0.8"/><text x="405" y="87" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">api.example.com</text><rect x="350" y="110" width="110" height="35" rx="6" fill="#f59e0b" opacity="0.8"/><text x="405" y="132" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">cdn.example.com</text><defs><marker id="arrow11" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="87" y1="60" x2="153" y2="75" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow11)"/><line x1="277" y1="55" x2="348" y2="37" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow11)"/><line x1="277" y1="85" x2="348" y2="82" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow11)"/><line x1="277" y1="115" x2="348" y2="127" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow11)"/><text x="120" y="55" text-anchor="middle" fill="#2dd4bf" font-size="8" font-family="system-ui">HTTPS</text><text x="505" y="42" text-anchor="start" fill="#94a3b8" font-size="8" font-family="system-ui">:3000</text><text x="505" y="87" text-anchor="start" fill="#94a3b8" font-size="8" font-family="system-ui">:8080</text><text x="505" y="132" text-anchor="start" fill="#94a3b8" font-size="8" font-family="system-ui">:9000</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">A reverse proxy terminates TLS, routes requests by hostname, and load-balances across backend services.</p></div>
Method 3: acme.sh
# Install acme.sh
curl https://get.acme.sh | sh
# Set Cloudflare API token
export CF_Token="your-cloudflare-api-token"
# Issue certificate
acme.sh --issue --dns dns_cf -d example.com -d "*.example.com"
# Install certificate to a specific location
acme.sh --install-cert -d example.com \
--key-file /etc/ssl/private/example.com.key \
--fullchain-file /etc/ssl/certs/example.com.crt \
--reloadcmd "systemctl reload nginx"Cloudflare API Token Setup
Create a scoped API token (not the global API key):
1. Go to Cloudflare Dashboard > My Profile > API Tokens 2. Click "Create Token" 3. Use the "Edit zone DNS" template 4. Scope to your specific zone 5. Copy the token
Required permissions:
Certificate Monitoring
Check Certificate Expiry
#!/bin/bash
# check-certs.sh
DOMAINS="example.com api.example.com app.example.com"
for domain in \$DOMAINS; do
EXPIRY=\$(echo | openssl s_client -servername "\$domain" -connect "\$domain:443" 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
DAYS_LEFT=\$(( (\$(date -d "\$EXPIRY" +%s) - \$(date +%s)) / 86400 ))
if [ "\$DAYS_LEFT" -lt 14 ]; then
echo "WARNING: \$domain expires in \$DAYS_LEFT days (\$EXPIRY)"
else
echo "OK: \$domain - \$DAYS_LEFT days remaining"
fi
doneMonitor with Uptime Kuma
Add HTTPS monitors in Uptime Kuma with "Certificate Expiry" notification enabled. It will alert you when certificates are within 14 days of expiry.
<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 220" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="220" rx="12" fill="#1a1a2e"/><path d="M300,25 L380,55 L380,120 Q380,170 300,195 Q220,170 220,120 L220,55 Z" fill="none" stroke="#6366f1" stroke-width="2.5"/><path d="M300,40 L365,65 L365,118 Q365,160 300,180 Q235,160 235,118 L235,65 Z" fill="#6366f1" opacity="0.15"/><rect x="280" y="95" width="40" height="30" rx="4" fill="#6366f1" opacity="0.9"/><path d="M288,95 L288,82 Q288,72 300,72 Q312,72 312,82 L312,95" fill="none" stroke="#6366f1" stroke-width="2.5"/><circle cx="300" cy="110" r="4" fill="#ffffff"/><text x="90" y="60" text-anchor="middle" fill="#3b82f6" font-size="10" font-family="system-ui">Firewall</text><line x1="130" y1="57" x2="218" y2="57" stroke="#3b82f6" stroke-width="1" stroke-dasharray="3,3"/><text x="90" y="100" text-anchor="middle" fill="#a855f7" font-size="10" font-family="system-ui">WAF</text><line x1="110" y1="97" x2="220" y2="85" stroke="#a855f7" stroke-width="1" stroke-dasharray="3,3"/><text x="90" y="140" text-anchor="middle" fill="#2dd4bf" font-size="10" font-family="system-ui">SSO / MFA</text><line x1="130" y1="137" x2="222" y2="120" stroke="#2dd4bf" stroke-width="1" stroke-dasharray="3,3"/><text x="510" y="60" text-anchor="middle" fill="#f59e0b" font-size="10" font-family="system-ui">TLS/SSL</text><line x1="470" y1="57" x2="382" y2="57" stroke="#f59e0b" stroke-width="1" stroke-dasharray="3,3"/><text x="510" y="100" text-anchor="middle" fill="#3b82f6" font-size="10" font-family="system-ui">RBAC</text><line x1="490" y1="97" x2="380" y2="85" stroke="#3b82f6" stroke-width="1" stroke-dasharray="3,3"/><text x="510" y="140" text-anchor="middle" fill="#a855f7" font-size="10" font-family="system-ui">Audit Logs</text><line x1="470" y1="137" x2="378" y2="120" stroke="#a855f7" stroke-width="1" stroke-dasharray="3,3"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Defense in depth: multiple security layers protect your infrastructure from threats.</p></div>
Security Headers
Once you have HTTPS, add security headers:
# Traefik middleware
http:
middlewares:
security-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
contentTypeNosniff: true
browserXssFilter: true
frameDeny: true
referrerPolicy: "strict-origin-when-cross-origin"At TechSaaS, we use Traefik with Cloudflare DNS challenge for all our certificates. Combined with Cloudflare Tunnel, our servers have zero open ports while still serving HTTPS for every service. Certificate provisioning and renewal is fully automatic.
Need help with SSL and security? Contact [email protected].
Need help with security?
TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.