SSL certificate expired or renewal failed: fix it now

Updated November 2026

Browsers show “Your connection is not private” the moment a certificate expires, and most visitors leave. Usually you can fix it in minutes: find out which certificate the server is really sending, renew it, and reload the web server. Then find out why automatic renewal failed, because it will fail again.

Do this first.

  1. Check what the server actually sends with openssl s_client (below). Sometimes the certificate was renewed but the web server was never reloaded.
  2. Run sudo certbot renew --dry-run and read the error. Do not run real renewals over and over: failed attempts count against Let’s Encrypt rate limits.
  3. After a successful renewal, reload (not restart) nginx or Apache.

Symptoms

Step 1: check the live certificate

Ask the server what it sends, using the real host name (the -servername option selects the right site on shared IPs):

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

# Exit status 0 = still valid in 14 days (1209600 seconds), 1 = expires sooner
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 1209600

For a mail server, add -starttls smtp and connect to port 25 or 587. Look at notAfter. Then compare with what Certbot has on disk:

sudo certbot certificates

This lists each certificate, its domains, its expiry date and its file paths. If Certbot shows a fresh date but openssl shows the old one, the certificate was renewed but the web server still has the old one in memory. Skip to step 4.

If the dates match and are in the past, the renewal itself failed. Check that the web server points at the files in /etc/letsencrypt/live/name/ (fullchain.pem and privkey.pem), not at copies. The files in live are links that Certbot updates on every renewal; a copy never changes.

Step 2: test renewal safely

sudo certbot renew --dry-run

A dry run gets test certificates from the Let’s Encrypt staging server and saves nothing, so it does not use up your production limits. Read the last lines of the output and the log in /var/log/letsencrypt/letsencrypt.log. To test one certificate only, add --cert-name example.com.

Step 3: fix the common failures

Port 80 blocked or not answering

The HTTP-01 challenge, used by the webroot, nginx, Apache and standalone methods, can only be done on port 80. Let’s Encrypt follows redirects to port 80 or 443, so an HTTP-to-HTTPS redirect is fine, but the first request must reach you on port 80.

# Is anything listening on port 80?
sudo ss -ltnp | grep ':80 '

# Firewall: Debian/Ubuntu with ufw
sudo ufw status
sudo ufw allow 80/tcp

# Firewall: RHEL, Rocky, AlmaLinux, Fedora
sudo firewall-cmd --list-all
sudo firewall-cmd --permanent --add-service=http && sudo firewall-cmd --reload

Also check any cloud security group, hosting panel firewall or CDN in front of the server. If the domain has an AAAA (IPv6) record, the site must answer on IPv6 as well, because the validation can arrive over IPv6.

Wrong webroot

With --webroot, Certbot writes a file to webroot/.well-known/acme-challenge/, and your web server must serve it over plain HTTP. Test it yourself:

# Find the webroot Certbot uses
sudo grep -E 'webroot|authenticator' /etc/letsencrypt/renewal/example.com.conf

# Put a test file there and fetch it from outside
sudo mkdir -p /var/www/example/.well-known/acme-challenge
echo ok | sudo tee /var/www/example/.well-known/acme-challenge/test
curl -i http://example.com/.well-known/acme-challenge/test
sudo rm /var/www/example/.well-known/acme-challenge/test

A 404 means the path is wrong, or a rule (for example a block on hidden files, a rewrite to index.php, or a forced login) hides it. Fix the server configuration or the path. Certbot’s own docs warn that hand-editing files in /etc/letsencrypt/renewal/ can damage them; back the file up first, or re-run certbot certonly with the correct -w path for that certificate.

DNS problems

Rate limits

As of November 2026, Let’s Encrypt allows 50 new certificates per registered domain per 7 days, 5 certificates for the exact same set of names per 7 days, and 5 failed validations per name per account per hour. Renewals are exempt from the per-domain limit, and renewals coordinated by ACME Renewal Information (ARI) are exempt from all limits. The error message includes a “retry after” time in UTC. Wait it out; you cannot ask for an override of the duplicate or failed-validation limits. Use --dry-run until the test passes, then renew once.

Step 4: renew and reload

sudo certbot renew --cert-name example.com

# Test the config, then reload (no dropped connections)
sudo nginx -t && sudo systemctl reload nginx
# Apache on Debian/Ubuntu
sudo apachectl configtest && sudo systemctl reload apache2
# Apache on RHEL family
sudo apachectl configtest && sudo systemctl reload httpd

Add a deploy hook so future renewals reload the server automatically. Any executable script in /etc/letsencrypt/renewal-hooks/deploy/ runs once after each successful renewal:

printf '#!/bin/sh\nsystemctl reload nginx\n' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx

Do not forget other services that use the same files, such as Postfix, Dovecot or HAProxy.

Step 5: check the renewal timer

Certbot relies on a scheduled job that runs certbot renew twice a day. Depending on how it was installed, that is a systemd timer (certbot.timer from Debian/Ubuntu packages, certbot-renew.timer from EPEL/Fedora, snap.certbot.renew.timer for the snap) or a cron entry.

systemctl list-timers --all | grep -i certbot
grep -rs certbot /etc/crontab /etc/cron.d/

# Enable the packaged timer if it is off (Debian/Ubuntu name shown)
sudo systemctl enable --now certbot.timer
sudo journalctl -u certbot.service --since "-7 days"

Shorter lifetimes are coming

Let’s Encrypt certificates are still valid for 90 days by default, but this is changing. Certbot 4.0 and later renew when less than a third of the lifetime is left; older versions renewed 30 days before expiry. Any script with a fixed “every 60 days” schedule will break.

Now 90 days 10 Feb 2027 64 days 16 Feb 2028 45 days Default (classic) profile; 45-day tlsserver profile available since May 2026
Let’s Encrypt’s announced schedule for its default certificate lifetime.

Let’s Encrypt also stopped sending expiry reminder emails on June 4, 2025, so you need your own monitoring. Industry rules cap all public certificates at 47 days from March 15, 2029.

Confirm it is fixed

Prevent it next time

When to get help

Ask your hosting provider if you do not control the firewall, load balancer or DNS. If a CDN or hosting panel issues the certificate for you, renew it there, not with Certbot. If crt.sh shows certificates for your domain that you did not request, treat it as a possible hijack: see Domain or DNS hijacked.

Sources