135 lines
4.5 KiB
Bash
135 lines
4.5 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
set -o errexit
|
||
|
|
set -o nounset
|
||
|
|
set -o pipefail
|
||
|
|
|
||
|
|
host_name=""
|
||
|
|
port=443
|
||
|
|
cert_file=""
|
||
|
|
warning_days=30
|
||
|
|
critical_days=7
|
||
|
|
servername=""
|
||
|
|
|
||
|
|
usage() {
|
||
|
|
cat <<'USAGE'
|
||
|
|
Usage: check_certificate_expiry.sh (--host HOST [--port PORT] | --file CERT_FILE) [--servername SNI_NAME] [--warning-days DAYS] [--critical-days DAYS] [--help]
|
||
|
|
|
||
|
|
Check TLS certificate expiry for a remote endpoint or local certificate file.
|
||
|
|
USAGE
|
||
|
|
}
|
||
|
|
|
||
|
|
is_number() {
|
||
|
|
[[ "$1" =~ ^[0-9]+$ ]]
|
||
|
|
}
|
||
|
|
|
||
|
|
while (($# > 0)); do
|
||
|
|
case "$1" in
|
||
|
|
--host) [[ $# -ge 2 ]] || { printf 'CRITICAL: --host requires a value\n'; exit 2; }; host_name="$2"; shift 2 ;;
|
||
|
|
--port) [[ $# -ge 2 ]] || { printf 'CRITICAL: --port requires a value\n'; exit 2; }; port="$2"; shift 2 ;;
|
||
|
|
--file) [[ $# -ge 2 ]] || { printf 'CRITICAL: --file requires a value\n'; exit 2; }; cert_file="$2"; shift 2 ;;
|
||
|
|
--servername) [[ $# -ge 2 ]] || { printf 'CRITICAL: --servername requires a value\n'; exit 2; }; servername="$2"; shift 2 ;;
|
||
|
|
--warning-days) [[ $# -ge 2 ]] || { printf 'CRITICAL: --warning-days requires a value\n'; exit 2; }; warning_days="$2"; shift 2 ;;
|
||
|
|
--critical-days) [[ $# -ge 2 ]] || { printf 'CRITICAL: --critical-days requires a value\n'; exit 2; }; critical_days="$2"; shift 2 ;;
|
||
|
|
--help|-h) usage; exit 0 ;;
|
||
|
|
*) printf 'CRITICAL: unknown option: %s\n' "$1"; usage; exit 2 ;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
|
||
|
|
if ! command -v openssl >/dev/null 2>&1; then
|
||
|
|
printf 'CRITICAL: required command not found: openssl\n'
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
for value in "$port" "$warning_days" "$critical_days"; do
|
||
|
|
if ! is_number "$value"; then
|
||
|
|
printf 'CRITICAL: numeric option expected, got: %s\n' "$value"
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
if ((critical_days >= warning_days)); then
|
||
|
|
printf 'CRITICAL: --critical-days must be lower than --warning-days\n'
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
if [[ -n "$host_name" && -n "$cert_file" ]]; then
|
||
|
|
printf 'CRITICAL: use either --host or --file, not both\n'
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
if [[ -z "$host_name" && -z "$cert_file" ]]; then
|
||
|
|
printf 'CRITICAL: either --host or --file is required\n'
|
||
|
|
usage
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
if [[ -n "$cert_file" && ! -r "$cert_file" ]]; then
|
||
|
|
printf 'CRITICAL: certificate file is not readable: %s\n' "$cert_file"
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
if [[ -z "$servername" ]]; then
|
||
|
|
servername="$host_name"
|
||
|
|
fi
|
||
|
|
|
||
|
|
tmp_cert="$(mktemp)"
|
||
|
|
trap 'rm -f "$tmp_cert"' EXIT
|
||
|
|
|
||
|
|
if [[ -n "$host_name" ]]; then
|
||
|
|
if ! openssl s_client -connect "${host_name}:${port}" -servername "$servername" -showcerts </dev/null 2>/dev/null \
|
||
|
|
| openssl x509 -outform PEM > "$tmp_cert" 2>/dev/null; then
|
||
|
|
printf 'CRITICAL: unable to retrieve certificate from %s:%s\n' "$host_name" "$port"
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
cp "$cert_file" "$tmp_cert"
|
||
|
|
fi
|
||
|
|
|
||
|
|
subject="$(openssl x509 -in "$tmp_cert" -noout -subject 2>/dev/null | sed 's/^subject=//')"
|
||
|
|
issuer="$(openssl x509 -in "$tmp_cert" -noout -issuer 2>/dev/null | sed 's/^issuer=//')"
|
||
|
|
not_before="$(openssl x509 -in "$tmp_cert" -noout -startdate 2>/dev/null | sed 's/^notBefore=//')"
|
||
|
|
not_after="$(openssl x509 -in "$tmp_cert" -noout -enddate 2>/dev/null | sed 's/^notAfter=//')"
|
||
|
|
san_text="$(openssl x509 -in "$tmp_cert" -noout -ext subjectAltName 2>/dev/null | sed '1d' | sed 's/^ *//')"
|
||
|
|
|
||
|
|
expiry_epoch="$(date -d "$not_after" +%s 2>/dev/null || printf '')"
|
||
|
|
now_epoch="$(date +%s)"
|
||
|
|
if [[ -z "$expiry_epoch" ]]; then
|
||
|
|
printf 'CRITICAL: unable to parse certificate expiry date: %s\n' "$not_after"
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
seconds_left=$((expiry_epoch - now_epoch))
|
||
|
|
days_left=$((seconds_left / 86400))
|
||
|
|
|
||
|
|
status="OK"
|
||
|
|
exit_code=0
|
||
|
|
if ((days_left < critical_days)); then
|
||
|
|
status="CRITICAL"
|
||
|
|
exit_code=3
|
||
|
|
elif ((days_left < warning_days)); then
|
||
|
|
status="WARNING"
|
||
|
|
exit_code=1
|
||
|
|
fi
|
||
|
|
|
||
|
|
target="$cert_file"
|
||
|
|
if [[ -n "$host_name" ]]; then
|
||
|
|
target="${host_name}:${port}"
|
||
|
|
fi
|
||
|
|
|
||
|
|
printf '%s: Certificate for %s expires in %s day(s)\n\n' "$status" "$target" "$days_left"
|
||
|
|
|
||
|
|
printf 'Certificate details:\n'
|
||
|
|
printf 'Subject: %s\n' "$subject"
|
||
|
|
printf 'Issuer: %s\n' "$issuer"
|
||
|
|
printf 'notBefore: %s\n' "$not_before"
|
||
|
|
printf 'notAfter: %s\n' "$not_after"
|
||
|
|
printf 'SAN/CN: %s\n' "${san_text:-$subject}"
|
||
|
|
printf '\n'
|
||
|
|
|
||
|
|
printf 'Evidence:\n'
|
||
|
|
printf 'Target: %s\n' "$target"
|
||
|
|
printf 'SNI: %s\n' "${servername:-not used}"
|
||
|
|
printf 'Thresholds: warning=%s days critical=%s days\n\n' "$warning_days" "$critical_days"
|
||
|
|
|
||
|
|
printf 'Recommended next steps:\n'
|
||
|
|
printf -- '- Renew certificate before the operational threshold is breached\n'
|
||
|
|
printf -- '- Check the full chain and intermediate certificates\n'
|
||
|
|
printf -- '- Check the load balancer, ingress, or reverse proxy serving this certificate\n'
|
||
|
|
printf -- '- Verify monitoring threshold and alert ownership\n'
|
||
|
|
printf -- '- Attach this output to incident or change ticket\n'
|
||
|
|
|
||
|
|
exit "$exit_code"
|