Key Takeaways
- Wildcard requires DNS-01: HTTP-01 challenge can’t prove DNS control — you must add a TXT record via DNS API.
- Request both apex and wildcard:
-d example.com -d "*.example.com"— one cert covers both. - Cloudflare API token, not global key: Create a scoped token with Zone:DNS:Edit only — principle of least privilege.
- Auto-renewal is automatic: Certbot’s systemd timer renews ~30 days before expiry. Test with
--dry-run.
Introduction
Direct Answer: How do I get a wildcard SSL certificate from Let’s Encrypt using Certbot in 2026?
Wildcard certificates require DNS-01 challenge. For Cloudflare DNS: install pip install certbot certbot-dns-cloudflare --break-system-packages. Create a Cloudflare API token at dash.cloudflare.com → My Profile → API Tokens → Custom Token with Zone:DNS:Edit permission. Save to ~/.secrets/cloudflare.ini: dns_cloudflare_api_token = YOUR_TOKEN. Set permissions: chmod 600 ~/.secrets/cloudflare.ini. Issue certificate: sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/cloudflare.ini -d example.com -d "*.example.com" --agree-tos --email [email protected]. Certificate saves to /etc/letsencrypt/live/example.com/. Test renewal: sudo certbot renew --dry-run.
Certificate Lifecycle Timeline
DAY 0 — Issuance
├─ 00:00 You run: sudo certbot certonly --dns-cloudflare ...
├─ 00:01 Certbot contacts Let's Encrypt ACME server
├─ 00:02 ACME server: "Prove you own this domain. Add DNS TXT record..."
├─ 00:03 Certbot adds: _acme-challenge.example.com TXT "..." via Cloudflare API
├─ 00:04 Wait for DNS propagation (--dns-cloudflare-propagation-seconds 30)
├─ 00:34 ACME server verifies TXT record exists
├─ 00:35 "Validation successful!" Certificate issued
└─ 00:36 Certificate saved: /etc/letsencrypt/live/example.com/fullchain.pem
(90 days pass)
DAY 60 — Renewal Period Starts
├─ Certbot watches certificate expiry
├─ When < 30 days remain: automatic renewal triggers
├─ Repeat steps from ACME challenge through validation
└─ New certificate generated, old one moved to archive
DAY 90 — Certificate Expires (if not renewed)
├─ Browsers: "Certificate expired" warning ⚠️
├─ Users see: red lock icon, "Not Secure"
├─ Service unavailable for HTTPS-only clients
└─ Urgent: renew immediately
DAY 91+ — Expired Certificate
└─ ❌ HTTPS broken, users blocked, SEO penalized
---
RENEWAL AUTOMATION (Recommended):
Place in crontab or systemd timer:
0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
This runs daily at 3 AM (off-peak):
- Checks: is renewal needed? (< 30 days until expiry?)
- If yes: attempts renewal
- If success: reloads Nginx with new cert
- If fail: sends email to [email protected]
Part 1: Cloudflare DNS — Certbot Plugin
# Install Certbot and the Cloudflare DNS plugin
# Why these packages?
# - certbot: ACME client that handles Let's Encrypt certificate lifecycle
# - certbot-dns-cloudflare: Allows Certbot to programmatically update DNS via Cloudflare API
# - python3-certbot: Python bindings for system integration
sudo apt-get update
sudo apt-get install -y certbot python3-certbot python3-pip
pip install certbot-dns-cloudflare --break-system-packages
# === Create Cloudflare API token ===
# Steps:
# 1. Login to dash.cloudflare.com
# 2. Go to "My Profile" → "API Tokens" → "Create Token"
# 3. Choose template: "Edit zone DNS"
# 4. In Zone Resources: Select "Include" → pick your domain
# 5. Copy the token string (you'll only see it once!)
# Store API credentials securely (readable only by you)
mkdir -p ~/.secrets
cat > ~/.secrets/cloudflare.ini << 'EOF'
# Cloudflare API token for automated DNS updates during ACME challenges
dns_cloudflare_api_token = your_cloudflare_api_token_here
EOF
# Restrict file permissions (600 = rw for owner only)
# This is critical: if token is world-readable, attackers can modify your DNS!
chmod 600 ~/.secrets/cloudflare.ini
# === Issue wildcard certificate ===
# Breaking down the command:
# --dns-cloudflare: Use Cloudflare DNS plugin (handles DNS-01 challenge)
# --dns-cloudflare-credentials: Path to API credentials file
# --dns-cloudflare-propagation-seconds 30: Wait 30s for DNS to propagate before validation
# -d example.com -d "*.example.com": Issue cert for both apex and wildcard
# --agree-tos: Auto-accept Let's Encrypt ToS (don't prompt)
# --non-interactive: Don't wait for user input
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
--dns-cloudflare-propagation-seconds 30 \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email [email protected] \
--non-interactive
# Result: Certificate saved to /etc/letsencrypt/live/example.com/fullchain.pem
Expected output:
Requesting a certificate for example.com and *.example.com
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem
Why wildcard certificates require DNS-01 challenge:
Let’s Encrypt supports two ACME challenges:
| Challenge Type | Method | Domain Types | Security |
|---|---|---|---|
| HTTP-01 | Certbot creates temp file at .well-known/acme-challenge/ on your web server | Single domains only (example.com) | Anyone with web server access can get certs |
| DNS-01 | Certbot adds temporary TXT record via DNS API | Wildcard domains (*.example.com) + single | Only those with DNS API token can get certs |
Why wildcard requires DNS-01:
- A wildcard cert (*.example.com) covers infinite subdomains: api.example.com, admin.example.com, test.example.com, etc.
- Let’s Encrypt can’t validate HTTP-01 for “all subdomains” — there’s no single .well-known file location
- DNS-01 proves domain ownership by adding a unique TXT record — the API token proves DNS control, which implies domain ownership
Developer gotcha: If you lose the DNS API token:
# Renew fails silently if API token is missing or invalid
sudo certbot renew --dry-run # Fails with cryptic DNS error
# Solution: Update credentials file
sudo vim ~/.secrets/cloudflare.ini # Add new token
sudo certbot renew # Works again
Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem This certificate expires on 2026-07-30. These files will be updated when the certificate renews.
```bash
# Verify certificate covers wildcard
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -text -noout | \
grep -A2 "Subject Alternative Name"
Expected output:
X509v3 Subject Alternative Name:
DNS:example.com, DNS:*.example.com
Part 2: Hetzner DNS — Certbot Plugin
pip install certbot-dns-hetzner --break-system-packages
# Create Hetzner DNS API token
# → dns.hetzner.com → API Tokens → Create API Token
cat > ~/.secrets/hetzner.ini << 'EOF'
dns_hetzner_api_token = your_hetzner_dns_token_here
EOF
chmod 600 ~/.secrets/hetzner.ini
sudo certbot certonly \
--authenticator dns-hetzner \
--dns-hetzner-credentials ~/.secrets/hetzner.ini \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email [email protected]
Part 3: ACME.sh (Alternative — More DNS Providers)
# Install acme.sh (shell-only, no Python required)
curl https://get.acme.sh | sh -s [email protected]
source ~/.bashrc
# Cloudflare credentials for acme.sh
export CF_Token="your_cloudflare_api_token"
export CF_Account_ID="your_cloudflare_account_id"
# Issue wildcard certificate using Let's Encrypt
~/.acme.sh/acme.sh \
--issue \
--dns dns_cf \
-d example.com \
-d "*.example.com" \
--server letsencrypt
# Install to a target directory for Nginx
~/.acme.sh/acme.sh --install-cert \
-d example.com \
--key-file /etc/ssl/private/example.com.key \
--fullchain-file /etc/ssl/certs/example.com.crt \
--reloadcmd "sudo systemctl reload nginx"
Expected output:
[Sat May 1 10:00:00 UTC 2026] Your cert is in: /root/.acme.sh/example.com_ecc/example.com.cer
[Sat May 1 10:00:00 UTC 2026] Your cert key is in: /root/.acme.sh/example.com_ecc/example.com.key
[Sat May 1 10:00:00 UTC 2026] The intermediate CA cert is in: /root/.acme.sh/example.com_ecc/ca.cer
[Sat May 1 10:00:00 UTC 2026] And the full chain certs is in: /root/.acme.sh/example.com_ecc/fullchain.cer
Part 4: Use the Wildcard Certificate in Nginx
# /etc/nginx/sites-available/wildcard
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com app.example.com api.example.com;
# Single wildcard cert covers all subdomains
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Route by server_name
location / {
if ($host = app.example.com) {
proxy_pass http://127.0.0.1:3000;
}
if ($host = api.example.com) {
proxy_pass http://127.0.0.1:4000;
}
# Default: serve static site
root /var/www/html;
index index.html;
}
}
server {
listen 80;
server_name example.com *.example.com;
return 301 https://$host$request_uri;
}
sudo nginx -t && sudo systemctl reload nginx
curl -sI https://app.example.com | grep -E "HTTP|Strict"
Part 5: Verify Auto-Renewal
# Test renewal without making changes
sudo certbot renew --dry-run
Expected output:
Simulating renewal of an existing certificate for example.com and *.example.com
Congratulations, all simulated renewals succeeded
# Check systemd timer (auto-renewal on Ubuntu)
systemctl list-timers | grep certbot
Expected output:
Mon 2026-07-28 02:00:00 UTC 88 days left snap.certbot.renew.timer
Part 6: Self-Hosted CA with step-ca (Internal Services)
# For internal services: use your own CA instead of Let's Encrypt
# No DNS challenge needed — full control over certificate issuance
sudo apt-get install -y step-cli step-ca
# Initialise your private CA
step ca init \
--name "Vucense Internal CA" \
--dns "ca.internal.example.com" \
--address ":443" \
--provisioner [email protected]
# Start the CA server
step-ca ~/.step/config/ca.json &
# Issue a certificate from your internal CA
step ca certificate "*.internal.example.com" server.crt server.key \
--ca-url https://ca.internal.example.com \
--san "*.internal.example.com"
echo "Internal wildcard cert issued — valid for 24h (auto-renewed by step)"
DNS Propagation Checklist (Critical for DNS-01 Challenge)
Before running certbot certonly, DNS must be propagated globally. Here’s how to verify:
Checklist:
Before certification:
□ 1. Have you created Cloudflare API token? (dash.cloudflare.com → API Tokens)
□ 2. Have you saved token to ~/.secrets/cloudflare.ini?
□ 3. Have you set chmod 600 ~/.secrets/cloudflare.ini? (security critical!)
□ 4. Have you verified domain is in Cloudflare? (dashboard → Websites)
□ 5. Have you set --dns-cloudflare-propagation-seconds to 60+ (not default 30)?
During certification:
□ 6. Watch Certbot output: "Adding DNS TXT record..."
□ 7. Verify TXT record exists (wait 5-10 seconds):
dig TXT _acme-challenge.example.com @8.8.8.8
(should show: _acme-challenge.example.com. 300 IN TXT "abcd1234...")
□ 8. Check multiple DNS servers (some are slow to update):
dig TXT _acme-challenge.example.com @1.1.1.1 (Cloudflare)
dig TXT _acme-challenge.example.com @8.8.8.8 (Google)
dig TXT _acme-challenge.example.com @208.67.222.123 (OpenDNS)
□ 9. If any show old/missing record: wait 30s, recheck
After certification:
□ 10. Certificate issued: "Successfully received certificate"
□ 11. TXT record auto-deleted by Certbot (cleanup)
□ 12. Certificate valid: openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates
Common DNS Propagation Errors:
| Error | Cause | Fix |
|---|---|---|
| ”NXDOMAIN looking up TXT” | Record not created yet | Increase propagation-seconds to 60 |
| ”Connection timed out” | DNS server slow | Wait longer, use propagation-seconds 90 |
| ”SERVFAIL” | Internal DNS error at ISP | Try different DNS server (@1.1.1.1 vs @8.8.8.8) |
| “Record exists but old value” | Cache not updated | Wait 30-60 seconds, DNS servers sync slowly |
Troubleshooting
DNS problem: NXDOMAIN looking up TXT for _acme-challenge.example.com
The DNS TXT record wasn’t created or hasn’t propagated.
Fix: Increase --dns-cloudflare-propagation-seconds 60. Check propagation: dig TXT _acme-challenge.example.com @8.8.8.8.
Error: urn:ietf:params:acme:error:rateLimited
Hit Let’s Encrypt’s rate limit (5 duplicate certificates per week, 50 per domain per week).
Fix: Wait for rate limit to reset, or use staging environment for testing: add --staging flag.
Wildcard cert doesn’t cover the apex domain
*.example.com only covers one level of subdomains — not example.com itself.
Fix: Always request both: -d example.com -d "*.example.com".
Certificate Renewal Failures — Debugging Guide
Automatic renewal fails silently if not monitoring. Here’s how to debug:
Step 1: Check renewal status
# Did renewal attempt? When? What error?
sudo certbot renew --dry-run --verbose 2>&1 | tee /tmp/renewal-test.log
# Output should include:
# - "Processing /etc/letsencrypt/renewal/example.com.conf"
# - "Attempting renewal..." if needed
# - "Renewal will succeed" or error details
Step 2: Diagnose common renewal failures
# Is the API token still valid?
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://api.cloudflare.com/client/v4/zones
# If "authentication failed" error → token expired or revoked
# → Generate new token at dash.cloudflare.com → API Tokens
# Is the credentials file readable by Certbot?
sudo ls -la /etc/letsencrypt/renewal/example.com.conf
# Owner should be: root (certbot runs as root via sudo)
# Does the credentials file reference the right API token path?
sudo grep "dns_cloudflare_credentials" /etc/letsencrypt/renewal/example.com.conf
# Should point to: /root/.secrets/cloudflare.ini
Step 3: Manual renewal with debug output
# Run renewal with maximum verbosity
sudo certbot renew --force-renewal -vvv 2>&1 | tee /tmp/renewal-debug.log
# Search log for errors:
grep -i "error\|fail\|denied" /tmp/renewal-debug.log
Renewal Failure Decision Tree
Problem: Renewal fails automatically
│
├─ Error: "No suitable block device found"?
│ └─ Fix: Certbot couldn't create TXT record (DNS API issue)
│ → Check: is DNS API token valid? (curl test above)
│ → Check: did Cloudflare API permissions change?
│ → Regenerate token with "Edit zone DNS" permission
│
├─ Error: "Connection refused" or "timeout"?
│ └─ Fix: Can't reach Cloudflare API
│ → Check: iptables/firewall allowing HTTPS outbound?
│ → Check: is ISP blocking API.cloudflare.com?
│ → Try: manual renewal from different network
│
├─ Error: "certificate not available"?
│ └─ Fix: Certificate file corrupted or deleted
│ → Check: ls -la /etc/letsencrypt/live/example.com/
│ → If missing: restore from backup or force-renew
│
├─ Error: "renewal disabled" or "skipped"?
│ └─ Fix: Renewal actually succeeded, just not scheduled
│ → Check: sudo systemctl status certbot.timer
│ → If inactive: sudo systemctl enable certbot.timer
│ → Manually trigger: sudo systemctl start certbot.service
│
└─ Error: "urn:ietf:params:acme:error:accountDoesNotExist"?
└─ Fix: ACME account expired or Let's Encrypt revoked it
→ Check: sudo cat /etc/letsencrypt/renewal/example.com.conf | grep account
→ If account missing: register new account
→ Run: sudo certbot register -n --agree-tos -m [email protected]
Monitoring renewals
# Check renewal logs regularly
sudo journalctl -u certbot.service --no-pager | tail -50
# Set up log rotation
echo "/var/log/letsencrypt/*.log {
daily
rotate 14
compress
missingok
}" | sudo tee /etc/logrotate.d/letsencrypt
# Alert on renewal failure via cron email
0 3 * * * certbot renew -q || echo "RENEWAL FAILED" | mail -s "SSL Cert Alert" [email protected]
Certbot vs ACME.sh: Which Tool Should You Use?
| Feature | Certbot | ACME.sh | acme-cli |
|---|---|---|---|
| Language | Python | Bash (pure POSIX) | Rust |
| Dependencies | Python 3, pip | bash, curl, openssl | Single binary |
| Installation | apt install certbot | curl https://get.acme.sh | sh | cargo install acme-cli |
| DNS plugins | Cloudflare, Route53, Hetzner, DigitalOcean (30+) | Cloudflare, Route53, Hetzner, Oracle, Aliyun (80+) | Minimal (mostly cloud providers) |
| Auto-renewal | systemd timer (automatic) | Cron job (automatic) | Manual |
| Key rotation | Automatic | Automatic | Manual |
| Config files | /etc/letsencrypt/ | ~/.acme.sh/ (non-root) | Per-setup |
| Privilege escalation | sudo required | Runs as non-root | sudo optional |
| Learning curve | Moderate | Steep (POSIX shell) | Low |
| Best for | Ubuntu/Debian servers | Lightweight/minimal systems | Embedded systems |
Winner: Certbot for most users (simple, packaged). ACME.sh if you want non-root operation or support more DNS providers.
Part 3: Certificate Lifecycle Management
Monitoring Certificate Expiry
# Check when your certificate expires
certbot certificates
# Output:
# Certificate Name: example.com
# Domains: example.com, *.example.com
# Expiry Date: 2026-08-18 (89 days left)
# Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
# Key Path: /etc/letsencrypt/live/example.com/privkey.pem
# Set up renewal reminder via systemd timer
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
# Check renewal status
sudo certbot renew --dry-run
# Monitor renewal in system journal
journalctl -u certbot.timer -f
Certificate Rotation Strategy
Let’s Encrypt certificates expire every 90 days. The renewal process is automatic via systemd, but understand the lifecycle:
# Timeline
# Day 0: Issue cert (valid 90 days)
# Day 60: Automatic renewal attempt begins (30 days before expiry)
# Day 90: Certificate expires
# If renewal fails (DNS API timeout, rate limit):
# Certbot retries every 12 hours
# If no retry succeeds by day 90 → certificate expires → HTTPS fails
# Verify renewal is working
sudo certbot renew --dry-run --verbose
# If renewal fails, manually renew with specific plugin
sudo certbot renew \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-vvv # Verbose output to debug
Part 4: Advanced Certificate Scenarios
Multi-Domain Certificates (SAN)
Subject Alternative Names (SANs) let one certificate cover multiple domains:
# Request cert covering multiple (non-wildcard) domains
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d example.com \
-d www.example.com \
-d api.example.com \
-d blog.example.com
# Combined with wildcard:
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d example.com \
-d "*.example.com" \
-d "*.api.example.com" # Second-level wildcard (separate SAN)
Wildcard Limitations & Workarounds
Limitation: *.example.com covers app.example.com but NOT api.mail.example.com (second level down).
Solution: Use multiple wildcard domains:
# Request both first and second-level wildcards
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d example.com \
-d "*.example.com" \
-d "*.api.example.com" # Separate wildcard for subdomains of api
# Result: One certificate covers all three:
# - example.com
# - *.example.com (app.example.com, api.example.com)
# - *.api.example.com (v1.api.example.com, v2.api.example.com)
Part 5: Debugging Certificate Issues
”Certificate renewal failed” — DNS propagation timeout
# Problem: DNS-01 challenge TXT record not propagating fast enough
# Cloudflare: usually propagates in <10 seconds
# Hetzner: usually propagates in <5 seconds
# Route53: can take 30+ seconds
# Fix 1: Increase propagation timeout
sudo certbot renew \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
--dns-cloudflare-propagation-seconds 60 # Wait up to 60 seconds
# Fix 2: Check DNS manually before renewal
nslookup _acme-challenge.example.com # Should return TXT record during renewal
# Fix 3: Use DNS API with faster propagation
# If using slow DNS provider, switch to Cloudflare (faster propagation)
”Certificate doesn’t include my domain” — SAN mismatch
# Check what domains are in the certificate
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -text -noout | grep -A1 "Subject Alternative Name"
# Output:
# X509v3 Subject Alternative Name:
# DNS:example.com, DNS:*.example.com
# If your subdomain is missing, re-request with it:
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d example.com \
-d "*.example.com" \
-d "*.subdomain.example.com" # Add missing domain
”HTTPS still fails after renewal” — Certificate path issue
# Check if your web server is using the correct path
# Certbot creates symlinks in /etc/letsencrypt/live/DOMAIN/
# Do NOT use /etc/letsencrypt/archive/ directly (breaks after renewal)
# Correct (uses symlink, survives renewal):
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Wrong (uses archive, breaks after renewal):
ssl_certificate /etc/letsencrypt/archive/example.com/fullchain1.pem;
ssl_certificate_key /etc/letsencrypt/archive/example.com/privkey1.pem;
# After renewal, symlinks update to point to new versions
# But hardcoded archive paths don't
# After fixing paths, reload web server
sudo nginx -s reload # or Apache: systemctl reload apache2
Part 6: Self-Hosted CA with step-ca (Optional)
For internal services (not public internet), create your own Certificate Authority:
# Install step-ca
curl -L https://github.com/smallstep/certificates/releases/download/v0.24.2/step-ca_linux_0.24.2_amd64.tar.gz | tar xz
sudo mv step-ca/bin/step* /usr/local/bin/
# Initialize CA
step ca init \
--deployment-type standalone \
--name "Internal CA" \
--dns internal.example.com \
--address localhost:9000
# Start CA daemon
step-ca ~/.step/config/ca.json
# In another terminal, request certificate for internal service
step ca certificate app.internal api.internal \
app.internal.crt app.internal.key
# Use in your internal service (PostgreSQL, Redis, microservices)
# Valid 24 hours by default, auto-renewals supported
Part 7: Geo-Specific DNS Providers
US-Based: Route53 (AWS)
# IAM policy for Certbot (principle of least privilege)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:GetChange",
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets"
],
"Resource": [
"arn:aws:route53:::hostedzone/ZONE_ID",
"arn:aws:route53:::change/*"
]
}
]
}
# Install plugin and authenticate
pip install certbot-dns-route53
aws configure # Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
# Request certificate
certbot certonly \
--dns-route53 \
-d example.com \
-d "*.example.com"
EU-Based: Hetzner DNS
# Hetzner: fast propagation (<5 seconds), EU data center
pip install certbot-dns-hetzner
# Create DNS API token at console.hetzner.cloud
cat > ~/.secrets/hetzner.ini << 'EOF'
dns_hetzner_token = YOUR_API_TOKEN
EOF
chmod 600 ~/.secrets/hetzner.ini
# Request certificate
certbot certonly \
--dns-hetzner \
--dns-hetzner-credentials ~/.secrets/hetzner.ini \
-d example.com \
-d "*.example.com"
Asia-Pacific: Alibaba Cloud (Aliyun)
pip install certbot-dns-aliyun
cat > ~/.secrets/aliyun.ini << 'EOF'
dns_aliyun_access_key = YOUR_ACCESS_KEY
dns_aliyun_access_key_secret = YOUR_SECRET
EOF
# Request certificate
certbot certonly \
--dns-aliyun \
--dns-aliyun-credentials ~/.secrets/aliyun.ini \
-d example.com \
-d "*.example.com"
Part 8: Security Best Practices
Protect Private Keys
# Your private key is the most sensitive file
# If compromised, attackers can impersonate your domain
# Restrict permissions (Certbot does this automatically)
sudo ls -la /etc/letsencrypt/live/example.com/
# -rw-r--r-- fullchain.pem (readable)
# -r-------- privkey.pem (root only, 0400)
# Verify permissions
sudo stat -c "%a %n" /etc/letsencrypt/live/example.com/privkey.pem
# Should output: 400 /etc/letsencrypt/live/example.com/privkey.pem
# Never commit to git or expose in logs
git add -f /etc/letsencrypt/ # DON'T DO THIS
echo "/etc/letsencrypt/" >> .gitignore # DO THIS
Certificate Pinning (Optional, Advanced)
For maximum security, pin the public key so only YOUR certificate is trusted (prevents MITM even if CA is compromised):
# Extract public key
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -pubkey -noout > public.key
# Add to HTTP headers
add_header Public-Key-Pins 'pin-sha256="YOUR_KEY_HASH"; max-age=2592000; includeSubDomains' always;
# Risk: If you lose the key, you lose availability (pinned key won't match renewal)
# Typically used only for highest-security applications
Conclusion
Let’s Encrypt wildcard certificates secured with Cloudflare DNS API cover every subdomain under your domain — a single cert for *.example.com replaces per-subdomain certificates for app.example.com, api.example.com, mail.example.com, and any new subdomain you add. Auto-renewal via Certbot’s systemd timer requires zero manual intervention.
See Apache SSL with Let’s Encrypt 2026 for single-domain HTTP-01 certificates without DNS API requirements.
People Also Ask
What is the difference between HTTP-01 and DNS-01 ACME challenges?
HTTP-01 requires Let’s Encrypt to reach a file at http://yourdomain.com/.well-known/acme-challenge/TOKEN — the server must be publicly accessible on port 80. It’s simpler but can’t issue wildcard certificates. DNS-01 requires creating a TXT record _acme-challenge.yourdomain.com = TOKEN in DNS — it works for wildcard certificates, doesn’t require port 80 to be open, and can be automated with DNS API credentials. Use HTTP-01 for standard single-domain certificates; use DNS-01 for wildcards or servers not publicly accessible (private servers, internal services).
Can I use Let’s Encrypt for internal/private servers?
Yes, via DNS-01 challenge — the domain must have public DNS records, but the server doesn’t need to be publicly accessible. Request the certificate with --dns-cloudflare (or your DNS provider’s plugin) on any machine with DNS API access, then copy the certificate files to the private server. The certificate is valid regardless of whether the server is publicly reachable.
Further Reading
- Nginx Reverse Proxy Tutorial 2026 — TLS configuration for the certificates issued here
- How to Enable HTTPS on Apache with Let’s Encrypt — HTTP-01 challenge for single domains
- Ubuntu 24.04 LTS Server Setup Checklist — SSL in the context of full server hardening
Tested on: Ubuntu 24.04 LTS (Hetzner CX22). Certbot 2.11.0, certbot-dns-cloudflare 2.11.0. Last verified: May 1, 2026.