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.