Switch Language
Toggle Theme

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.

200M+
Active certificates

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:

  1. 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.
  2. DNS-01: required for wildcards. The CA checks a TXT record. No web server needed, but you need DNS API access.
  3. 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 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 certificate
  • chain.pem — intermediate chain
  • fullchain.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_certificate at fullchain.pem, not cert.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.

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

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 year
  • includeSubDomains — applies to subdomains
  • preload — 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.

63%
Certificate incidents tied to expiry

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 firewall
  • DNS problem: NXDOMAIN — DNS
  • Rate 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.pem instead of fullchain.pem
  • Path uses the wrong cert name (first -d becomes 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:

  1. ACME defines validation (HTTP-01 vs DNS-01) and issuance; pick the method that matches your topology.
  2. Auto-renewal via systemd timer or cron renews within 30 days of expiry; deploy hooks reload Nginx after success.
  3. Multi-domain: SAN certs simplify related hosts; wildcards scale subdomains; splitting by service limits blast radius.
  4. Hardening: HTTP/2, OCSP Stapling, TLS 1.2+, and HSTS push SSL Labs toward A+.
  5. 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. 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. 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. 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. 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. 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. 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?
A 90-day lifetime paired with automated renewal is a security feature. Shorter-lived certs reduce exposure; if a private key leaks, it becomes useless within 90 days at most.
What is the difference between HTTP-01 and DNS-01 validation?
HTTP-01: the CA fetches a token at a well-known URL on your domain—simple, but port 80 must be reachable. Best for single-host certs.

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?
Only one level of subdomain: sub.example.com, api.example.com, blog.example.com, etc.

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?
Renewal starts when the cert is within 30 days of expiry. A systemd timer or cron job checks about twice daily (often midnight and noon). After a successful renew, a deploy hook reloads the web server.
How do I fix an SSL Labs grade of B or C?
Common causes and fixes:

• 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?
Group by service when possible:

• 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?
Check logs first: sudo tail -100 /var/log/letsencrypt/letsencrypt.log

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

Comments

Sign in with GitHub to leave a comment