Initial template: TLS cert expiry checker
This commit is contained in:
214
bin/cert-expiry-check.sh
Executable file
214
bin/cert-expiry-check.sh
Executable file
@@ -0,0 +1,214 @@
|
||||
#!/bin/sh
|
||||
# cert-expiry-check.sh - POSIX-friendly TLS cert expiry checker template
|
||||
# Fetches certificate notAfter via openssl s_client, computes days left, prints status lines.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = all OK
|
||||
# 1 = at least one WARN (expiring soon)
|
||||
# 2 = at least one EXPIRED or ERROR
|
||||
# 3 = usage/config error
|
||||
|
||||
set -eu
|
||||
|
||||
# -------------------- defaults (override via config) --------------------
|
||||
WARN_DAYS="${WARN_DAYS:-30}"
|
||||
TIMEOUT="${TIMEOUT:-12}"
|
||||
FORCE_SEND="${FORCE_SEND:-0}"
|
||||
ENABLE_EMAIL="${ENABLE_EMAIL:-0}"
|
||||
|
||||
DOMAINS_FILE="${DOMAINS_FILE:-./config/domains.txt}"
|
||||
CONF_FILE="${CONF_FILE:-./config/cert-expiry.conf}"
|
||||
MSMTP_CONFIG="${MSMTP_CONFIG:-./config/msmtprc}"
|
||||
LOG_FILE="${LOG_FILE:-}" # empty => no logfile
|
||||
|
||||
RECIPIENT="${RECIPIENT:-}"
|
||||
SENDER="${SENDER:-}"
|
||||
|
||||
CHECK_ONLY=0
|
||||
|
||||
# -------------------- helpers --------------------
|
||||
die() { echo "ERROR: $*" >&2; exit 3; }
|
||||
|
||||
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
log() {
|
||||
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
line="$ts $*"
|
||||
echo "$line"
|
||||
if [ -n "$LOG_FILE" ]; then
|
||||
printf '%s\n' "$line" >>"$LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: cert-expiry-check.sh [--check-only] [--domains FILE] [--config FILE]
|
||||
--check-only Do not send email, just print results and exit with status policy
|
||||
--domains FILE Use a custom domains file (default: ./config/domains.txt)
|
||||
--config FILE Use a custom config file (default: ./config/cert-expiry.conf)
|
||||
USAGE
|
||||
}
|
||||
|
||||
# -------------------- args --------------------
|
||||
while [ "${1:-}" != "" ]; do
|
||||
case "$1" in
|
||||
--check-only) CHECK_ONLY=1 ;;
|
||||
--domains) shift; [ -n "${1:-}" ] || die "--domains requires a file"; DOMAINS_FILE="$1" ;;
|
||||
--config) shift; [ -n "${1:-}" ] || die "--config requires a file"; CONF_FILE="$1" ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) die "Unknown arg: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# -------------------- load config (optional) --------------------
|
||||
if [ -f "$CONF_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$CONF_FILE"
|
||||
fi
|
||||
|
||||
# -------------------- validate --------------------
|
||||
have_cmd openssl || die "openssl not found"
|
||||
have_cmd date || die "date not found"
|
||||
[ -f "$DOMAINS_FILE" ] || die "Domains file not found: $DOMAINS_FILE"
|
||||
|
||||
# date string -> epoch (GNU date)
|
||||
to_epoch() {
|
||||
# input like: "Jul 5 12:00:00 2026 GMT"
|
||||
date -u -d "$1" +%s 2>/dev/null
|
||||
}
|
||||
|
||||
normalize_target() {
|
||||
# host or host:port -> host:port (default 443)
|
||||
case "$1" in
|
||||
*:*) printf '%s\n' "$1" ;;
|
||||
*) printf '%s:443\n' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_notafter() {
|
||||
hp="$1"
|
||||
host="${hp%:*}"
|
||||
port="${hp##*:}"
|
||||
|
||||
# Use timeout if available, else best-effort.
|
||||
if have_cmd timeout; then
|
||||
timeout "$TIMEOUT" openssl s_client -servername "$host" -connect "$host:$port" </dev/null 2>/dev/null \
|
||||
| openssl x509 -noout -enddate 2>/dev/null \
|
||||
| sed 's/^notAfter=//'
|
||||
else
|
||||
openssl s_client -servername "$host" -connect "$host:$port" </dev/null 2>/dev/null \
|
||||
| openssl x509 -noout -enddate 2>/dev/null \
|
||||
| sed 's/^notAfter=//'
|
||||
fi
|
||||
}
|
||||
|
||||
send_email() {
|
||||
# Returns 0 on success, non-zero on failure.
|
||||
have_cmd msmtp || return 1
|
||||
[ -n "$RECIPIENT" ] || return 1
|
||||
[ -n "$SENDER" ] || return 1
|
||||
[ -f "$MSMTP_CONFIG" ] || return 1
|
||||
|
||||
subject="[cert-expiry] warn=$warn_count expired=$expired_count error=$error_count"
|
||||
{
|
||||
printf 'From: %s\n' "$SENDER"
|
||||
printf 'To: %s\n' "$RECIPIENT"
|
||||
printf 'Subject: %s\n' "$subject"
|
||||
printf '\n'
|
||||
printf '%s\n' "$summary_line"
|
||||
printf '\n'
|
||||
# mail_body contains \n escapes
|
||||
printf "%b" "$mail_body"
|
||||
} | msmtp -C "$MSMTP_CONFIG" -t
|
||||
}
|
||||
|
||||
# -------------------- run --------------------
|
||||
now_epoch="$(date -u +%s)"
|
||||
|
||||
ok_count=0
|
||||
warn_count=0
|
||||
expired_count=0
|
||||
error_count=0
|
||||
|
||||
mail_body=""
|
||||
|
||||
log "Certificate expiry report ($(date -u))"
|
||||
log "Domains file: $DOMAINS_FILE"
|
||||
log "Threshold: ${WARN_DAYS} days"
|
||||
|
||||
while IFS= read -r raw || [ -n "$raw" ]; do
|
||||
# trim trailing spaces
|
||||
line="$(printf '%s' "$raw" | sed 's/[[:space:]]*$//')"
|
||||
# skip comments/empty
|
||||
case "$line" in
|
||||
""|\#*) continue ;;
|
||||
esac
|
||||
|
||||
target="$(normalize_target "$line")"
|
||||
notafter="$(get_notafter "$target" || true)"
|
||||
|
||||
if [ -z "$notafter" ]; then
|
||||
error_count=$((error_count + 1))
|
||||
log "ERROR $target could_not_fetch_cert_enddate"
|
||||
mail_body="${mail_body}ERROR $target could_not_fetch_cert_enddate\n"
|
||||
continue
|
||||
fi
|
||||
|
||||
exp_epoch="$(to_epoch "$notafter" || true)"
|
||||
if [ -z "${exp_epoch:-}" ]; then
|
||||
error_count=$((error_count + 1))
|
||||
log "ERROR $target could_not_parse_notAfter='$notafter'"
|
||||
mail_body="${mail_body}ERROR $target could_not_parse_notAfter='$notafter'\n"
|
||||
continue
|
||||
fi
|
||||
|
||||
seconds_left=$((exp_epoch - now_epoch))
|
||||
days_left=$((seconds_left / 86400))
|
||||
|
||||
if [ "$seconds_left" -lt 0 ]; then
|
||||
expired_count=$((expired_count + 1))
|
||||
log "EXPIRED $target days_left=$days_left notAfter='$notafter'"
|
||||
mail_body="${mail_body}EXPIRED $target days_left=$days_left notAfter='$notafter'\n"
|
||||
elif [ "$days_left" -le "$WARN_DAYS" ]; then
|
||||
warn_count=$((warn_count + 1))
|
||||
log "WARN $target days_left=$days_left notAfter='$notafter'"
|
||||
mail_body="${mail_body}WARN $target days_left=$days_left notAfter='$notafter'\n"
|
||||
else
|
||||
ok_count=$((ok_count + 1))
|
||||
log "OK $target days_left=$days_left notAfter='$notafter'"
|
||||
fi
|
||||
done <"$DOMAINS_FILE"
|
||||
|
||||
summary_line="SUMMARY ok=$ok_count warn=$warn_count expired=$expired_count error=$error_count warn_days=$WARN_DAYS"
|
||||
log "$summary_line"
|
||||
|
||||
# Email decision
|
||||
should_send=0
|
||||
if [ "$ENABLE_EMAIL" -eq 1 ] && [ "$CHECK_ONLY" -eq 0 ]; then
|
||||
if [ "$FORCE_SEND" -eq 1 ]; then
|
||||
should_send=1
|
||||
elif [ "$warn_count" -gt 0 ] || [ "$expired_count" -gt 0 ] || [ "$error_count" -gt 0 ]; then
|
||||
should_send=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$should_send" -eq 1 ]; then
|
||||
if send_email; then
|
||||
log "EMAIL sent_to=$RECIPIENT"
|
||||
else
|
||||
log "EMAIL failed (msmtp/config/recipient/sender missing)"
|
||||
error_count=$((error_count + 1))
|
||||
fi
|
||||
else
|
||||
log "EMAIL skipped (enable_email=$ENABLE_EMAIL check_only=$CHECK_ONLY force_send=$FORCE_SEND)"
|
||||
fi
|
||||
|
||||
# -------------------- exit code policy (FIXED) --------------------
|
||||
if [ "$expired_count" -gt 0 ] || [ "$error_count" -gt 0 ]; then
|
||||
exit 2
|
||||
elif [ "$warn_count" -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
Reference in New Issue
Block a user