SSL Certificate Monitoring and Expiry Alerting: Never Let a Cert Expire Again

Certificate expiry is one of those problems that feels embarrassing when it happens. Every time I read about a major site going down because of an expired certificate, I think about how preventable it is. The certificate was always going to expire on a known date. The notification system failed, not the certificate.

This guide covers practical monitoring setups — from simple shell scripts to integration with professional monitoring platforms.

Why Certificates Expire Without Warning

The default Let’s Encrypt reminder email goes to whatever address you registered with. That email address might be an employee who left. It might be a shared mailbox nobody monitors. It might land in spam. Paid CAs send reminders 30, 14, and 7 days before expiry, but again — only to the registered contact email.

Your own monitoring is the reliable layer. The CA’s emails are a bonus.

Checking Expiry from the Command Line

Check a remote server’s certificate expiry:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate

Output: notAfter=Dec 31 23:59:59 2025 GMT

For a local certificate file:

openssl x509 -in /etc/nginx/ssl/certificate.crt -noout -enddate

Get the number of days until expiry:

cert_expiry=$(echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate | cut -d= -f2)
expiry_epoch=$(date -d "$cert_expiry" +%s)
now_epoch=$(date +%s)
days_remaining=$(( (expiry_epoch - now_epoch) / 86400 ))
echo "Certificate expires in $days_remaining days"

Simple Monitoring Script with Email Alerts

This script checks multiple domains and sends an email alert when any certificate is within 30 days of expiry:

#!/bin/bash
# /usr/local/bin/check-ssl-certs.sh

DOMAINS="example.com api.example.com shop.example.com"
ALERT_DAYS=30
ALERT_EMAIL="ops@example.com"

for domain in $DOMAINS; do
    expiry=$(echo | openssl s_client -connect "${domain}:443" -servername "$domain" 2>/dev/null \
        | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)

    if [ -z "$expiry" ]; then
        echo "WARNING: Could not check certificate for $domain" | \
            mail -s "SSL Check Failed: $domain" "$ALERT_EMAIL"
        continue
    fi

    expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$expiry" +%s)
    now_epoch=$(date +%s)
    days_remaining=$(( (expiry_epoch - now_epoch) / 86400 ))

    if [ "$days_remaining" -lt "$ALERT_DAYS" ]; then
        echo "Certificate for $domain expires in $days_remaining days (on $expiry)" | \
            mail -s "SSL Certificate Expiring: $domain ($days_remaining days)" "$ALERT_EMAIL"
    fi
done

Add to cron to run daily:

0 8 * * * /usr/local/bin/check-ssl-certs.sh

Checking Multiple Ports and Non-Standard Configurations

Not everything runs on port 443. Mail servers use 465 and 993. LDAP over TLS uses 636. The same openssl s_client approach works for any port:

# SMTP with STARTTLS
echo | openssl s_client -connect mail.example.com:587 -starttls smtp 2>/dev/null \
  | openssl x509 -noout -enddate

# IMAPS
echo | openssl s_client -connect mail.example.com:993 2>/dev/null \
  | openssl x509 -noout -enddate

# LDAPS
echo | openssl s_client -connect ldap.example.com:636 2>/dev/null \
  | openssl x509 -noout -enddate

Using Prometheus and Grafana for Certificate Monitoring

For teams already running Prometheus, the ssl_exporter provides certificate metrics:

# Install ssl_exporter
wget https://github.com/ribbybibby/ssl_exporter/releases/latest/download/ssl_exporter_linux_amd64.tar.gz
tar xzf ssl_exporter_linux_amd64.tar.gz
sudo mv ssl_exporter /usr/local/bin/

# Start the exporter
ssl_exporter --web.listen-address=:9219

Configure Prometheus targets in prometheus.yml:

scrape_configs:
  - job_name: 'ssl'
    metrics_path: /probe
    static_configs:
      - targets:
        - example.com:443
        - api.example.com:443
        - mail.example.com:465
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: localhost:9219

The key metric is ssl_cert_not_after — the Unix timestamp of certificate expiry. Create a Grafana alert:

# Alert when certificate expires within 30 days
(ssl_cert_not_after - time()) / 86400 < 30

This gives you a dashboard showing all certificates with their days-remaining, and automatic alerts via whatever notification channel Prometheus AlertManager is configured to use.

Zabbix Template for SSL Monitoring

If you’re using Zabbix, the built-in web monitoring can check certificate expiry. In Zabbix 6.0+, there’s a built-in SSL certificate check item type.

For older versions or custom checks, add an external check script:

# /usr/lib/zabbix/externalscripts/check_ssl_cert.sh
#!/bin/bash
DOMAIN=$1
PORT=${2:-443}

expiry=$(echo | openssl s_client -connect "${DOMAIN}:${PORT}" -servername "$DOMAIN" 2>/dev/null \
    | openssl x509 -noout -enddate | cut -d= -f2)

expiry_epoch=$(date -d "$expiry" +%s)
now_epoch=$(date +%s)
echo $(( (expiry_epoch - now_epoch) / 86400 ))

In Zabbix, create an external check item:

  • Type: External check
  • Key: check_ssl_cert.sh[{HOST.CONN},{$SSL_PORT}]
  • Units: days

Add a trigger: {cert_days_remaining} < 30

Uptime Kuma: Simple Web-Based Monitoring

Uptime Kuma is a self-hosted monitoring tool with a clean UI and built-in SSL certificate monitoring. Setting it up with Docker:

docker run -d --restart=always \
  -p 3001:3001 \
  -v uptime-kuma:/app/data \
  --name uptime-kuma \
  louislam/uptime-kuma:1

Add a monitor, set the type to “HTTPS” or “HTTP(s)”, and Uptime Kuma automatically checks and displays SSL certificate expiry. You can configure notifications (email, Slack, Telegram, PagerDuty, and many others) to alert at a configurable number of days before expiry.

For small to medium setups, Uptime Kuma is genuinely excellent — no complex configuration, nice dashboard, and it covers SSL monitoring as a built-in feature.

Certificate Inventory Management

When you manage certificates across many servers, keeping track of what’s where becomes its own problem. A simple approach: maintain a list in a text file or spreadsheet with columns for domain, server, expiry date, CA, and type (DV/OV/wildcard/SAN). Update it every time you add or renew a certificate.

A more scalable approach: use the monitoring tool to discover what’s deployed. Both Prometheus with ssl_exporter and Uptime Kuma build their inventory from what’s actively monitoring. The limitation is that you have to add new domains manually when they’re created.

For large environments with dozens or hundreds of certificates, dedicated certificate lifecycle management tools (Venafi, AppViewX, or open-source options like cert-manager in Kubernetes) provide automated discovery, policy enforcement, and renewal workflows.

Testing Your Monitoring Before You Need It

Set up monitoring, then verify it works before an actual expiry. Create a test with a soon-to-expire certificate (or temporarily lower your alert threshold to 400 days so you get an alert on any valid certificate), confirm the alert fires and reaches the right people.

Monitoring that hasn’t been tested is monitoring that may not work when it matters. The worst time to find out your alerts aren’t reaching anyone is when you have a production outage from an expired certificate.

Scroll to Top