bash Network February 20, 2025

SSL Certificate Checker

A Bash script to check SSL/TLS certificate expiration dates, cipher suites, and common misconfigurations across multiple domains.

ssltlscertificatesmonitoringbash

Description

This Bash script performs comprehensive SSL/TLS certificate audits against one or more domains. It connects using openssl s_client, extracts certificate metadata, and evaluates expiration status, cipher strength, protocol support, and trust chain issues such as self-signed certificates.

Designed for sysadmins and security engineers who need a quick, dependency-light way to monitor certificate health across infrastructure without relying on third-party SaaS tools.

Features

  • Multi-domain scanning — pass a list of domains via arguments or a file.
  • Expiry countdown — displays the number of days until each certificate expires, with color-coded severity.
  • Cipher suite inspection — reports the negotiated cipher and flags weak or deprecated suites.
  • Protocol detection — tests for SSLv3, TLS 1.0, and TLS 1.1 support and warns if enabled.
  • Self-signed detection — identifies certificates where the issuer matches the subject.
  • Trust chain validation — catches incomplete chains and untrusted root CAs.
  • Colored terminal output — green, yellow, and red indicators for quick visual triage.
  • Exit codes — returns non-zero when critical issues are found, suitable for CI/CD pipelines.

Usage

Basic — check a single domain

./ssl-cert-checker.sh example.com

Multiple domains inline

./ssl-cert-checker.sh example.com github.com expired.badssl.com

Read domains from a file

./ssl-cert-checker.sh -f domains.txt

Where domains.txt contains one domain per line:

example.com
github.com
self-signed.badssl.com

Custom port

./ssl-cert-checker.sh -p 8443 internal.corp.local

Example output

══════════════════════════════════════════════════════
  SSL Certificate Checker — 3 domain(s)
══════════════════════════════════════════════════════

[✔] example.com:443
    Subject:    CN=example.com
    Issuer:     CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1
    Expires:    Mar 14 2026 (324 days remaining)
    Cipher:     TLS_AES_256_GCM_SHA384 (TLSv1.3)
    Status:     OK

[!] self-signed.badssl.com:443
    Subject:    CN=*.badssl.com
    Issuer:     CN=*.badssl.com
    Expires:    Jul 20 2025 (87 days remaining)
    Cipher:     ECDHE-RSA-AES128-GCM-SHA256 (TLSv1.2)
    Warnings:   Self-signed certificate detected
    Status:     WARNING

Source Code

#!/usr/bin/env bash
# ssl-cert-checker.sh — Check SSL/TLS certificates across multiple domains
# Author: FuryBee | License: MIT

set -euo pipefail

# ── Colors ──────────────────────────────────────────
RED='\033[0;31m'
YEL='\033[0;33m'
GRN='\033[0;32m'
CYN='\033[0;36m'
BLD='\033[1m'
RST='\033[0m'

PORT=443
DOMAINS=()
TIMEOUT=5
ISSUES_FOUND=0
WEAK_CIPHERS="RC4|DES|MD5|NULL|EXPORT|anon"
DEPRECATED_PROTOCOLS=("ssl3" "tls1" "tls1_1")

usage() {
  echo "Usage: $0 [-p port] [-f file] [domain ...]"
  echo "  -p PORT   Target port (default: 443)"
  echo "  -f FILE   Read domains from file (one per line)"
  echo "  -h        Show this help"
  exit 0
}

while getopts ":p:f:h" opt; do
  case $opt in
    p) PORT="$OPTARG" ;;
    f) while IFS= read -r line; do
         [[ -n "$line" && ! "$line" =~ ^# ]] && DOMAINS+=("$line")
       done < "$OPTARG" ;;
    h) usage ;;
    *) echo "Unknown option: -$OPTARG" >&2; exit 1 ;;
  esac
done
shift $((OPTIND - 1))
DOMAINS+=("$@")

if [[ ${#DOMAINS[@]} -eq 0 ]]; then
  echo -e "${RED}Error: No domains specified.${RST}" >&2
  usage
fi

command -v openssl >/dev/null 2>&1 || {
  echo -e "${RED}Error: openssl is required but not installed.${RST}" >&2
  exit 1
}

separator() {
  echo -e "${CYN}══════════════════════════════════════════════════════${RST}"
}

separator
echo -e "  ${BLD}SSL Certificate Checker${RST} — ${#DOMAINS[@]} domain(s)"
separator
echo ""

check_deprecated_protocols() {
  local domain="$1" warnings=""
  for proto in "${DEPRECATED_PROTOCOLS[@]}"; do
    local label="${proto/tls1_1/TLS 1.1}"
    label="${label/tls1/TLS 1.0}"
    label="${label/ssl3/SSLv3}"
    if echo | openssl s_client -connect "${domain}:${PORT}" \
        -"${proto}" 2>/dev/null | grep -q "BEGIN CERTIFICATE"; then
      warnings+="    ${YEL}⚠  Deprecated protocol supported: ${label}${RST}\n"
    fi
  done
  echo -e "$warnings"
}

check_domain() {
  local domain="$1"
  local status="OK"
  local warnings=""
  local icon="${GRN}✔${RST}"

  # Fetch certificate data
  local cert_data
  cert_data=$(echo | timeout "$TIMEOUT" openssl s_client \
    -servername "$domain" -connect "${domain}:${PORT}" 2>/dev/null)

  if [[ -z "$cert_data" ]] || ! echo "$cert_data" | grep -q "BEGIN CERTIFICATE"; then
    echo -e "[${RED}✘${RST}] ${BLD}${domain}:${PORT}${RST}"
    echo -e "    ${RED}Connection failed or no certificate returned.${RST}"
    echo ""
    ISSUES_FOUND=1
    return
  fi

  # Extract fields
  local subject issuer expiry_str cipher_line cipher protocol
  subject=$(echo "$cert_data" | openssl x509 -noout -subject 2>/dev/null | sed 's/subject=//')
  issuer=$(echo "$cert_data" | openssl x509 -noout -issuer 2>/dev/null | sed 's/issuer=//')
  expiry_str=$(echo "$cert_data" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
  cipher_line=$(echo "$cert_data" | grep "Cipher is" | head -1)
  cipher=$(echo "$cipher_line" | sed 's/.*Cipher is //')
  protocol=$(echo "$cert_data" | grep "Protocol  :" | head -1 | awk '{print $NF}')

  # Calculate days until expiry
  local expiry_epoch now_epoch days_left
  expiry_epoch=$(date -d "$expiry_str" +%s 2>/dev/null) || expiry_epoch=0
  now_epoch=$(date +%s)
  days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

  # Format expiry display
  local expiry_display
  local friendly_date
  friendly_date=$(date -d "$expiry_str" "+%b %d %Y" 2>/dev/null || echo "$expiry_str")

  if [[ $days_left -le 0 ]]; then
    expiry_display="${RED}${friendly_date} (EXPIRED ${days_left#-} days ago)${RST}"
    status="CRITICAL"
    icon="${RED}✘${RST}"
    ISSUES_FOUND=1
  elif [[ $days_left -le 30 ]]; then
    expiry_display="${RED}${friendly_date} (${days_left} days remaining)${RST}"
    status="CRITICAL"
    icon="${RED}✘${RST}"
    ISSUES_FOUND=1
  elif [[ $days_left -le 90 ]]; then
    expiry_display="${YEL}${friendly_date} (${days_left} days remaining)${RST}"
    [[ "$status" == "OK" ]] && status="WARNING" && icon="${YEL}!${RST}"
  else
    expiry_display="${GRN}${friendly_date} (${days_left} days remaining)${RST}"
  fi

  # Self-signed check
  local subject_cn issuer_cn
  subject_cn=$(echo "$subject" | grep -oP 'CN\s*=\s*\K[^,/]+' | xargs)
  issuer_cn=$(echo "$issuer" | grep -oP 'CN\s*=\s*\K[^,/]+' | xargs)

  if [[ "$subject_cn" == "$issuer_cn" ]]; then
    warnings+="    ${YEL}⚠  Self-signed certificate detected${RST}\n"
    [[ "$status" == "OK" ]] && status="WARNING" && icon="${YEL}!${RST}"
  fi

  # Verify trust chain
  local verify_result
  verify_result=$(echo "$cert_data" | grep "Verify return code:" | head -1)
  if ! echo "$verify_result" | grep -q "0 (ok)"; then
    local verify_msg
    verify_msg=$(echo "$verify_result" | sed 's/.*Verify return code: //')
    warnings+="    ${YEL}⚠  Chain issue: ${verify_msg}${RST}\n"
    [[ "$status" == "OK" ]] && status="WARNING" && icon="${YEL}!${RST}"
  fi

  # Weak cipher check
  if echo "$cipher" | grep -qEi "$WEAK_CIPHERS"; then
    warnings+="    ${RED}⚠  Weak cipher detected: ${cipher}${RST}\n"
    status="CRITICAL"
    icon="${RED}✘${RST}"
    ISSUES_FOUND=1
  fi

  # Deprecated protocol check
  local proto_warnings
  proto_warnings=$(check_deprecated_protocols "$domain")

  # Output
  echo -e "[${icon}] ${BLD}${domain}:${PORT}${RST}"
  echo -e "    Subject:    ${subject}"
  echo -e "    Issuer:     ${issuer}"
  echo -e "    Expires:    ${expiry_display}"
  echo -e "    Cipher:     ${cipher} (${protocol})"
  [[ -n "$warnings" ]] && echo -e "${warnings}" | sed '/^$/d'
  [[ -n "$proto_warnings" ]] && echo -e "${proto_warnings}" | sed '/^$/d'

  # Status line
  case "$status" in
    OK)       echo -e "    Status:     ${GRN}${status}${RST}" ;;
    WARNING)  echo -e "    Status:     ${YEL}${status}${RST}" ;;
    CRITICAL) echo -e "    Status:     ${RED}${status}${RST}" ;;
  esac
  echo ""
}

for domain in "${DOMAINS[@]}"; do
  check_domain "$domain"
done

separator
if [[ $ISSUES_FOUND -eq 0 ]]; then
  echo -e "  ${GRN}All certificates healthy.${RST}"
else
  echo -e "  ${RED}Issues detected — review warnings above.${RST}"
fi
separator

exit $ISSUES_FOUND

Notes

  • OpenSSL required — the script relies entirely on openssl s_client and openssl x509. Most Linux distributions ship with OpenSSL by default; on macOS, the Homebrew version (brew install openssl) is recommended for full protocol flag support.
  • Deprecated protocol probes — testing SSLv3, TLS 1.0, and TLS 1.1 requires that your local OpenSSL build still supports those protocols. Newer builds may have them compiled out, in which case the check silently skips.
  • Timeout — the default connection timeout is 5 seconds. For high-latency targets or .onion addresses behind a proxy, increase it by editing the TIMEOUT variable.
  • CI/CD integration — the script exits with code 1 when any critical issue is found, making it suitable for pipeline gates. Combine with cron or a systemd timer for scheduled monitoring.
  • SNI support — the -servername flag ensures Server Name Indication is sent, which is necessary for hosts serving multiple certificates on a single IP.
  • Limitations — this script does not perform OCSP/CRL revocation checks or evaluate HSTS headers. For full compliance audits, pair it with tools like testssl.sh or sslyze.