Skip to content
Go back

Certificate Expiry: Monitor Before the 3 AM Call

By SumGuy 4 min read
Certificate Expiry: Monitor Before the 3 AM Call

Nobody notices your certificate is expiring until your users start seeing browser warnings. Then your phone rings at 3 AM. Here’s the thing: monitoring cert expiry is trivial and you’re probably not doing it.

I’ve been on too many calls where someone says “but I thought certbot was set to auto-renew.” It was. But it wasn’t running. Or the renewal ran but failed silently. Or the certificate was never installed via certbot in the first place — it’s just a file sitting on the server.

Let’s fix that with a simple monitoring strategy that actually works.

Quick Manual Check: openssl

First, let’s just look at what we have. If you’re running a web server with TLS, use this:

Terminal window
$ openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | \
openssl x509 -noout -dates

Output:

notBefore=May 28 00:00:00 2025 GMT
notAfter=May 27 23:59:59 2026 GMT

That notAfter is your expiration. If it’s less than 30 days away, start sweating.

For a local cert file:

Terminal window
$ openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates

Check certbot Renewals (If You’re Using Let’s Encrypt)

If you’re running certbot, test the renewal process without actually renewing:

Terminal window
$ sudo certbot renew --dry-run

This simulates the entire renewal workflow. If it fails, you see the error immediately. Problems like:

All visible in the output. If this passes, your renewals should work.

For real-world certs, also check the renewal log:

Terminal window
$ tail -f /var/log/letsencrypt/letsencrypt.log

Automated Monitoring: Cron + Expiry Check

Here’s a bash script that checks expiry and emails you before it’s too late:

check_cert_expiry.sh
#!/bin/bash
CERT_PATH="/etc/letsencrypt/live/example.com/fullchain.pem"
WARNING_DAYS=30
EMAIL="ops@example.com"
HOSTNAME=$(hostname)
# Get expiry date in seconds since epoch
EXPIRY_EPOCH=$(openssl x509 -in "$CERT_PATH" -noout -enddate | cut -d= -f2 | date -f - +%s)
NOW=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW) / 86400 ))
if [ "$DAYS_LEFT" -lt "$WARNING_DAYS" ]; then
SUBJECT="ALERT: Certificate expiring in $DAYS_LEFT days on $HOSTNAME"
MESSAGE="Certificate $CERT_PATH expires in $DAYS_LEFT days.
Expiry date: $(date -d @$EXPIRY_EPOCH)
Action: Run 'sudo certbot renew --force-renewal' to renew immediately."
echo "$MESSAGE" | mail -s "$SUBJECT" "$EMAIL"
exit 1
fi
exit 0

Add to crontab to run daily:

Terminal window
$ sudo crontab -e
# Check SSL certs every morning at 2 AM
0 2 * * * /usr/local/bin/check_cert_expiry.sh

Now you get an email 30 days before expiry. No surprises.

The Advanced Path: Uptime-Kuma Cert Monitoring

If you’re already running Uptime-Kuma (the excellent self-hosted monitoring tool), you get cert monitoring almost for free:

  1. Create a monitor for your domain (HTTP/HTTPS)
  2. Uptime-Kuma checks the cert automatically on each request
  3. Set up a notification that fires when cert is within N days of expiry

No additional scripts needed. Just go to Uptime-Kuma’s monitor settings and enable TLS expiry alerts. It works for any domain you’re monitoring.

Check All Your Certs (The Audit)

If you’re managing multiple services, make a list:

audit_all_certs.sh
#!/bin/bash
# Domains you care about
DOMAINS=(
"example.com"
"api.example.com"
"dashboard.example.com"
)
for domain in "${DOMAINS[@]}"; do
echo "=== $domain ==="
openssl s_client -connect "$domain:443" -servername "$domain" </dev/null 2>/dev/null | \
openssl x509 -noout -dates -subject
done

Run it manually every quarter. Better yet, wrap it in that cron script and email the full report.

Self-Signed Certs (The Forgotten Ones)

Self-signed certs are the ones that bite you the hardest because nobody remembers they exist. If you’re using self-signed certs (internal services, testing):

Terminal window
$ find /etc -name "*.pem" -o -name "*.crt" 2>/dev/null | while read cert; do
echo "=== $cert ==="
openssl x509 -in "$cert" -noout -enddate 2>/dev/null || echo "Not a valid cert"
done

Make a list. Add those to your monitoring too.

The One Thing You Must Do

Right now, before you finish reading this:

Terminal window
$ sudo certbot renew --dry-run

If that passes, you’re good. If it fails, fix it today. That’s it. That’s your insurance policy against the 3 AM call.

The rest — cron scripts, Uptime-Kuma alerts, automated emails — that’s just being responsible. But the dry-run test, right now, is mandatory.

Monitor Your Internal Services Too

If you’re self-hosting behind a reverse proxy, your internal services have certs too. Don’t forget them:

Terminal window
# Check a local cert (replace port/hostname)
echo | openssl s_client -connect localhost:8443 2>/dev/null | openssl x509 -noout -enddate

Add your internal domains to whatever monitoring you set up. A cert expiring on your internal Gitea or Nextcloud is just as painful as a public one.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
jq One-Liners Every Sysadmin Needs
Next Post
xargs vs while read: Which One and When

Related Posts