Certbot renews at 30 days remaining, so a healthy certificate never falls below it and the old 14 day threshold could only fire after renewal had been broken for 16 straight days, leaving 14 days to react. 25 days fires about five days after the first failed renewal.
104 lines
3.0 KiB
Bash
104 lines
3.0 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
set -uo pipefail
|
|
|
|
CERT_DIR="${CERT_DIR:-/etc/letsencrypt/live}"
|
|
EXPIRY_DAYS="${EXPIRY_DAYS:-25}"
|
|
NTFY_URL="${NTFY_URL:-http://127.0.0.1:2586/certbot}"
|
|
NTFY_TITLE="${NTFY_TITLE:-SSL certificate warning}"
|
|
NTFY_PRIORITY="${NTFY_PRIORITY:-high}"
|
|
NTFY_TAGS="${NTFY_TAGS:-warning,lock}"
|
|
NTFY_TOKEN="${NTFY_TOKEN:-}"
|
|
ALERT_ON_NO_CERTS="${ALERT_ON_NO_CERTS:-true}"
|
|
|
|
host="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "unknown-host")"
|
|
now_epoch="$(date +%s)"
|
|
threshold_seconds=$((EXPIRY_DAYS * 86400))
|
|
|
|
send_alert() {
|
|
local message="$1"
|
|
local curl_args=(
|
|
-fsS
|
|
-H "Title: ${NTFY_TITLE}"
|
|
-H "Priority: ${NTFY_PRIORITY}"
|
|
-H "Tags: ${NTFY_TAGS}"
|
|
)
|
|
|
|
if [[ -n "${NTFY_TOKEN}" ]]; then
|
|
curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
|
|
fi
|
|
|
|
curl "${curl_args[@]}" --data-binary "${message}" "${NTFY_URL}" >/dev/null
|
|
}
|
|
|
|
fail() {
|
|
local message="$1"
|
|
send_alert "${message}" || echo "Failed to send ntfy alert to ${NTFY_URL}" >&2
|
|
echo "${message}" >&2
|
|
exit 1
|
|
}
|
|
|
|
if ! command -v openssl >/dev/null 2>&1; then
|
|
fail "SSL certificate check failed on ${host}: openssl is not installed."
|
|
fi
|
|
|
|
if ! command -v curl >/dev/null 2>&1; then
|
|
echo "SSL certificate check failed on ${host}: curl is not installed." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -d "${CERT_DIR}" ]]; then
|
|
fail "SSL certificate check failed on ${host}: certificate directory ${CERT_DIR} does not exist."
|
|
fi
|
|
|
|
warnings=()
|
|
cert_count=0
|
|
|
|
while IFS= read -r -d '' cert_path; do
|
|
cert_count=$((cert_count + 1))
|
|
cert_name="$(basename "$(dirname "${cert_path}")")"
|
|
|
|
enddate_line="$(openssl x509 -in "${cert_path}" -noout -enddate 2>/dev/null)"
|
|
if [[ -z "${enddate_line}" ]]; then
|
|
warnings+=("${cert_name}: could not read expiry date from ${cert_path}")
|
|
continue
|
|
fi
|
|
|
|
not_after="${enddate_line#notAfter=}"
|
|
expiry_epoch="$(date -d "${not_after}" +%s 2>/dev/null)"
|
|
if [[ -z "${expiry_epoch}" ]]; then
|
|
warnings+=("${cert_name}: could not parse expiry date '${not_after}'")
|
|
continue
|
|
fi
|
|
|
|
seconds_left=$((expiry_epoch - now_epoch))
|
|
days_left=$((seconds_left / 86400))
|
|
|
|
if (( seconds_left < 0 )); then
|
|
warnings+=("${cert_name}: EXPIRED on ${not_after}")
|
|
elif ! openssl x509 -checkend "${threshold_seconds}" -noout -in "${cert_path}" >/dev/null 2>&1; then
|
|
warnings+=("${cert_name}: expires in ${days_left} day(s), on ${not_after}")
|
|
fi
|
|
done < <(find "${CERT_DIR}" -mindepth 2 -maxdepth 2 \( -type f -o -type l \) -name cert.pem -print0)
|
|
|
|
if (( cert_count == 0 )); then
|
|
if [[ "${ALERT_ON_NO_CERTS}" == "true" ]]; then
|
|
fail "SSL certificate check found no cert.pem files under ${CERT_DIR} on ${host}."
|
|
fi
|
|
|
|
echo "No certificates found under ${CERT_DIR}; no alert sent."
|
|
exit 0
|
|
fi
|
|
|
|
if (( ${#warnings[@]} > 0 )); then
|
|
message="SSL certificate expiry warning on ${host}. Threshold: ${EXPIRY_DAYS} day(s).
|
|
|
|
$(printf '%s\n' "${warnings[@]}")"
|
|
|
|
send_alert "${message}" || echo "Failed to send ntfy alert to ${NTFY_URL}" >&2
|
|
echo "${message}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "All ${cert_count} certificate(s) are valid for more than ${EXPIRY_DAYS} day(s)."
|