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.