TL;DR — Quick Summary
Certbot wildcard SSL certificates via DNS-01 challenge. Covers manual and automated DNS plugins, Cloudflare setup, auto-renewal, rate limits, troubleshooting.
Wildcard SSL certificates let a single certificate secure every subdomain under your domain — *.example.com covers api.example.com, app.example.com, staging.example.com, and any other subdomain you create. Rather than issuing individual certificates for each service, you obtain one certificate and deploy it across your entire infrastructure. Let’s Encrypt provides wildcard certificates free of charge, but requires the DNS-01 challenge for validation — a requirement that HTTP-based challenges cannot satisfy.
This guide covers every aspect of obtaining and maintaining wildcard certificates with Certbot: from understanding why DNS-01 is mandatory, through manual validation, automated DNS plugins, Cloudflare deep-dive configuration, auto-renewal with systemd, multi-domain SANs, rate limits, and troubleshooting.
Prerequisites
Before you begin, have the following ready:
- Ubuntu 22.04 LTS or 24.04 LTS (or any Debian-based Linux) with sudo access.
- A registered domain with DNS managed by a supported provider (Cloudflare, AWS Route 53, DigitalOcean, Google Cloud DNS, or manual access to your zone).
- Certbot installed — or you will install it below.
- An API token for your DNS provider with permission to create and delete DNS records.
- Port 80 does NOT need to be open for wildcard DNS-01 challenges (unlike HTTP-01 challenges).
Why Wildcard Certificates?
A standard certificate covers one or more specific hostnames: example.com, www.example.com, api.example.com. If you add a new subdomain — metrics.example.com — you must either add it to the SAN list and reissue the certificate, or obtain a new one.
A wildcard certificate (*.example.com) eliminates this problem:
- One certificate, unlimited subdomains — Any hostname matching
*.example.comat a single level is covered automatically. - Simplified certificate management — One renewal process, one private key, one deploy hook.
- Works with internal services — Subdomains that do not have public web servers (internal APIs, management UIs) are covered without needing HTTP access.
- Cost efficiency — Let’s Encrypt issues these free. Commercial wildcard certificates from traditional CAs typically cost $100–$300/year.
The one limitation: wildcards cover exactly one subdomain level. *.example.com covers api.example.com but not v1.api.example.com. For nested subdomains, add a second wildcard: *.api.example.com.
Understanding ACME and the DNS-01 Challenge
Let’s Encrypt uses the ACME protocol (RFC 8555) to automate certificate issuance. You must prove control of your domain by completing a challenge.
Why HTTP-01 Cannot Validate Wildcards
The HTTP-01 challenge places a token file at http://example.com/.well-known/acme-challenge/<TOKEN>. This works for specific hostnames that have a reachable web server. But *.example.com represents an infinite set of hostnames — most of which may have no HTTP server at all. The ACME server has no way to probe every possible subdomain, so HTTP-01 is explicitly prohibited for wildcard certificates by the ACME specification.
How DNS-01 Works
DNS-01 validates domain control by proving you can write to the DNS zone:
- Certbot contacts the ACME server and requests authorization for
*.example.com. - The ACME server issues a token and instructs you to publish a TXT record at
_acme-challenge.example.comwith a specific value derived from the token. - Certbot (or its DNS plugin) creates the TXT record via your DNS provider’s API.
- The ACME server queries DNS resolvers to verify the TXT record exists.
- Once validated, the certificate is signed and returned.
- Certbot (or its plugin) deletes the TXT record.
DNS-01 works even when no web server exists, when port 80 is blocked by a firewall, and when the subdomain has no A record.
Installing Certbot
The snap package is the officially maintained installation method and ensures you always have the latest version.
# Remove any OS-packaged certbot to avoid conflicts
sudo apt remove certbot -y 2>/dev/null || true
# Install Certbot via snap
sudo snap install --classic certbot
# Symlink so certbot is in PATH
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# Verify
certbot --version
If your distribution does not support snaps, use pip in a virtual environment:
sudo apt install python3-pip python3-venv -y
python3 -m venv /opt/certbot
/opt/certbot/bin/pip install certbot
sudo ln -s /opt/certbot/bin/certbot /usr/local/bin/certbot
Manual DNS Validation
For a one-time certificate or when testing, manual DNS validation requires no API tokens. You create the TXT record yourself.
sudo certbot certonly \
--manual \
--preferred-challenges dns \
-d "*.example.com" \
-d example.com
Certbot will output something like:
Please deploy a DNS TXT record under the name:
_acme-challenge.example.com
with the following value:
gfj9Xq8R3mT2nKp1vLwY5sAqZdBcF7eH
Once deployed, press Enter to continue...
Log in to your DNS provider, add the TXT record, then verify propagation before pressing Enter:
dig +short TXT _acme-challenge.example.com
# Should return: "gfj9Xq8R3mT2nKp1vLwY5sAqZdBcF7eH"
# Or query a public resolver
dig @8.8.8.8 +short TXT _acme-challenge.example.com
Wait until the value appears (typically 30–120 seconds, but can take up to 10 minutes depending on your provider’s TTL). Then press Enter.
Note: Manual validation does not support automatic renewal. Certbot will prompt you to recreate the TXT record manually every 60–89 days. For production use, set up an automated DNS plugin.
Automated DNS Plugins
DNS plugins eliminate manual steps by calling your DNS provider’s API to create and delete the _acme-challenge TXT record automatically during both initial issuance and every subsequent renewal.
| Plugin | Provider | Install Command |
|---|---|---|
certbot-dns-cloudflare | Cloudflare | sudo snap install certbot-dns-cloudflare |
certbot-dns-route53 | AWS Route 53 | sudo snap install certbot-dns-route53 |
certbot-dns-google | Google Cloud DNS | sudo snap install certbot-dns-google |
certbot-dns-digitalocean | DigitalOcean | sudo snap install certbot-dns-digitalocean |
certbot-dns-linode | Linode/Akamai | sudo snap install certbot-dns-linode |
certbot-dns-hetzner | Hetzner DNS | pip install certbot-dns-hetzner |
Each plugin follows the same pattern:
- Install the plugin.
- Create a credentials file with your API token.
- Set file permissions to
600(only root can read it). - Run
certbot certonlywith the plugin’s--dns-<provider>flag and--dns-<provider>-credentialspath.
Cloudflare Plugin Deep Dive
Cloudflare is one of the most common DNS providers and has excellent Certbot integration. Here is a complete walkthrough.
Step 1: Create a Cloudflare API Token
Log in to the Cloudflare dashboard → My Profile → API Tokens → Create Token.
Use the “Edit zone DNS” template, then configure:
- Permissions:
Zone→DNS→Edit - Zone Resources:
Include→Specific zone→ select your domain (orAll zonesif managing multiple domains) - IP Address Filtering: Optionally restrict to your server’s IP for extra security
- TTL: Set an expiration date if you want token rotation enforced
Copy the token value — it is only shown once.
Step 2: Create the Credentials File
sudo mkdir -p /etc/letsencrypt/cloudflare
sudo tee /etc/letsencrypt/cloudflare/credentials.ini > /dev/null <<'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN_HERE
EOF
# Restrict to root read-only
sudo chmod 600 /etc/letsencrypt/cloudflare/credentials.ini
sudo chown root:root /etc/letsencrypt/cloudflare/credentials.ini
Security note: Never use the legacy
dns_cloudflare_email+dns_cloudflare_api_key(Global API Key) method. The scoped API token limits blast radius if the credentials file is ever exposed.
Step 3: Install the Plugin
sudo snap install certbot-dns-cloudflare
# Connect the snap plugin to the certbot snap
sudo snap set certbot trust-plugin-with-root=ok
sudo snap connect certbot:plugin certbot-dns-cloudflare
Step 4: Obtain the Certificate
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
--dns-cloudflare-propagation-seconds 60 \
-d example.com \
-d "*.example.com" \
--email admin@example.com \
--agree-tos \
--non-interactive
The --dns-cloudflare-propagation-seconds 60 flag tells Certbot to wait 60 seconds after creating the TXT record before asking Let’s Encrypt to validate. Cloudflare’s API propagation is usually near-instant, but this buffer prevents transient failures.
Step 5: Verify the Certificate
sudo certbot certificates
Expected output:
Found the following certs:
Certificate Name: example.com
Serial Number: 3a1f...
Key Type: ECDSA
Domains: example.com *.example.com
Expiry Date: 2026-06-20 00:00:00+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem
Certificate File Locations
After issuance, certificate files are stored under /etc/letsencrypt/. The live/ directory contains symlinks to the most recent versions:
| File | Path | Purpose |
|---|---|---|
| Full chain | /etc/letsencrypt/live/example.com/fullchain.pem | Certificate + intermediate CA (use this in Nginx/Apache) |
| Private key | /etc/letsencrypt/live/example.com/privkey.pem | Private key — protect carefully |
| Chain only | /etc/letsencrypt/live/example.com/chain.pem | Intermediate CA chain only |
| Cert only | /etc/letsencrypt/live/example.com/cert.pem | Domain certificate without chain |
Always reference the live/ path in your server configuration. The actual files are in /etc/letsencrypt/archive/example.com/ and are rotated on each renewal. The symlinks in live/ are updated automatically — your web server config never needs to change.
Configure Nginx to use the wildcard certificate:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name *.example.com example.com;
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_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}
Auto-Renewal with systemd
Certbot automatically installs a systemd timer when installed via snap or apt.
Verify the Timer
sudo systemctl status certbot.timer
sudo systemctl list-timers certbot.timer
The timer triggers twice daily at random times to spread load on Let’s Encrypt servers. Certbot only requests a new certificate when the existing one has 30 or fewer days remaining.
Add a Deploy Hook for Nginx
A deploy hook runs after a certificate is successfully renewed. Use it to reload your web server so it picks up the new certificate:
# One-time deploy hook
sudo certbot renew --deploy-hook "systemctl reload nginx"
# Permanent: write to renewal configuration
sudo bash -c 'cat >> /etc/letsencrypt/renewal/example.com.conf <<EOF
[renewalparams]
deploy_hook = systemctl reload nginx
EOF'
For HAProxy, which reads certificates differently:
# Combine fullchain and privkey into a single PEM for HAProxy
cat /etc/letsencrypt/live/example.com/fullchain.pem \
/etc/letsencrypt/live/example.com/privkey.pem \
> /etc/haproxy/certs/example.com.pem
systemctl reload haproxy
Create a script at /etc/letsencrypt/renewal-hooks/deploy/reload-haproxy.sh and make it executable:
#!/bin/bash
cat /etc/letsencrypt/live/example.com/fullchain.pem \
/etc/letsencrypt/live/example.com/privkey.pem \
> /etc/haproxy/certs/example.com.pem
systemctl reload haproxy
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-haproxy.sh
Test Renewal
Always test with a dry run before relying on automated renewal:
sudo certbot renew --dry-run
A successful dry run confirms the DNS plugin credentials are valid, the API token has the correct permissions, and the renewal pipeline will work when the certificate nears expiry.
Multi-Domain Wildcards
A single certificate can include multiple wildcard SANs. This is useful when managing staging, production, and internal environments with different base domains.
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d example.com \
-d "*.example.com" \
-d staging.example.net \
-d "*.staging.example.net"
Certbot will create a certificate with four SANs covering both domains and all their single-level subdomains. Both domains must be managed in Cloudflare (or you must configure separate credentials for each DNS provider used).
For nested subdomains, add explicit wildcards:
-d "*.api.example.com" \
-d "*.internal.example.com"
Note that *.example.com does NOT cover *.api.example.com. Each level requires a separate wildcard entry.
Rate Limits
Let’s Encrypt enforces rate limits to prevent abuse. Understand these before testing extensively:
| Limit | Value | Window |
|---|---|---|
| Certificates per registered domain | 50 | 7 days |
| Duplicate certificates | 5 | 7 days |
| Failed validations | 5 | 1 hour (per account per hostname) |
| New orders | 300 | 3 hours |
| Accounts per IP | 10 | 3 hours |
The most commonly hit limit during development is 5 duplicate certificates per week. A duplicate certificate covers exactly the same set of domains as an existing certificate.
Testing with Staging
The Let’s Encrypt staging environment issues real-format certificates but from an untrusted CA. Use it during development to avoid consuming rate limit quota:
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
--staging \
-d example.com \
-d "*.example.com"
Staging certificates display a “Fake LE Root X1” issuer in browsers. Once your configuration works correctly, delete the staging certificate and obtain a production one:
sudo certbot delete --cert-name example.com
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d example.com \
-d "*.example.com"
Use --dry-run on existing certificates to test renewal without actually issuing a new certificate:
sudo certbot renew --dry-run --cert-name example.com
Certbot vs Alternatives
| Tool | Wildcard Support | Auto-DNS | Let’s Encrypt | Other CAs | Notes |
|---|---|---|---|---|---|
| Certbot | Yes (DNS plugins) | Plugin-based | Yes | Limited | Official EFF client, widest plugin ecosystem |
| acme.sh | Yes | Built-in (150+ providers) | Yes | Yes | Bash-only, no dependencies, highly portable |
| Caddy | Yes (native) | Automatic | Yes | Yes | Zero-config; DNS module must be compiled in |
| Traefik | Yes (native) | Automatic | Yes | Limited | Ideal for container environments |
| Commercial CA | Yes | No (manual) | No | Yes | $100–$300/year, OV/EV options, no automation |
For most Linux server administrators already comfortable with Nginx or Apache, Certbot with a DNS plugin is the most straightforward path. If you manage many domains across many servers, acme.sh’s zero-dependency Bash approach scales better. For container-native environments, Traefik’s automatic HTTPS with DNS challenge removes all manual steps.
Troubleshooting Common Errors
CAA Record Blocking Issuance
Symptom: Error: CAA record for example.com prevents issuance
CAA (Certification Authority Authorization) records restrict which CAs may issue certificates for your domain. If your zone has a CAA record that does not include letsencrypt.org, issuance will fail.
Check your CAA records:
dig +short CAA example.com
If no Let’s Encrypt entry exists, add one:
example.com. CAA 0 issue "letsencrypt.org"
example.com. CAA 0 issuewild "letsencrypt.org"
The issuewild tag is specifically required for wildcard certificate authorization.
DNS Propagation Delays
Symptom: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.example.com
The ACME server queried DNS before your TXT record propagated. Increase the propagation wait:
--dns-cloudflare-propagation-seconds 120
For providers with slow propagation, you may need 300 seconds or more. Verify the TXT record is visible from a public resolver before Certbot validates:
dig @8.8.8.8 +short TXT _acme-challenge.example.com
dig @1.1.1.1 +short TXT _acme-challenge.example.com
Rate Limit Hit
Symptom: Error creating new order :: too many certificates already issued for exact set of domains
You have issued 5 duplicate certificates for the same domain set within 7 days. Options:
- Wait for the rate limit window to expire (check expiry at crt.sh).
- Add or remove a domain from the SAN list to make it a non-duplicate.
- Use
--stagingfor further testing.
Check issuance history:
curl -s "https://crt.sh/?q=example.com&output=json" | python3 -m json.tool | grep -E '"not_before"|"name_value"' | head -20
Credentials File Permission Error
Symptom: Unsafe permissions on credentials configuration file
The plugin refuses to use the credentials file if its permissions are too permissive:
sudo chmod 600 /etc/letsencrypt/cloudflare/credentials.ini
sudo chown root:root /etc/letsencrypt/cloudflare/credentials.ini
Renewal Fails with API Token Error
Symptom: Error: 9103 - Authentication error (Cloudflare) or similar API errors
The API token may have expired or been revoked. Verify it:
curl -s -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer YOUR_TOKEN" | python3 -m json.tool
Generate a new token in the Cloudflare dashboard and update the credentials file.
Summary
Wildcard SSL certificates from Let’s Encrypt give you the flexibility to secure unlimited subdomains under a single certificate at no cost. The key points to remember:
- Wildcards require DNS-01 challenge — HTTP-01 cannot validate
*.example.com. - Manual DNS validation works for one-time issuance but does not support auto-renewal.
- DNS plugins (certbot-dns-cloudflare, certbot-dns-route53, etc.) enable fully automated renewal.
- The Cloudflare API token must have
Zone:DNS:Editpermission; credentials file must bechmod 600. - Certificate files live at
/etc/letsencrypt/live/— always usefullchain.pemin your web server config. - Deploy hooks reload Nginx, HAProxy, or other services after each renewal.
- Rate limits are strict — use
--stagingfor testing and--dry-runfor renewal validation. - CAA records must include
letsencrypt.orgwithissuewildfor wildcard authorization.