Automatically check SSL certificate expiration dates and alert on certificates expiring soon.

Overview

This script monitors SSL certificates for multiple domains, checking expiration dates, validity, issuer information, and Subject Alternative Names (SANs). It can alert when certificates are expiring within a specified threshold.

Use Case: Certificate lifecycle management, preventing expired certificates, security audits, monitoring multi-domain certificates, compliance reporting.

Platform: Linux/macOS (Bash with openssl) Requirements: openssl command-line tool Execution Time: ~1-2 seconds per domain

The Script

Lang: bash
  1#!/bin/bash
  2
  3#
  4# SSL Certificate Checker Script
  5#
  6# Description:
  7#   Checks SSL certificate expiration dates for multiple domains.
  8#   Supports reading domains from a file or command line arguments.
  9#   Alerts on certificates expiring within specified days.
 10#
 11# Usage:
 12#   ./ssl-cert-checker.sh [OPTIONS] [domain1 domain2 ...]
 13#   ./ssl-cert-checker.sh -f domains.txt
 14#   ./ssl-cert-checker.sh -w 30 -c 7 example.com google.com
 15#
 16# Options:
 17#   -f FILE      Read domains from file (one per line)
 18#   -w DAYS      Warning threshold in days (default: 30)
 19#   -c DAYS      Critical threshold in days (default: 7)
 20#   -p PORT      Port to check (default: 443)
 21#   -v           Verbose mode (show certificate details)
 22#   -h           Show help
 23#
 24# Author: glyph.sh
 25# Reference: https://glyph.sh/scripts/ssl-cert-checker/
 26#
 27
 28# Default values
 29WARNING_DAYS=30
 30CRITICAL_DAYS=7
 31PORT=443
 32VERBOSE=0
 33DOMAINS_FILE=""
 34DOMAINS=()
 35
 36# Color codes
 37RED='\033[0;31m'
 38GREEN='\033[0;32m'
 39YELLOW='\033[1;33m'
 40CYAN='\033[0;36m'
 41BLUE='\033[0;34m'
 42NC='\033[0m' # No Color
 43
 44#
 45# Display help
 46#
 47show_help() {
 48    cat << EOF
 49SSL Certificate Checker
 50
 51Usage: $0 [OPTIONS] [domain1 domain2 ...]
 52
 53Options:
 54  -f FILE      Read domains from file (one per line)
 55  -w DAYS      Warning threshold in days (default: 30)
 56  -c DAYS      Critical threshold in days (default: 7)
 57  -p PORT      Port to check (default: 443)
 58  -v           Verbose mode (show certificate details)
 59  -h           Show help
 60
 61Examples:
 62  $0 example.com google.com
 63  $0 -f domains.txt
 64  $0 -w 45 -c 14 example.com
 65  $0 -v -f domains.txt
 66
 67Domain file format (one domain per line):
 68  example.com
 69  google.com
 70  github.com
 71
 72EOF
 73    exit 0
 74}
 75
 76#
 77# Parse command line arguments
 78#
 79while getopts "f:w:c:p:vh" opt; do
 80    case $opt in
 81        f)
 82            DOMAINS_FILE="$OPTARG"
 83            ;;
 84        w)
 85            WARNING_DAYS="$OPTARG"
 86            ;;
 87        c)
 88            CRITICAL_DAYS="$OPTARG"
 89            ;;
 90        p)
 91            PORT="$OPTARG"
 92            ;;
 93        v)
 94            VERBOSE=1
 95            ;;
 96        h)
 97            show_help
 98            ;;
 99        \?)
100            echo "Invalid option: -$OPTARG" >&2
101            exit 1
102            ;;
103    esac
104done
105
106shift $((OPTIND-1))
107
108# Check if openssl is available
109if ! command -v openssl &> /dev/null; then
110    echo -e "${RED}Error: openssl is not installed${NC}"
111    exit 1
112fi
113
114# Get domains from file if specified
115if [ -n "$DOMAINS_FILE" ]; then
116    if [ ! -f "$DOMAINS_FILE" ]; then
117        echo -e "${RED}Error: Domain file '$DOMAINS_FILE' not found${NC}"
118        exit 1
119    fi
120
121    while IFS= read -r domain || [ -n "$domain" ]; do
122        # Skip empty lines and comments
123        [[ -z "$domain" || "$domain" =~ ^[[:space:]]*# ]] && continue
124        # Trim whitespace
125        domain=$(echo "$domain" | xargs)
126        DOMAINS+=("$domain")
127    done < "$DOMAINS_FILE"
128fi
129
130# Add domains from command line arguments
131DOMAINS+=("$@")
132
133# Check if we have any domains
134if [ ${#DOMAINS[@]} -eq 0 ]; then
135    echo -e "${RED}Error: No domains specified${NC}"
136    echo "Use -h for help"
137    exit 1
138fi
139
140#
141# Get certificate information for a domain
142#
143check_certificate() {
144    local domain="$1"
145    local port="$2"
146
147    echo -e "${CYAN}Checking: ${domain}:${port}${NC}"
148
149    # Get certificate information
150    local cert_info
151    cert_info=$(echo | openssl s_client -servername "$domain" -connect "${domain}:${port}" 2>/dev/null | openssl x509 -noout -dates -issuer -subject -ext subjectAltName 2>/dev/null)
152
153    if [ -z "$cert_info" ]; then
154        echo -e "  ${RED}✗ Failed to retrieve certificate${NC}"
155        echo ""
156        return 1
157    fi
158
159    # Extract dates
160    local not_before
161    local not_after
162    not_before=$(echo "$cert_info" | grep "notBefore" | cut -d= -f2)
163    not_after=$(echo "$cert_info" | grep "notAfter" | cut -d= -f2)
164
165    # Convert expiration date to epoch
166    local exp_epoch
167    if [[ "$OSTYPE" == "darwin"* ]]; then
168        # macOS
169        exp_epoch=$(date -j -f "%b %d %T %Y %Z" "$not_after" +%s 2>/dev/null)
170    else
171        # Linux
172        exp_epoch=$(date -d "$not_after" +%s 2>/dev/null)
173    fi
174
175    if [ -z "$exp_epoch" ]; then
176        echo -e "  ${RED}✗ Failed to parse expiration date${NC}"
177        echo ""
178        return 1
179    fi
180
181    # Calculate days until expiration
182    local current_epoch
183    current_epoch=$(date +%s)
184    local seconds_diff=$((exp_epoch - current_epoch))
185    local days_remaining=$((seconds_diff / 86400))
186
187    # Extract issuer and subject
188    local issuer
189    local subject
190    issuer=$(echo "$cert_info" | grep "issuer=" | sed 's/issuer=//')
191    subject=$(echo "$cert_info" | grep "subject=" | sed 's/subject=//')
192
193    # Extract SANs
194    local sans
195    sans=$(echo "$cert_info" | grep -A1 "Subject Alternative Name" | tail -n1 | sed 's/^[[:space:]]*//')
196
197    # Determine status
198    local status_color="$GREEN"
199    local status_icon="✓"
200    local status_text="Valid"
201
202    if [ $days_remaining -lt 0 ]; then
203        status_color="$RED"
204        status_icon="✗"
205        status_text="EXPIRED"
206    elif [ $days_remaining -le $CRITICAL_DAYS ]; then
207        status_color="$RED"
208        status_icon="⚠"
209        status_text="CRITICAL"
210    elif [ $days_remaining -le $WARNING_DAYS ]; then
211        status_color="$YELLOW"
212        status_icon="⚠"
213        status_text="WARNING"
214    fi
215
216    # Display results
217    echo -e "  ${status_color}${status_icon} Status: ${status_text}${NC}"
218    echo -e "  Expires: ${not_after} (${days_remaining} days)"
219    echo -e "  Valid from: ${not_before}"
220
221    # Verbose output
222    if [ $VERBOSE -eq 1 ]; then
223        echo -e "  ${BLUE}Issuer:${NC}"
224        echo "    $issuer"
225        echo -e "  ${BLUE}Subject:${NC}"
226        echo "    $subject"
227
228        if [ -n "$sans" ]; then
229            echo -e "  ${BLUE}Subject Alternative Names:${NC}"
230            echo "    $sans"
231        fi
232    fi
233
234    echo ""
235
236    # Return status code
237    if [ $days_remaining -lt 0 ]; then
238        return 2  # Expired
239    elif [ $days_remaining -le $CRITICAL_DAYS ]; then
240        return 3  # Critical
241    elif [ $days_remaining -le $WARNING_DAYS ]; then
242        return 4  # Warning
243    fi
244
245    return 0  # OK
246}
247
248#
249# Main execution
250#
251echo -e "${CYAN}========================================${NC}"
252echo -e "${CYAN} SSL Certificate Checker${NC}"
253echo -e "${CYAN}========================================${NC}"
254echo -e "Warning threshold: ${WARNING_DAYS} days"
255echo -e "Critical threshold: ${CRITICAL_DAYS} days"
256echo -e "Total domains: ${#DOMAINS[@]}"
257echo ""
258
259# Statistics
260TOTAL=0
261OK=0
262WARNING=0
263CRITICAL=0
264EXPIRED=0
265FAILED=0
266
267# Check each domain
268for domain in "${DOMAINS[@]}"; do
269    ((TOTAL++))
270    check_certificate "$domain" "$PORT"
271    status=$?
272
273    case $status in
274        0) ((OK++)) ;;
275        2) ((EXPIRED++)) ;;
276        3) ((CRITICAL++)) ;;
277        4) ((WARNING++)) ;;
278        *) ((FAILED++)) ;;
279    esac
280done
281
282#
283# Summary
284#
285echo -e "${CYAN}========================================${NC}"
286echo -e "${CYAN} Summary${NC}"
287echo -e "${CYAN}========================================${NC}"
288echo -e "Total domains checked: ${TOTAL}"
289echo -e "${GREEN}✓ Valid: ${OK}${NC}"
290
291if [ $WARNING -gt 0 ]; then
292    echo -e "${YELLOW}⚠ Warning: ${WARNING}${NC}"
293fi
294
295if [ $CRITICAL -gt 0 ]; then
296    echo -e "${RED}⚠ Critical: ${CRITICAL}${NC}"
297fi
298
299if [ $EXPIRED -gt 0 ]; then
300    echo -e "${RED}✗ Expired: ${EXPIRED}${NC}"
301fi
302
303if [ $FAILED -gt 0 ]; then
304    echo -e "${RED}✗ Failed: ${FAILED}${NC}"
305fi
306
307echo ""
308
309# Exit with appropriate code
310if [ $EXPIRED -gt 0 ] || [ $FAILED -gt 0 ]; then
311    exit 2
312elif [ $CRITICAL -gt 0 ]; then
313    exit 3
314elif [ $WARNING -gt 0 ]; then
315    exit 4
316fi
317
318exit 0

Usage

Basic Usage

Check single domain:

Lang: bash
1./ssl-cert-checker.sh example.com

Check multiple domains:

Lang: bash
1./ssl-cert-checker.sh example.com google.com github.com

Using a Domain File

Create a file with domains (one per line):

Lang: bash
1cat > domains.txt << EOF
2example.com
3google.com
4github.com
5cloudflare.com
6EOF
7
8./ssl-cert-checker.sh -f domains.txt

Custom Thresholds

Set custom warning and critical thresholds:

Lang: bash
1# Warn at 45 days, critical at 14 days
2./ssl-cert-checker.sh -w 45 -c 14 example.com

Verbose Mode

Show detailed certificate information:

Lang: bash
1./ssl-cert-checker.sh -v example.com

Custom Port

Check certificate on non-standard port:

Lang: bash
1./ssl-cert-checker.sh -p 8443 example.com

Sample Output

Normal Output

Lang:
========================================
 SSL Certificate Checker
========================================
Warning threshold: 30 days
Critical threshold: 7 days
Total domains: 3

Checking: example.com:443
  ✓ Status: Valid
  Expires: Dec 15 23:59:59 2025 GMT (43 days)
  Valid from: Sep 15 00:00:00 2025 GMT

Checking: expiring-soon.com:443
  ⚠ Status: WARNING
  Expires: Nov 20 23:59:59 2025 GMT (18 days)
  Valid from: Nov 20 00:00:00 2024 GMT

Checking: critical-cert.com:443
  ⚠ Status: CRITICAL
  Expires: Nov 8 23:59:59 2025 GMT (6 days)
  Valid from: Nov 8 00:00:00 2024 GMT

========================================
 Summary
========================================
Total domains checked: 3
✓ Valid: 1
⚠ Warning: 1
⚠ Critical: 1

Verbose Output

Lang:
Checking: example.com:443
  ✓ Status: Valid
  Expires: Dec 15 23:59:59 2025 GMT (43 days)
  Valid from: Sep 15 00:00:00 2025 GMT
  Issuer:
    C=US, O=Let's Encrypt, CN=R3
  Subject:
    CN=example.com
  Subject Alternative Names:
    DNS:example.com, DNS:www.example.com

Domain File Format

Create a text file with one domain per line:

Lang: txt
 1# Production domains
 2example.com
 3www.example.com
 4api.example.com
 5
 6# Staging domains
 7staging.example.com
 8
 9# Partner domains
10partner1.com
11partner2.com

Comments (lines starting with #) and empty lines are ignored.

Scheduled Monitoring

Cron Job (Daily Check)

Lang: bash
1# Run daily at 9 AM, email results if issues found
20 9 * * * /path/to/ssl-cert-checker.sh -f /path/to/domains.txt || mail -s "SSL Certificate Alert" admin@example.com < /path/to/output.log

Weekly Report

Lang: bash
1# Generate weekly report every Monday at 8 AM
20 8 * * 1 /path/to/ssl-cert-checker.sh -v -f /path/to/domains.txt > /var/log/ssl-weekly-report.log 2>&1

Email Notifications

Lang: bash
1#!/bin/bash
2OUTPUT=$(/path/to/ssl-cert-checker.sh -f /path/to/domains.txt)
3STATUS=$?
4
5if [ $STATUS -ne 0 ]; then
6    echo "$OUTPUT" | mail -s "SSL Certificate Alert - Action Required" admin@example.com
7fi

Exit Codes

The script uses exit codes to indicate status:

  • 0 - All certificates valid
  • 2 - One or more expired certificates or connection failures
  • 3 - One or more certificates in critical threshold
  • 4 - One or more certificates in warning threshold

This makes it easy to integrate with monitoring systems.

Integration Examples

Nagios/Icinga

Lang: bash
1#!/bin/bash
2# Nagios plugin wrapper
3OUTPUT=$(/path/to/ssl-cert-checker.sh -f /etc/nagios/domains.txt)
4STATUS=$?
5
6echo "$OUTPUT"
7exit $STATUS

Zabbix

Lang: bash
1#!/bin/bash
2# Return 1 for OK, 0 for problems
3/path/to/ssl-cert-checker.sh -f /etc/zabbix/domains.txt > /dev/null 2>&1
4if [ $? -eq 0 ]; then
5    echo 1
6else
7    echo 0
8fi

Slack Notification

Lang: bash
1#!/bin/bash
2OUTPUT=$(/path/to/ssl-cert-checker.sh -f /path/to/domains.txt)
3STATUS=$?
4
5if [ $STATUS -ne 0 ]; then
6    curl -X POST -H 'Content-type: application/json' \
7        --data "{\"text\":\"SSL Certificate Alert:\n\`\`\`$OUTPUT\`\`\`\"}" \
8        YOUR_SLACK_WEBHOOK_URL
9fi

Advanced Usage

Check Multiple Ports

Lang: bash
1#!/bin/bash
2DOMAIN="example.com"
3PORTS=(443 8443 9443)
4
5for port in "${PORTS[@]}"; do
6    ./ssl-cert-checker.sh -p $port $DOMAIN
7done

Export to CSV

Lang: bash
1#!/bin/bash
2# Modified script output to CSV format
3echo "Domain,Status,Days Remaining,Expiration Date" > report.csv
4./ssl-cert-checker.sh -f domains.txt | grep -E "(Checking|Status|Expires)" | \
5    awk '/Checking/{domain=$2} /Status/{status=$3} /Expires/{print domain","status","$NF}' >> report.csv

JSON Output (Modified Script)

Add this function for JSON output:

Lang: bash
 1# Add to script for JSON output
 2if [ "$JSON_OUTPUT" = "1" ]; then
 3    echo "{"
 4    echo "  \"domain\": \"$domain\","
 5    echo "  \"status\": \"$status_text\","
 6    echo "  \"days_remaining\": $days_remaining,"
 7    echo "  \"expires\": \"$not_after\","
 8    echo "  \"issuer\": \"$issuer\""
 9    echo "}"
10fi

Troubleshooting

“openssl: command not found”

Install OpenSSL:

Lang: bash
1# Ubuntu/Debian
2sudo apt-get install openssl
3
4# RHEL/CentOS/Rocky
5sudo yum install openssl
6
7# macOS
8brew install openssl

Connection Timeout

Some servers may have strict firewall rules. Try:

Lang: bash
1# Increase timeout (modify script)
2timeout 10 openssl s_client -servername "$domain" -connect "${domain}:${port}"

SNI Issues

If you get certificate mismatches, ensure SNI is enabled (it is by default with -servername).

macOS Date Parsing Issues

The script handles both macOS and Linux date formats. If you still have issues:

Lang: bash
1# Check date command
2date --version  # GNU date (Linux)
3date -j         # BSD date (macOS)

Certificate Details Explained

  • Not Before: Certificate validity start date
  • Not After: Certificate expiration date
  • Issuer: Certificate Authority that issued the certificate
  • Subject: Domain(s) the certificate is issued to
  • SANs: Subject Alternative Names (additional domains covered)

See Also

Download

Lang: bash
1curl -O https://glyph.sh/scripts/ssl-cert-checker.sh
2chmod +x ssl-cert-checker.sh
3./ssl-cert-checker.sh -h