#!/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 \ | 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"