An expired certificate takes a site offline for most visitors: browsers show a full-page warning and APIs refuse to connect. With Let’s Encrypt’s 90-day certificates, renewal is automated — but automation fails silently sometimes (DNS changes, firewall rules, a moved site). Checking and monitoring expiry takes minutes to set up.

Check a live site

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates -subject -issuer
notBefore=Sep 10 04:12:33 2026 GMT
notAfter=Dec  9 04:12:32 2026 GMT
subject=CN=example.com
issuer=C=US, O=Let's Encrypt, CN=R11

Always pass -servername — servers hosting several sites return a default certificate without SNI.

With curl:

curl -vsI https://example.com 2>&1 | grep -E 'expire date|subject:|issuer:'

Days remaining

domain=example.com
end=$(echo | openssl s_client -connect $domain:443 -servername $domain 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
echo "$domain: $(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 )) days left"

Or simply ask whether it expires within 14 days (exit code 1 if so):

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -checkend $((14*86400))

Check a certificate file

sudo openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -enddate
sudo certbot certificates          # all Certbot-managed certs with days remaining

Check many domains

#!/bin/bash
# ssl-check.sh — warn about certificates expiring within N days
DAYS=${DAYS:-14}
while read -r domain; do
  [ -z "$domain" ] && continue
  end=$(echo | timeout 10 openssl s_client -connect "$domain:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
  if [ -z "$end" ]; then echo "ERROR  $domain: could not fetch certificate"; continue; fi
  left=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
  if [ "$left" -lt "$DAYS" ]; then echo "WARN   $domain: $left days left"; else echo "OK     $domain: $left days"; fi
done < domains.txt

Run it from cron daily and send output containing WARN or ERROR to email or a chat webhook.

Make sure renewal will work

For Certbot:

sudo certbot renew --dry-run
systemctl list-timers | grep certbot

A failing dry run means the real renewal will fail too — fix it now. Typical causes: port 80 blocked, DNS changed, a server block that no longer serves /.well-known/acme-challenge/, or a CDN in front. See Let’s Encrypt with Certbot.

Also confirm the web server reloads after renewal (Certbot’s Nginx/Apache plugins do; webroot setups need a deploy hook). A renewed certificate on disk doesn’t help if Nginx still serves the old one from memory.

Monitoring tools

  • Uptime Kuma — HTTP monitors can notify you N days before expiry.
  • Prometheus blackbox_exporter — exposes probe_ssl_earliest_cert_expiry; alert when it’s under 14 days. See Prometheus and Grafana.
  • Netdata — has an x509 check collector.
  • Hosted monitors and certificate transparency watchers can alert as well.

Monitor from outside your server, so you test what visitors actually see (including CDN certificates).

Don’t forget non-web certificates

Mail servers (SMTP, IMAP), databases with TLS, VPN servers, internal APIs and load balancers have certificates too:

echo | openssl s_client -connect mail.example.com:587 -starttls smtp 2>/dev/null | openssl x509 -noout -enddate
echo | openssl s_client -connect mail.example.com:993 2>/dev/null | openssl x509 -noout -enddate

Shorter lifetimes are coming

Certificate authorities and browsers are moving toward shorter maximum certificate lifetimes over the next few years. Automated renewal and monitoring are no longer optional — treat any manual certificate step as technical debt.

Related: SSL certificate chain errors and OpenSSL commands.

  • SSL
  • Monitoring
  • Command Line