SSL Certificate Checker
A Bash script to check SSL/TLS certificate expiration dates, cipher suites, and common misconfigurations across multiple domains.
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_clientandopenssl 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
TIMEOUTvariable. - CI/CD integration — the script exits with code
1when any critical issue is found, making it suitable for pipeline gates. Combine withcronor a systemd timer for scheduled monitoring. - SNI support — the
-servernameflag 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.shorsslyze.