SSL Certificate Setup: Let's Encrypt Auto-Renewal and Multi-Domain Management
Production once hit an expired Let’s Encrypt SSL certificate: monitoring alerts, browser warnings, recovery only after a manual certbot renew. The root cause was issuing a cert without systemd timer or cron auto-renewal and Nginx reload.
Certbot attempts renewal about 30 days before expiry; with a deploy hook configured, a successful renewal can reload the web server automatically. This guide covers single-domain certs, multi-domain SAN, wildcard DNS-01, and grouped multi-cert strategies so HTTPS stays hands-off long term.
What Is Let’s Encrypt? Understand the Basics First
Many teams use Let’s Encrypt without knowing how issuance works. Understanding CA validation and ACME makes troubleshooting faster.
The role of a certificate authority (CA)
Let’s Encrypt is a certificate authority (CA) built around free, automated, open issuance. Since 2016 it has driven broad HTTPS adoption—over 200 million active certificates with universal browser trust.
Traditional CAs (DigiCert, GeoTrust, etc.) charge fees, require manual steps, and slow down issuance. Let’s Encrypt automates verification and delivery. Certs last 90 days, which looks short but works well with auto-renewal: shorter lifetimes limit compromise windows.
ACME: the automation protocol
Let’s Encrypt uses ACME (Automated Certificate Management Environment), standardized in RFC 8555. The protocol defines how to prove domain control and receive certificates.
Three validation methods:
- HTTP-01: most common. The CA requests
/.well-known/acme-challenge/on your domain and checks for the challenge token—proof you control DNS and HTTP. - DNS-01: required for wildcards. The CA checks a TXT record. No web server needed, but you need DNS API access.
- TLS-ALPN-01: less common; used in specific edge cases.
HTTP-01 is simplest when port 80 is reachable. DNS-01 fits internal services and wildcard coverage.
Certbot: the recommended client
Certbot is the official Let’s Encrypt client. It handles issuance, optional web server configuration, and renewal scheduling.
Core capabilities:
- Multiple validation plugins
- Nginx/Apache config integration
- systemd timer or cron setup
- Dry-run renewal tests
With these basics clear, the sections below map directly to config files and logs when something breaks.
Single-Domain SSL: Start From Zero
Start with one domain on one server. Once that flow is solid, SAN and wildcard setups follow the same patterns.
Install Certbot
Install paths differ by distribution. On Ubuntu/Debian, Snap is the recommended path—current packages and timely updates.
Ubuntu/Debian (Snap):
# Install Snap (if needed)
sudo apt update
sudo apt install snapd
# Install Certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
CentOS/RHEL:
sudo yum install certbot
# or
sudo dnf install certbot
Verify installation:
certbot --version
# e.g. certbot 2.11.0
Obtain a certificate: three approaches
Option 1: Nginx plugin (good default)
Certbot edits Nginx, requests the cert, enables HTTPS, and can redirect HTTP in one command:
sudo certbot --nginx -d example.com -d www.example.com
You will be prompted for:
- Email (expiry and failure notices)
- Terms of service
- Whether to share email with EFF (optional)
- HTTP → HTTPS redirect (choose Yes for production sites)
On success, paths are printed:
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
Option 2: Apache plugin
Same flow with the Apache plugin:
sudo certbot --apache -d example.com -d www.example.com
Option 3: Cert only (manual web server config)
Use certonly when you do not want Certbot to touch vhosts—Caddy, Node.js, or custom Nginx layouts:
sudo certbot certonly --webroot \
-w /var/www/html \
-d example.com \
-d www.example.com
-w is the web root where challenge files are served. You wire fullchain.pem and privkey.pem yourself.
Confirm the certificate works
Inspect files on disk
sudo ls -la /etc/letsencrypt/live/example.com/
Expect:
cert.pem— leaf certificatechain.pem— intermediate chainfullchain.pem— leaf + chain (use this in Nginx)privkey.pem— private key
SSL Labs test
Open https://www.ssllabs.com/ssltest/ and enter your hostname. Aim for at least grade A.
Typical issues:
- B or C: old TLS versions or weak cipher suites (see hardening below).
- F: incomplete chain—point
ssl_certificateatfullchain.pem, notcert.pem.
Browser check
Visit https://example.com. The lock icon should show issuer “Let’s Encrypt”.
Auto-Renewal: Avoid Expiry Surprises
Ninety-day validity is manageable when renewal is automated—frequent rotation limits long-lived compromised keys.
How Certbot schedules renewal
Installation usually registers a scheduler:
systemd timer (modern Linux):
certbot.timer triggers certbot.service about twice per day. Certs within 30 days of expiry are renewed.
Check timer status:
sudo systemctl list-timers | grep certbot
Example output:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-04-02 12:00:00 UTC 1h left Thu 2026-04-02 00:00:00 UTC 11h ago certbot.timer certbot.service
Cron (legacy systems):
On hosts without systemd timers, Certbot may install cron instead:
sudo crontab -l
# or
cat /etc/cron.d/certbot
Typical entry:
0 0,12 * * * root certbot renew --quiet
Prove renewal will work
Dry run
Simulates renewal without replacing certs:
sudo certbot renew --dry-run
Example output:
Processing /etc/letsencrypt/renewal/example.com.conf
Cert not due for renewal, but simulating renewal for dry run
...
The dry run was successful.
“The dry run was successful” means scheduling and hooks are wired correctly.
Renewal config per certificate
cat /etc/letsencrypt/renewal/example.com.conf
This file stores original domains, authenticator, and hooks—read on every certbot renew.
Reload the web server after renewal
Renewed files on disk do not affect running workers until reload.
Deploy hook (recommended)
Runs only when renewal succeeds:
sudo certbot renew --deploy-hook "systemctl reload nginx"
Or in /etc/letsencrypt/renewal/example.com.conf:
# append:
deploy_hook = systemctl reload nginx
Post hook (runs every attempt)
sudo certbot renew --post-hook "systemctl reload nginx"
Prefer deploy hooks so failed renewals do not reload with stale or broken state.
When renewal fails
DNS propagation
After DNS changes, the CA may still see old records. Wait for TTL, or force:
sudo certbot renew --force-renewal
Firewall or port 80
HTTP-01 needs reachable port 80:
sudo ufw status
sudo iptables -L -n
Open temporarily if needed:
sudo ufw allow 80/tcp
Permissions
sudo ls -la /etc/letsencrypt/live/
sudo ls -la /etc/letsencrypt/archive/
Ensure the web server user (e.g. www-data) can read certs.
Logs
sudo tail -f /var/log/letsencrypt/letsencrypt.log
Use the logged error to fix DNS, auth, or rate limits before the cert actually expires.
Multi-Domain Management: Consolidate or Split
Several domains need a deliberate strategy—staggered expiries, scattered paths, and wrong ssl_certificate lines are common operational bugs.
One certificate, many names (SAN)
Request all names in one invocation:
sudo certbot --nginx \
-d example.com \
-d www.example.com \
-d api.example.com \
-d admin.example.com
Benefits:
- One renewal event for every name
- One directory under
/etc/letsencrypt/live/ - One pair of paths in Nginx
Subject Alternative Names (SAN) list every hostname the cert covers. Browsers match the requested host against SAN.
List certs and domains:
sudo certbot certificates
Example:
Found the following certs:
Certificate Name: example.com
Domains: example.com www.example.com api.example.com admin.example.com
Expiry Date: 2026-07-01 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
Wildcard certificates
*.example.com covers first-level subdomains (blog, api, admin, etc.) in one cert.
DNS-01 is required—HTTP-01 cannot issue wildcards. You need a DNS provider plugin and API credentials.
DNS plugin example (Cloudflare)
# Install plugin
sudo snap install certbot-dns-cloudflare
# Credentials file
sudo nano /root/.secrets/certbot/cloudflare.ini
dns_cloudflare_api_token = your_cloudflare_api_token
Create an API token with DNS:Edit for the zone.
Request wildcard + apex
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
-d "*.example.com" \
-d example.com
Wildcards do not include the apex—always add -d example.com.
Wildcard limits
*.example.com matches sub.example.com but not sub.sub.example.com.
For nested names, add another cert:
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
-d "*.example.com" \
-d "*.api.example.com" \
-d example.com
That covers api.example.com and v1.api.example.com.
When to use multiple certificates
Group by service (recommended)
Web:
sudo certbot --nginx -d example.com -d www.example.com -d blog.example.com -d docs.example.com
API:
sudo certbot --nginx -d api.example.com -d admin.example.com -d v1.api.example.com
Internal (DNS-01):
sudo certbot certonly --dns-cloudflare -d "*.internal.example.com"
Why split:
- Renewal blast radius stays small
- Different teams can own credentials
- One bad cert does not take down every vhost
Mix wildcard and dedicated certs
# Wildcard for most subdomains
sudo certbot certonly --dns-cloudflare -d "*.example.com" -d example.com
# Special hostname with its own policy
sudo certbot --nginx -d secure.example.com
Directory layout
Live symlinks: /etc/letsencrypt/live/[cert-name]/
Point Nginx at these paths; after each renew, symlinks update to new files in archive/.
sudo ls -la /etc/letsencrypt/live/example.com/
lrwxrwxrwx 1 root root 42 Apr 2 12:00 cert.pem -> ../../archive/example.com/cert2.pem
lrwxrwxrwx 1 root root 43 Apr 2 12:00 chain.pem -> ../../archive/example.com/chain2.pem
lrwxrwxrwx 1 root root 44 Apr 2 12:00 fullchain.pem -> ../../archive/example.com/fullchain2.pem
lrwxrwxrwx 1 root root 40 Apr 2 12:00 privkey.pem -> ../../archive/example.com/privkey2.pem
Archive: /etc/letsencrypt/archive/[cert-name]/ — versioned cert1.pem, cert2.pem, …
Renewal config: /etc/letsencrypt/renewal/[cert-name].conf
cat /etc/letsencrypt/renewal/example.com.conf
Records authenticator (webroot, nginx, dns-cloudflare), domain list, and hooks.
Advanced Tuning: Security and Performance
After basic HTTPS works, tighten protocols and add stapling so SSL Labs grades and handshake cost both improve.
HTTP/2 and OCSP Stapling
Enable HTTP/2 in Nginx
server {
listen 443 ssl http2; # add http2
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# ... other directives
}
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
OCSP Stapling
OCSP checks revocation status. Stapling serves a signed OCSP response from your server so clients skip an extra CA round trip.
server {
# ... SSL directives
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
}
SSL Labs should report “OCSP Stapling: Yes”.
Disable legacy TLS
TLS 1.0 and 1.1 are deprecated; major browsers dropped them years ago.
server {
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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
}
Retest on SSL Labs—A or A+ is realistic with a complete chain and stapling.
HSTS
HSTS tells browsers to use HTTPS only for your host (and optionally subdomains).
server {
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}
max-age=31536000— one yearincludeSubDomains— applies to subdomainspreload— eligible for browser preload lists (submit separately)
Once HSTS is live with a long max-age, temporary HTTP-only debugging becomes harder—roll out gradually if unsure.
Monitoring and alerts
Automation can still fail silently until browsers complain. Check expiry before the 30-day renewal window.
Simple expiry script
#!/bin/bash
# /usr/local/bin/check-ssl-expiry.sh
DOMAIN="example.com"
EXPIRY_DAYS=$(openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_DATE=$(date -d "$EXPIRY_DAYS" +%s)
CURRENT_DATE=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_DATE - $CURRENT_DATE) / 86400 ))
if [ $DAYS_LEFT -lt 7 ]; then
echo "WARNING: SSL certificate for $DOMAIN expires in $DAYS_LEFT days"
# mail -s "SSL Certificate Expiry Warning" [email protected] <<< "SSL certificate for $DOMAIN expires in $DAYS_LEFT days"
fi
Daily cron:
0 6 * * * /usr/local/bin/check-ssl-expiry.sh
Certbot email
Failures also notify the address used at issuance—keep it valid and monitored.
Common Problems and Fixes
Certificate issuance fails
DNS not pointing at the server
HTTP-01 needs the domain to resolve to your host.
dig example.com +short
# or
nslookup example.com
Wait for TTL after DNS changes, then retry.
Port 80 blocked or in use
sudo netstat -tulpn | grep :80
sudo lsof -i :80
Stop the conflicting service if it is not your web server:
sudo systemctl stop <service-name>
Open firewall ports:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Webroot permissions
For certonly --webroot, Certbot must write under .well-known/:
ls -la /var/www/html/.well-known/
sudo mkdir -p /var/www/html/.well-known/acme-challenge
sudo chown -R www-data:www-data /var/www/html/.well-known
Renewal failures
Log review
sudo tail -100 /var/log/letsencrypt/letsencrypt.log
Typical messages:
Connection refused— port or firewallDNS problem: NXDOMAIN— DNSRate limit exceeded— too many orders for the same names
Force renewal
sudo certbot renew --force-renewal
Broken renewal config
cat /etc/letsencrypt/renewal/example.com.conf
If corrupt, delete and re-issue:
sudo certbot delete --cert-name example.com
sudo certbot --nginx -d example.com -d www.example.com
Multiple certificates on one host
Wrong path in Nginx
sudo nginx -T | grep ssl_certificate
Each server block must reference the correct live directory.
Mistakes:
cert.peminstead offullchain.pem- Path uses the wrong cert name (first
-dbecomes the directory name)
sudo certbot certificates
Align Nginx paths with the listed Certificate Name.
Wildcard pitfalls
Only one subdomain level
*.example.com does not cover sub.sub.example.com—issue *.api.example.com or a separate cert.
Apex not included
# Wrong: apex will fail
sudo certbot certonly --dns-cloudflare -d "*.example.com"
# Correct
sudo certbot certonly --dns-cloudflare -d "*.example.com" -d example.com
Summary: From Manual Certs to Automated HTTPS
Let’s Encrypt and ACME replace slow manual CA workflows with repeatable issuance and renewal.
Takeaways:
- ACME defines validation (HTTP-01 vs DNS-01) and issuance; pick the method that matches your topology.
- Auto-renewal via systemd timer or cron renews within 30 days of expiry; deploy hooks reload Nginx after success.
- Multi-domain: SAN certs simplify related hosts; wildcards scale subdomains; splitting by service limits blast radius.
- Hardening: HTTP/2, OCSP Stapling, TLS 1.2+, and HSTS push SSL Labs toward A+.
- Monitoring: expiry scripts and valid Certbot email catch failures before users do.
Next steps at scale:
- Platforms: cert-manager on Kubernetes, Traefik with ACME, or similar ingress automation
- External monitors: SSL expiry checks in Uptime Robot or dedicated SSL monitors
- CI/CD: fail deploys when prod certs are near expiry
Once renewal, reload, and alerts are in place, HTTPS becomes routine infrastructure—not an emergency ticket when a cert lapses.
Configure Let's Encrypt SSL certificate auto-renewal
End-to-end SSL setup from installing Certbot through auto-renewal so HTTPS does not expire unnoticed
⏱️ Estimated time: 30 min
- 1
Step1: Install Certbot
Choose an install method for your OS:
• Ubuntu/Debian (recommended): sudo snap install --classic certbot
• CentOS/RHEL: sudo yum install certbot
• Verify: certbot --version - 2
Step2: Request an SSL certificate
Pick the approach that fits your stack:
• Nginx auto-config: sudo certbot --nginx -d example.com -d www.example.com
• Apache auto-config: sudo certbot --apache -d example.com -d www.example.com
• Cert only: sudo certbot certonly --webroot -w /var/www/html -d example.com - 3
Step3: Verify auto-renewal
Confirm Certbot scheduled renewal:
• Systemd timer: sudo systemctl list-timers | grep certbot
• Dry run: sudo certbot renew --dry-run
• Expect output "successful" - 4
Step4: Reload the web server after renewal
Add a deploy hook in the renewal config:
• Edit: sudo nano /etc/letsencrypt/renewal/example.com.conf
• Add: deploy_hook = systemctl reload nginx
• Or CLI: sudo certbot renew --deploy-hook "systemctl reload nginx" - 5
Step5: Security hardening
Add these Nginx SSL settings:
• Disable old TLS: ssl_protocols TLSv1.2 TLSv1.3;
• HTTP/2: listen 443 ssl http2;
• OCSP Stapling: ssl_stapling on;
• HSTS: add_header Strict-Transport-Security "max-age=31536000" always; - 6
Step6: Set up monitoring and alerts
Monitor certificate expiry:
• Script: sudo nano /usr/local/bin/check-ssl-expiry.sh
• Cron: 0 6 * * * /usr/local/bin/check-ssl-expiry.sh
• Ensure Certbot email notifications are configured
FAQ
Why are Let's Encrypt certificates only valid for 90 days?
What is the difference between HTTP-01 and DNS-01 validation?
DNS-01: the CA checks a DNS TXT record—requires DNS API access. Required for wildcard certs; suited to internal services or many subdomains.
Which hostnames does a wildcard *.example.com certificate cover?
Not covered:
• Nested subdomains: sub.sub.example.com (request *.sub.example.com separately)
• Apex: example.com (add -d example.com explicitly)
When does Certbot auto-renewal run?
How do I fix an SSL Labs grade of B or C?
• Old TLS: set ssl_protocols TLSv1.2 TLSv1.3;
• Weak ciphers: use the recommended suite in section six
• Incomplete chain: use fullchain.pem, not cert.pem
• Missing OCSP Stapling: add ssl_stapling on;
Reload Nginx and retest—you should reach A or A+.
One certificate for many domains, or separate certificates?
• Single SAN cert: related domains, one renewal, simple config—e.g. example.com, www.example.com, blog.example.com
• Multiple certs: isolate services, failures, and permissions—web, API, internal tools separately
• Wildcard: many subdomains with fewer certs—*.example.com covers all first-level names
What if certificate renewal fails?
Common causes:
• DNS: wait for propagation or try sudo certbot renew --force-renewal
• Port 80 blocked or in use: free port 80 temporarily
• Firewall: allow 80/443
• Permissions: verify cert file permissions
After fixing: sudo certbot renew --force-renewal
8 min read · Published on: Apr 2, 2026 · Modified on: May 19, 2026
Related Posts
AI-Generated Game Sound Effect Prompts: How to Describe Attack, Pickup, Victory, and Defeat Sounds
AI-Generated Game Sound Effect Prompts: How to Describe Attack, Pickup, Victory, and Defeat Sounds
Cocos Mini-Game Character Movement & Attack: Implementation from Nodes to Animation
Cocos Mini-Game Character Movement & Attack: Implementation from Nodes to Animation
Where Does Game Feel Come From: Flash, Shake, Floating Text, Sound, Particle Feedback
Comments
Sign in with GitHub to leave a comment