Skip to content

Instantly share code, notes, and snippets.

@mihaikelemen
Created October 7, 2025 05:22
Show Gist options
  • Select an option

  • Save mihaikelemen/b851a88ea69833bd4536e6a307ea606e to your computer and use it in GitHub Desktop.

Select an option

Save mihaikelemen/b851a88ea69833bd4536e6a307ea606e to your computer and use it in GitHub Desktop.
SSL Certificate Monitor
#!/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