Created
October 7, 2025 05:22
-
-
Save mihaikelemen/b851a88ea69833bd4536e6a307ea606e to your computer and use it in GitHub Desktop.
SSL Certificate Monitor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # | |
| ################################################ | |
| # SSL Certificate Monitor | |
| ################################################ | |
| # | |
| # DESCRIPTION: | |
| # Monitors SSL certificate expiration for given domains and provides | |
| # status alerts. Supports custom ports and warning thresholds. | |
| # Can be integrated with cron jobs, monitoring systems, and alerting | |
| # platforms (email, Slack, webhooks, system logs). | |
| # | |
| # DEPENDENCIES: | |
| # - openssl, date, timeout | |
| # | |
| # AUTHOR: | |
| # Mihai Kelemen <mihai@webmanage.ro> | |
| # | |
| # LICENSE: | |
| # MIT License | |
| # | |
| # VERSION: | |
| # 1.0.0 | |
| # | |
| # USAGE: | |
| # ./ssl_monitor.sh [OPTIONS] | |
| # | |
| # OPTIONS: | |
| # -d DOMAIN Domain(s) to check (supports domain:port format) | |
| # -w DAYS Warning threshold in days (default: 5) | |
| # -t SECONDS Connection timeout in seconds (default: 10) | |
| # -v Enable verbose output | |
| # -h Show this help message | |
| # | |
| # EXAMPLES: | |
| # ./ssl_monitor.sh -d domain1.com -d domain2.com | |
| # ./ssl_monitor.sh -d example.com:8443 -w 7 -v | |
| # ./ssl_monitor.sh -d api.domain.com -t 15 | |
| # | |
| # EXIT CODES: | |
| # 0 - All certificates valid | |
| # 1 - One or more certificates expiring within threshold | |
| # 2 - One or more certificates expired | |
| # 3 - One or more certificates have unknown status | |
| # | |
| ################################################ | |
| set -uo pipefail | |
| DAYS_WARNING=5 | |
| TIMEOUT=10 | |
| VERBOSE=false | |
| declare -a DOMAINS=() | |
| if [[ -t 1 ]]; then | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' | |
| else | |
| RED=''; GREEN=''; YELLOW=''; BLUE=''; NC='' | |
| fi | |
| # Counters | |
| EXPIRED=0; WARNING=0; VALID=0; UNKNOWN=0 | |
| show_help() { | |
| # Extract and display header comments (skip shebang and first comment line) | |
| sed -n '4,48p' "$0" | \ | |
| sed 's/^# //g; s/^#$//g; s/^#//' | \ | |
| sed '/^################################################$/d' | |
| } | |
| log() { | |
| local level="$1"; shift | |
| case "$level" in | |
| ERROR) echo -e "${RED}ERROR${NC}: $*" >&2 ;; | |
| WARN) echo -e "${YELLOW}WARN${NC}: $*" >&2 ;; | |
| OK) echo -e "${GREEN}OK${NC}: $*" >&2 ;; | |
| UNKNOWN) echo -e "${BLUE}UNKNOWN${NC}: $*" >&2 ;; | |
| *) [[ "$VERBOSE" == "true" ]] && echo "INFO: $*" >&2 ;; | |
| esac | |
| } | |
| die() { log ERROR "$*"; exit 1; } | |
| check_deps() { | |
| for cmd in openssl date timeout; do | |
| command -v "$cmd" >/dev/null || die "Missing: $cmd" | |
| done | |
| } | |
| validate() { | |
| [[ "$DAYS_WARNING" =~ ^[0-9]+$ ]] && [[ "$DAYS_WARNING" -gt 0 ]] || die "Invalid days: $DAYS_WARNING" | |
| [[ "$TIMEOUT" =~ ^[0-9]+$ ]] && [[ "$TIMEOUT" -gt 0 ]] || die "Invalid timeout: $TIMEOUT" | |
| [[ ${#DOMAINS[@]} -gt 0 ]] || die "No domains specified" | |
| } | |
| get_cert_expiry() { | |
| local domain="$1" port="${2:-443}" | |
| log INFO "Checking ${domain}:${port}" | |
| local cert_info | |
| cert_info=$(echo "" | timeout "$TIMEOUT" openssl s_client \ | |
| -servername "$domain" -connect "$domain:$port" 2>/dev/null | \ | |
| openssl x509 -noout -dates 2>/dev/null) | |
| [[ -n "$cert_info" ]] || return 1 | |
| echo "$cert_info" | grep "notAfter" | cut -d'=' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | |
| } | |
| date_to_epoch() { | |
| date -d "$1" +%s 2>/dev/null || date -j -f "%b %d %H:%M:%S %Y %Z" "$1" +%s 2>/dev/null | |
| } | |
| parse_domain() { | |
| local input="$1" | |
| if [[ "$input" == *":"* ]]; then | |
| echo "${input%:*}" "${input#*:}" | |
| else | |
| echo "$input" "443" | |
| fi | |
| } | |
| check_cert() { | |
| local domain="$1" port="$2" endpoint="$domain:$port" | |
| local expiry_date | |
| expiry_date=$(get_cert_expiry "$domain" "$port") | |
| if [[ $? -ne 0 ]]; then | |
| log UNKNOWN "$endpoint: Unable to retrieve certificate (connection failed, invalid domain, or no SSL)" | |
| return 3 | |
| fi | |
| local expiry_epoch current_epoch days | |
| expiry_epoch=$(date_to_epoch "$expiry_date") | |
| if [[ $? -ne 0 ]]; then | |
| log UNKNOWN "$endpoint: Unable to parse certificate date format" | |
| return 3 | |
| fi | |
| current_epoch=$(date +%s) | |
| days=$(( (expiry_epoch - current_epoch) / 86400 )) | |
| if [[ $expiry_epoch -lt $current_epoch ]]; then | |
| log ERROR "$endpoint: EXPIRED $((-days)) day(s) ago" | |
| return 2 | |
| elif [[ $days -le $DAYS_WARNING ]]; then | |
| log WARN "$endpoint: Expires in $days day(s)" | |
| return 1 | |
| else | |
| log OK "$endpoint: Valid for $days day(s)" | |
| return 0 | |
| fi | |
| } | |
| parse_args() { | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -d) [[ -n "${2:-}" ]] || die "-d requires domain"; DOMAINS+=("$2"); shift 2 ;; | |
| -w) [[ -n "${2:-}" ]] || die "-w requires number"; DAYS_WARNING="$2"; shift 2 ;; | |
| -t) [[ -n "${2:-}" ]] || die "-t requires seconds"; TIMEOUT="$2"; shift 2 ;; | |
| -v) VERBOSE=true; shift ;; | |
| -h) show_help; exit 0 ;; | |
| -*) die "Unknown option: $1" ;; | |
| *) die "Use -d to specify domains" ;; | |
| esac | |
| done | |
| } | |
| main() { | |
| parse_args "$@" | |
| check_deps | |
| validate | |
| local exit_code=0 | |
| for domain_input in "${DOMAINS[@]}"; do | |
| read -r domain port <<< "$(parse_domain "$domain_input")" | |
| check_cert "$domain" "$port" | |
| case $? in | |
| 0) VALID=$((VALID + 1)) ;; | |
| 1) WARNING=$((WARNING + 1)); [[ $exit_code -eq 0 ]] && exit_code=1 ;; | |
| 2) EXPIRED=$((EXPIRED + 1)); exit_code=2 ;; | |
| 3) UNKNOWN=$((UNKNOWN + 1)); [[ $exit_code -eq 0 ]] && exit_code=3 ;; | |
| esac | |
| done | |
| echo "Summary: ${VALID} valid, ${WARNING} expiring, ${EXPIRED} expired, ${UNKNOWN} unknown" | |
| if [[ $EXPIRED -gt 0 ]]; then | |
| log ERROR "Found $EXPIRED expired certificate(s)" | |
| elif [[ $WARNING -gt 0 ]]; then | |
| log WARN "Found $WARNING certificate(s) expiring within $DAYS_WARNING days" | |
| elif [[ $UNKNOWN -gt 0 ]]; then | |
| log UNKNOWN "Found $UNKNOWN certificate(s) with unknown status" | |
| else | |
| log OK "All $VALID certificates are valid" | |
| fi | |
| exit $exit_code | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment