What you will learn
Scan a list of TLS endpoints for certificates nearing expiry and fire a renewal hook automatically, catching a lapsed certificate days before it takes down traffic instead of finding out from a customer.
Before you begin
Requires openssl. This checks certificates as served over the network (the actual thing clients see), not just files on disk — a renewed cert that was never deployed to the load balancer is a real failure mode this catches and a file-based check misses.
1. Extract the expiry date from a live endpoint
#!/usr/bin/env bash
set -Eeuo pipefail
WARN_DAYS=14
ENDPOINTS=("myapp.example.com:443" "api.example.com:443" "admin.example.com:8443")
check_endpoint() {
local endpoint="$1" host port expiry_str expiry_epoch now_epoch days_left
host="${endpoint%%:*}"
port="${endpoint##*:}"
expiry_str="$(echo | openssl s_client -servername "$host" -connect "${host}:${port}" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)"
if [[ -z "$expiry_str" ]]; then
echo "ERROR ${endpoint}: could not retrieve certificate"
return 2
fi
expiry_epoch="$(date -d "$expiry_str" +%s)"
now_epoch="$(date +%s)"
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if (( days_left < 0 )); then
echo "EXPIRED ${endpoint}: expired $((-days_left)) day(s) ago"
return 2
elif (( days_left <= WARN_DAYS )); then
echo "WARN ${endpoint}: expires in ${days_left} day(s)"
return 1
else
echo "OK ${endpoint}: expires in ${days_left} day(s)"
return 0
fi
}
-servername sets SNI on the handshake — without it, a host serving multiple certificates behind one IP (common with reverse proxies) may return the wrong certificate, giving a false expiry reading.
2. Run the check across every endpoint and track worst status
worst_status=0
for ep in "${ENDPOINTS[@]}"; do
check_endpoint "$ep"
status=$?
(( status > worst_status )) && worst_status=$status
done
exit "$worst_status"
Using the highest severity across all endpoints as the script's own exit code lets a cron wrapper or monitoring system treat this as a single pass/fail check while still logging per-endpoint detail.
3. Fire a renewal hook automatically for known-managed certs
if (( worst_status >= 1 )); then
for ep in "${ENDPOINTS[@]}"; do
host="${ep%%:*}"
if [[ -x "/etc/letsencrypt/renewal-hooks/manual/${host}.sh" ]]; then
echo "triggering renewal hook for ${host}"
"/etc/letsencrypt/renewal-hooks/manual/${host}.sh" || true
fi
done
fi
Example output
OK myapp.example.com:443: expires in 62 day(s)
WARN api.example.com:443: expires in 9 day(s)
triggering renewal hook for api.example.com
EXPIRED admin.example.com:8443: expired 2 day(s) ago
Verify the result
Point the endpoint list at a host with a known-short-lived test certificate and confirm the WARN threshold triggers exactly at WARN_DAYS, not off by one.
Troubleshooting
If every endpoint reports "could not retrieve certificate," confirm outbound port 443/8443 isn't blocked from the host running the check, and that openssl s_client isn't hanging on a TCP-level firewall drop — add -connect_timeout 5 (OpenSSL 3+) or wrap the call in timeout 10.
Next steps
Schedule daily under flock-cron-concurrency-guard and alert on WARN/EXPIRED via webhook-alert-dispatcher.