SSL certificate expiry is one of the most preventable outages in web operations — and one of the most common. The fix is simple: get an email alert far enough in advance to act on it. In practice, this turns out to be harder than it sounds, because the places you'd expect the alert to come from tend to fail silently.
This guide explains why SSL expiry email alerts get missed, how to build a working bash script that sends alerts directly from your server using openssl and sendmail, and the limitations you'll hit as the number of certificates grows.
There are several common alert paths for SSL expiry, and each has a characteristic failure mode:
Most certificate authorities and domain registrars send renewal reminder emails 30 and 7 days before expiry. These emails go to the address on file when the certificate was purchased — which may be a colleague who left the company, a shared inbox that nobody actively monitors, or a personal email that the original purchaser used when setting up a freelance project. When the team rotates or the original owner moves on, the alert path breaks.
Let's Encrypt sends an expiry notification to the email address registered when the certificate was created. This has the same problem: the email goes to whoever ran certbot certonly the first time, not to whoever is currently responsible for the infrastructure. If auto-renewal is configured correctly, these emails become background noise and people stop reading them. If auto-renewal breaks, the emails arrive but are ignored because they've always arrived and the cert has always renewed.
Cron jobs that send alerts via mail or sendmail on the same server route to the local mail spool (/var/mail/root or similar). On most production Linux servers, nobody reads the local mail spool. These alerts are functionally invisible unless you've explicitly configured mail forwarding.
The most reliable DIY approach is a script that checks expiry directly and sends an email via SMTP — bypassing the local mail spool and going directly to an address you actually monitor:
#!/bin/bash
# ssl-expiry-alert.sh
# Dependencies: openssl, curl (for SMTP via curl, or configure sendmail/postfix)
DOMAINS=(
"example.com"
"api.example.com"
"staging.example.com"
)
WARN_DAYS=30
ALERT_FROM="monitoring@example.com"
ALERT_TO="ops@example.com"
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_USER="monitoring@example.com"
SMTP_PASS="your-app-password" # Use an app password, not your main password
send_alert_email() {
local domain="$1"
local days="$2"
local expiry="$3"
local subject="SSL EXPIRY ALERT: $domain expires in $days days"
local body="SSL Certificate Expiry Warning
Domain: $domain
Expires: $expiry
Days left: $days
Action required: renew the certificate before expiry to avoid browser security warnings.
-- SSL Monitoring Script"
curl -s --ssl-reqd \
--url "smtps://$SMTP_HOST:$SMTP_PORT" \
--user "$SMTP_USER:$SMTP_PASS" \
--mail-from "$ALERT_FROM" \
--mail-rcpt "$ALERT_TO" \
--upload-file - <
To: Ops Team <$ALERT_TO>
Subject: $subject
$body
EOF
}
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 "$(date): WARNING — could not retrieve cert for $DOMAIN"
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=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
echo "$(date): $DOMAIN — $DAYS days until expiry ($EXPIRY)"
if [ "$DAYS" -le "$WARN_DAYS" ]; then
echo "$(date): Sending alert for $DOMAIN ($DAYS days)"
send_alert_email "$DOMAIN" "$DAYS" "$EXPIRY"
fi
done
Schedule it in crontab to run daily:
# Run daily at 8am, log output
0 8 * * * /usr/local/bin/ssl-expiry-alert.sh >> /var/log/ssl-expiry-alerts.log 2>&1
A single alert 30 days out is easy to miss or defer. A better approach is escalating alerts at multiple thresholds — with increasing urgency:
WARN_DAYS_CRITICAL=7
WARN_DAYS_HIGH=14
WARN_DAYS_MEDIUM=30
if [ "$DAYS" -le "$WARN_DAYS_CRITICAL" ]; then
SUBJECT="🚨 CRITICAL: SSL cert for $DOMAIN expires in $DAYS days"
PRIORITY="high"
elif [ "$DAYS" -le "$WARN_DAYS_HIGH" ]; then
SUBJECT="⚠️ URGENT: SSL cert for $DOMAIN expires in $DAYS days"
PRIORITY="normal"
elif [ "$DAYS" -le "$WARN_DAYS_MEDIUM" ]; then
SUBJECT="SSL cert for $DOMAIN expires in $DAYS days — action needed"
PRIORITY="low"
else
# No alert needed
continue
fi
This structure means you get a low-urgency prompt at 30 days, a higher-urgency prompt at 14, and an actionable critical alert at 7. The critical alert should also go to a channel you're guaranteed to see promptly (Slack, SMS, PagerDuty) rather than only email.
The most important thing you can do after setting up any alert script is confirm it actually delivers the email before depending on it. Set the warning threshold temporarily to a large number to force the alert to fire:
# Test: force alert for any cert with less than 9999 days remaining
WARN_DAYS=9999 bash /usr/local/bin/ssl-expiry-alert.sh
If you don't receive the email within a few minutes, check:
curl --mail-from ... --mail-rcpt ... callThe openssl + curl + cron script works well for a handful of domains on infrastructure you control. It has real limitations:
For personal projects and small deployments, the script is a practical solution. For any domain where SSL expiry has material business impact, external monitoring that sends from a reliable email infrastructure is worth the low cost.
Check your SSL certificate right now with the free SSL Certificate Checker — see expiry date, issuer, and chain validity instantly. SSLWatch monitors your certificates continuously and sends escalating email alerts before expiry, from an infrastructure built for reliable delivery.
Check SSL certificate → Get automated alerts with SSLWatch