#!/bin/bash # ----------------------------------------------------------------------------------------------- # --------------------------------- Certificate Monitor ---------------------------------------- # ----------------------------------------------------------------------------------------------- # Monitors SSL certificate expiry for all configured domains by connecting directly # via openssl — no dependency on NPM or any other service. Reads the actual certificate # the server is presenting to the outside world. # # This approach catches real-world cert issues that API-based checks miss: # - Cert renewed but server not reloaded # - Wrong cert being served # - Cert chain issues # # Each domain and subdomain is a separate entry — they have independent certs. # Silent when all certs are healthy. Notifies when any approach warning threshold. # Notifications batched per severity — one message for warnings, one for criticals. # # All configuration in Master.conf under Certificate Monitor section. # Supports --dry-run to check certs and show results without sending notifications. # ----------------------------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../Master.conf" source "$SCRIPT_DIR/../common.sh" parse_args "$@" # ----------------------------------------------------------------------------------------------- # ━━━ $ICON_GEAR Setup ━━━ # ----------------------------------------------------------------------------------------------- echo "" echo "━━━ $ICON_GEAR Setup ━━━" if ! command -v openssl >/dev/null 2>&1; then error "openssl not found — required for certificate checks" exit 1 fi success "openssl available" if [[ ${#CERT_MONITOR_DOMAINS[@]} -eq 0 ]]; then warn "CERT_MONITOR_DOMAINS is empty in Master.conf — add your domains to enable monitoring" exit 0 fi info "$ICON_CERT Domains to check: ${#CERT_MONITOR_DOMAINS[@]}" # ----------------------------------------------------------------------------------------------- # ━━━ $ICON_SUMMARY Status ━━━ # ----------------------------------------------------------------------------------------------- if [[ "$SHOW_STATUS" == true ]]; then echo "" echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━" echo "$ICON_CERT Domains: ${CERT_MONITOR_DOMAINS[*]}" echo "$ICON_WARN Warn at: ${CERT_WARN_DAYS} days remaining" echo "$ICON_ERROR Crit at: ${CERT_CRIT_DAYS} days remaining" echo "$ICON_TIME Timeout: ${CERT_TIMEOUT}s per domain" echo "$ICON_GEAR Dry Run: $DRY_RUN" echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi [[ "$DRY_RUN" == true ]] && warn "DRY RUN — results shown but no notifications sent" # ----------------------------------------------------------------------------------------------- # CERT CHECK FUNCTION # Connects to domain:443 via openssl, extracts expiry date, calculates days remaining. # Returns 0=healthy 1=warning 2=critical 3=failed # ----------------------------------------------------------------------------------------------- check_cert() { local domain="$1" local port="${2:-443}" local expiry_str expiry_str=$(echo | timeout "$CERT_TIMEOUT" openssl s_client \ -connect "${domain}:${port}" \ -servername "$domain" \ 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) if [[ -z "$expiry_str" ]]; then error "$ICON_CERT $domain — could not retrieve certificate" return 3 fi local expiry_epoch expiry_epoch=$(date -d "$expiry_str" +%s 2>/dev/null) if [[ -z "$expiry_epoch" ]]; then error "$ICON_CERT $domain — could not parse expiry date: $expiry_str" return 3 fi local now days_remaining expiry_display now=$(date +%s) days_remaining=$(( (expiry_epoch - now) / 86400 )) expiry_display=$(date -d "$expiry_str" '+%Y-%m-%d' 2>/dev/null) if [[ "$days_remaining" -le "$CERT_CRIT_DAYS" ]]; then error "$ICON_CERT $domain — CRITICAL: ${days_remaining} days remaining (expires $expiry_display)" return 2 elif [[ "$days_remaining" -le "$CERT_WARN_DAYS" ]]; then warn "$ICON_CERT $domain — WARNING: ${days_remaining} days remaining (expires $expiry_display)" return 1 else success "$ICON_CERT $domain — OK: ${days_remaining} days remaining (expires $expiry_display)" return 0 fi } # ----------------------------------------------------------------------------------------------- # ━━━ $ICON_CERT Certificate Monitor ━━━ # ----------------------------------------------------------------------------------------------- echo "" echo "━━━ $ICON_CERT Certificate Monitor — $(date '+%Y-%m-%d %H:%M:%S') ━━━" echo "$ICON_WARN Warn threshold: ${CERT_WARN_DAYS} days" echo "$ICON_ERROR Crit threshold: ${CERT_CRIT_DAYS} days" echo "" START=$(date +%s) HEALTHY=() WARNING=() CRITICAL=() FAILED=() declare -A DOMAIN_STATUS for domain in "${CERT_MONITOR_DOMAINS[@]}"; do [[ -z "$domain" ]] && continue echo "━━━ $ICON_CERT $domain ━━━" check_cert "$domain" result=$? case $result in 0) HEALTHY+=("$domain"); DOMAIN_STATUS["$domain"]="OK" ;; 1) WARNING+=("$domain"); DOMAIN_STATUS["$domain"]="WARN" ;; 2) CRITICAL+=("$domain"); DOMAIN_STATUS["$domain"]="CRIT" ;; 3) FAILED+=("$domain"); DOMAIN_STATUS["$domain"]="FAIL" ;; esac echo "" done END=$(date +%s) if [[ "$DRY_RUN" == false ]]; then [[ ${#CRITICAL[@]} -gt 0 ]] && \ notify "Certificate CRITICAL on $(hostname) — expiring within ${CERT_CRIT_DAYS} days: ${CRITICAL[*]}" "Certificate Monitor" "warning" [[ ${#WARNING[@]} -gt 0 ]] && \ notify "Certificate WARNING on $(hostname) — expiring within ${CERT_WARN_DAYS} days: ${WARNING[*]}" "Certificate Monitor" "warning" [[ ${#FAILED[@]} -gt 0 ]] && \ notify "Certificate check FAILED on $(hostname) — could not reach: ${FAILED[*]}" "Certificate Monitor" "warning" fi # ----------------------------------------------------------------------------------------------- # ━━━ $ICON_SUMMARY Summary ━━━ # ----------------------------------------------------------------------------------------------- echo "━━━━━ $ICON_SUMMARY CERTIFICATE MONITOR SUMMARY ━━━━━" echo "$ICON_TIME Duration: $(format_duration $((END - START)))" echo "" echo " $ICON_SUCCESS Healthy: ${#HEALTHY[@]} $ICON_WARN Warning: ${#WARNING[@]} $ICON_ERROR Critical: ${#CRITICAL[@]} Failed: ${#FAILED[@]}" echo "" for domain in "${CERT_MONITOR_DOMAINS[@]}"; do [[ -z "$domain" ]] && continue case "${DOMAIN_STATUS[$domain]:-UNKN}" in OK) echo " $ICON_SUCCESS $domain" ;; WARN) echo " $ICON_WARN $domain" ;; CRIT) echo " $ICON_ERROR $domain" ;; FAIL) echo " $ICON_ERROR $domain (unreachable)" ;; esac done echo "" if [[ "$DRY_RUN" == true ]]; then echo "$ICON_WARN Status: DRY RUN — no notifications sent" elif [[ ${#CRITICAL[@]} -gt 0 || ${#FAILED[@]} -gt 0 ]]; then echo "$ICON_ERROR Status: ACTION REQUIRED" elif [[ ${#WARNING[@]} -gt 0 ]]; then echo "$ICON_WARN Status: WARNINGS — renewal recommended" else echo "$ICON_DONE Status: $ICON_SUCCESS ALL CERTS HEALTHY" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" [[ ${#CRITICAL[@]} -gt 0 || ${#FAILED[@]} -gt 0 ]] && exit 1 exit 0