How to get alerted before your SSL certificate expires
A certificate doesn't warn you when it's about to expire — it just stops working at a specific second, and suddenly every browser, API client, and health check throws a hard error. The fix is trivial (renew the cert), but only if you find out before the deadline instead of from an angry customer. This post walks through five practical ways to build an SSL certificate expiration alert, from a one-line cron job to dedicated monitoring, with honest tradeoffs for each.
Why silent expiry causes outages
TLS certificates have a fixed notAfter date. The moment the clock passes it, clients reject the connection rather than degrade gracefully. You see CERT_HAS_EXPIRED in browsers, SSL certificate problem: certificate has expired from curl, and TLS handshake failures in every dependent service.
What makes this dangerous:
- It's binary. There's no slow degradation. The site works perfectly at 23:59:58 and is fully down at 00:00:00.
- Automation hides it. Auto-renewal (certbot, cert-manager, ACME) usually works — until a renewal hook fails silently, a DNS challenge breaks, or a cert on a load balancer or internal service was never automated in the first place.
- It cascades. Expired certs on internal mTLS endpoints, message brokers, or database connections take down things that have nothing to do with your public website.
The goal is to know the notAfter date of every cert you depend on and get a nudge well before it arrives. For a deeper tour of the whole problem space, see the complete SSL certificate monitoring guide.
1. Calendar reminders (and why they fail)
The simplest approach: when you issue a cert, create a calendar event 14 days before expiry.
It costs nothing and needs no infrastructure, but it fails in predictable ways:
- It's manual and unverified — nobody confirms the cert was actually renewed; the event just gets dismissed.
- It drifts from reality. Re-issue a cert early and the reminder is now wrong.
- It doesn't scale. Ten domains with SAN entries and staging variants become an unmanageable pile of events.
- It assumes the deployed cert matches your records. It checks your memory, not the live endpoint.
Use it as a backstop, never as your primary alert.
2. A cron + openssl script that emails you
This is the first approach that actually inspects the live certificate. The idea: connect to the endpoint, read notAfter, compute days remaining, and email when it drops below a threshold.
#!/usr/bin/env bash
set -euo pipefail
# Domains to check (host:port). Default port 443 if omitted.
DOMAINS=("example.com:443" "api.example.com:443" "internal.example.com:8443")
THRESHOLD_DAYS=21
ALERT_EMAIL="ops@example.com"
for entry in "${DOMAINS[@]}"; do
host="${entry%%:*}"
port="${entry##*:}"
[ "$host" = "$port" ] && port=443
# Pull the leaf cert's expiry date from the live endpoint.
end_date=$(echo | openssl s_client -servername "$host" \
-connect "${host}:${port}" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null \
| cut -d= -f2) || true
if [ -z "${end_date:-}" ]; then
printf 'WARNING: could not read certificate for %s:%s\n' "$host" "$port" \
| mail -s "SSL check failed: $host" "$ALERT_EMAIL"
continue
fi
# GNU date. On macOS use: date -j -f "%b %e %T %Y %Z" "$end_date" +%s
end_epoch=$(date -d "$end_date" +%s)
now_epoch=$(date +%s)
days_left=$(( (end_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt "$THRESHOLD_DAYS" ]; then
printf 'Certificate for %s:%s expires in %s day(s) — on %s\n' \
"$host" "$port" "$days_left" "$end_date" \
| mail -s "SSL expiring soon: $host ($days_left days)" "$ALERT_EMAIL"
fi
done
Schedule it daily via cron:
# Run every day at 08:00, log output for debugging.
0 8 * * * /opt/scripts/check-ssl-expiry.sh >> /var/log/ssl-check.log 2>&1
Coverage: checks the actual deployed cert, including internal hosts on non-standard ports. Tradeoffs: you own the upkeep — the box must stay reachable, mail (or an SMTP relay) must be configured, the domain list drifts out of date, and the script itself is a single point of failure. If the cron host's own cert or its outbound mail breaks, you get silence, which looks identical to "all good." Want to spot-check a single host without deploying anything? Use the SSL checker tool.
3. Prometheus blackbox-exporter
If you already run Prometheus, the blackbox exporter probes HTTPS endpoints and exports probe_ssl_earliest_cert_expiry — a Unix timestamp of the earliest-expiring cert in the chain. No script to maintain; alerting lives next to the rest of your monitoring.
Point a scrape job at the exporter with your targets:
scrape_configs:
- job_name: blackbox-ssl
metrics_path: /probe
params:
module: [http_2xx] # blackbox module that performs a TLS handshake
static_configs:
- targets:
- https://example.com
- https://api.example.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
Then an alert rule that fires when expiry is less than 21 days out:
groups:
- name: ssl-expiry
rules:
- alert: SSLCertExpiringSoon
expr: (probe_ssl_earliest_cert_expiry - time()) / 86400 < 21
for: 1h
labels:
severity: warning
annotations:
summary: "TLS cert for {{ $labels.instance }} expires soon"
description: "{{ $labels.instance }} expires in {{ $value | printf \"%.1f\" }} days."
- alert: SSLCertExpiringCritical
expr: (probe_ssl_earliest_cert_expiry - time()) / 86400 < 7
for: 1h
labels:
severity: critical
Coverage: strong, with two severity tiers and routing through Alertmanager (PagerDuty, Slack, email). Tradeoffs: you need a running Prometheus + Alertmanager + blackbox exporter stack, and you have to remember to add every new endpoint to the target list. Overkill if TLS expiry is the only thing you're monitoring.
4. Generic uptime tools
Many uptime monitors (UptimeRobot, Pingdom, self-hosted Uptime Kuma) include an SSL-expiry check alongside HTTP monitoring.
- Pros: zero infrastructure, you likely already run one, and one config covers both "is it up" and "is the cert valid."
- Cons: SSL checks are often a coarse secondary feature — fixed thresholds, only the public-facing leaf cert, no view into the full chain or intermediate expiry, and no checks for hosts that aren't HTTP endpoints (SMTP, message brokers, internal mTLS).
Good enough for a handful of public sites; thin once your TLS surface gets interesting.
5. Dedicated SSL monitoring
A purpose-built monitor checks each cert daily from outside your network, tracks the whole chain, and escalates as expiry approaches without you maintaining a script or a Prometheus stack.
What dedicated tooling adds over the DIY options:
- Chain awareness — flags an intermediate expiring before the leaf, a frequent silent outage cause.
- Issuer and config drift — notices when a cert is reissued, the issuer changes, or the served hostname stops matching.
- External vantage point — the check fails independently of your own infrastructure, so an outage doesn't also kill your alerting.
- No upkeep — no cron host to patch, no exporter to babysit, no mail relay to keep alive.
Comparison: maintenance vs. coverage
| Approach | Coverage | Maintenance burden | |---|---|---| | Calendar reminders | Very low — checks memory, not the cert | Low, but manual and error-prone | | cron + openssl | Good — live leaf cert, any host/port | High — script, host, and mail are yours | | Prometheus blackbox | Strong — tiers, full alert routing | Medium-high — full stack to run | | Generic uptime tool | Moderate — public leaf cert only | Low | | Dedicated SSL monitor | Strong — full chain, external checks | Very low |
DIY genuinely works. The honest caveat is upkeep: a cron script or Prometheus rule is only as reliable as the box it runs on and the person who remembers to add new domains. The moment your alerting depends on the same infrastructure that might go down, you have a blind spot.
If you're terminating TLS yourself, pair any of these with solid renewal automation — see the nginx guide for reloading certs cleanly after renewal so a successful renewal actually reaches the listener.
Monitor it automatically
If you'd rather not maintain a script or a metrics stack, SSLNudge checks your certificates every day from outside your network, watches the full chain, and alerts you well before the notAfter date — so a renewal that slips through the cracks becomes an email, not an outage. Add a domain, set a threshold, and let it watch the calendar so you don't have to.
Stop tracking expiry dates by hand
SSLNudge checks your certificates daily and alerts you before they expire.