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.