Checking the SSL certificate of many sites

Say you have a (long) list of sites that you have to check the SSL certificate of (SANs, expiration date, etc). This script does it all.

HOSTFILE="${1:?usage: $0 <hostfile> [warn_days]}"
WARN_DAYS="${2:-30}"
# Max seconds to wait per host, so one dead/firewalled host can't hang the loop.
TIMEOUT=10

while read -r entry; do
    # Skip empty lines and comments so the host file can be annotated.
    [[ -z "$entry" || "$entry" =~ ^[[:space:]]*# ]] && continue

    # Split "host:port"; if there is no colon, ${entry##*:} returns the whole
    # string, which is how we detect "no port given" and fall back to 443.
    host="${entry%%:*}"
    port="${entry##*:}"
    [[ "$port" == "$entry" ]] && port=443

    echo "=== ${host}:${port} ==="

    # Connect and capture the full handshake output once.
    #  -servername: sends SNI. Without it, servers hosting several sites on one
    #               IP (nginx, haproxy, k3s ingress...) may hand you the default
    #               cert instead of the one for this hostname.
    #  </dev/null:  closes s_client's stdin so it disconnects immediately (this
    #               replaces your manual "q<enter>"). It ALSO stops s_client from
    #               reading stdin, which here is the host file - without it,
    #               s_client would swallow the remaining hosts and the loop
    #               would only process the first one.
    #  2>/dev/null: hides the handshake chatter that s_client writes to stderr.
    out=$(timeout "$TIMEOUT" openssl s_client -connect "${host}:${port}" \
            -servername "$host" </dev/null 2>/dev/null)

    # openssl x509 picks the first PEM block out of the output - the server's
    # own certificate. This replaces the scroll-back/copy/paste step.
    cert=$(openssl x509 <<<"$out" 2>/dev/null)
    if [[ -z "$cert" ]]; then
        echo "  ERROR: could not retrieve a certificate (unreachable, timeout, or no TLS)"
        echo
        continue
    fi

    # "notAfter=Dec 31 12:00:00 2026 GMT" -> keep only the date part.
    enddate=$(openssl x509 -noout -enddate <<<"$cert" | cut -d= -f2)

    # Convert expiry to epoch seconds (GNU date) to compute whole days left.
    days_left=$(( ( $(date -d "$enddate" +%s) - $(date +%s) ) / 86400 ))

    # -checkend N exits 0 if the cert is still valid N seconds from now.
    # Check "already expired" first, then "expires within the warning window".
    if ! openssl x509 -noout -checkend 0 <<<"$cert" >/dev/null; then
        status="EXPIRED"
    elif ! openssl x509 -noout -checkend $(( WARN_DAYS * 86400 )) <<<"$cert" >/dev/null; then
        status="WARNING (expires within ${WARN_DAYS} days)"
    else
        status="OK"
    fi

    # s_client already verified the chain against the system CA store during
    # the handshake; it prints the result as "Verify return code: 0 (ok)".
    # Anything other than 0 means self-signed, missing intermediate, etc.
    verify=$(grep -m1 'Verify return code' <<<"$out" | sed 's/.*Verify return code: //')

    # -checkhost tests whether the name we connected to is covered by the cert
    # (CN or any SAN, wildcards included). Catches "right server, wrong cert".
    if openssl x509 -noout -checkhost "$host" <<<"$cert" | grep -q 'does match'; then
        hostmatch="yes"
    else
        hostmatch="NO"
    fi

    echo "  Subject:    $(openssl x509 -noout -subject <<<"$cert" | sed 's/^subject=//')"
    echo "  Issuer:     $(openssl x509 -noout -issuer  <<<"$cert" | sed 's/^issuer=//')"
    echo "  Expires:    ${enddate} (${days_left} days)"
    echo "  Status:     ${status}"
    echo "  Chain:      ${verify:-unknown}"
    echo "  Host match: ${hostmatch}"
    echo "  SANs:"
    # -ext subjectAltName prints a header line, then one line with all SANs
    # comma-separated ("DNS:a, DNS:b, IP Address:1.2.3.4").
    # tail drops the header, sed puts each SAN on its own indented line.
    openssl x509 -noout -ext subjectAltName <<<"$cert" 2>/dev/null \
        | tail -n +2 \
        | sed -e 's/^[[:space:]]*//' -e 's/, /\n/g' \
        | sed 's/^/    /'
    echo
done < "$HOSTFILE"

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *