#!/bin/sh # tls.sh - TLS and certificate report card for a hostname, in your terminal. # # curl -fsSL https://sitereliability.sh/tls.sh | sh -s -- example.com # sh tls.sh example.com [port] # # Source: https://sitereliability.sh/tls.sh (read it before you pipe it) # Checksums: https://sitereliability.sh/checksums.txt # # Read-only: performs TLS handshakes and one HTTPS HEAD request against the # target. Nothing is written, nothing is sent anywhere else. set -u HOST="${1:-}" PORT="${2:-443}" if [ -z "$HOST" ]; then echo "usage: curl -fsSL https://sitereliability.sh/tls.sh | sh -s -- example.com [port]" >&2 exit 2 fi command -v openssl >/dev/null 2>&1 || { echo "error: openssl is required" >&2; exit 1; } if [ -t 1 ]; then C_OK="$(printf '\033[32m')"; C_WARN="$(printf '\033[33m')" C_BAD="$(printf '\033[31m')"; C_DIM="$(printf '\033[2m')"; C_OFF="$(printf '\033[0m')" else C_OK=""; C_WARN=""; C_BAD=""; C_DIM=""; C_OFF="" fi PASS=0; WARN=0; FAIL=0 ok() { PASS=$((PASS+1)); printf ' %spass%s %s\n' "$C_OK" "$C_OFF" "$1"; } warn() { WARN=$((WARN+1)); printf ' %swarn%s %s\n' "$C_WARN" "$C_OFF" "$1"; } bad() { FAIL=$((FAIL+1)); printf ' %sfail%s %s\n' "$C_BAD" "$C_OFF" "$1"; } printf '\nsitereliability.sh/tls.sh — report card for %s:%s\n\n' "$HOST" "$PORT" # --- handshake and certificate ------------------------------------------- S_CLIENT_OUT="$(printf '' | openssl s_client -servername "$HOST" \ -connect "$HOST:$PORT" 2>/dev/null)" if [ -z "$S_CLIENT_OUT" ] || ! printf '%s' "$S_CLIENT_OUT" | grep -q 'BEGIN CERTIFICATE'; then bad "no TLS handshake — is $HOST:$PORT reachable and serving TLS?" printf '\n%s0 pass, 0 warn, 1 fail%s\n' "$C_BAD" "$C_OFF" exit 1 fi ok "TLS handshake completed" CERT="$(printf '%s' "$S_CLIENT_OUT" | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p')" VERIFY_CODE="$(printf '%s' "$S_CLIENT_OUT" | sed -n 's/^ *Verify return code: \([0-9]*\).*/\1/p' | head -1)" if [ "$VERIFY_CODE" = "0" ]; then ok "certificate chain verifies against system trust store" else DETAIL="$(printf '%s' "$S_CLIENT_OUT" | sed -n 's/^ *Verify return code: //p' | head -1)" bad "chain does not verify (${DETAIL:-unknown})" fi PROTO="$(printf '%s' "$S_CLIENT_OUT" | sed -n 's/^ *Protocol *: *//p' | head -1)" case "$PROTO" in TLSv1.3) ok "negotiated $PROTO" ;; TLSv1.2) ok "negotiated $PROTO" ;; "") warn "could not determine negotiated protocol" ;; *) bad "negotiated $PROTO — below the TLS 1.2 floor" ;; esac # Legacy protocol acceptance. Failure to even attempt (old flag unsupported by # the local openssl) is indistinguishable from refusal, which is the safe read. for legacy in tls1 tls1_1; do if printf '' | openssl s_client -servername "$HOST" -connect "$HOST:$PORT" \ "-$legacy" 2>/dev/null | grep -q 'BEGIN CERTIFICATE'; then bad "server still accepts legacy protocol ($legacy)" fi done # --- expiry --------------------------------------------------------------- END_DATE="$(printf '%s' "$CERT" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)" if printf '%s' "$CERT" | openssl x509 -noout -checkend 0 >/dev/null 2>&1; then if ! printf '%s' "$CERT" | openssl x509 -noout -checkend 1209600 >/dev/null 2>&1; then bad "certificate expires within 14 days ($END_DATE)" elif ! printf '%s' "$CERT" | openssl x509 -noout -checkend 2592000 >/dev/null 2>&1; then warn "certificate expires within 30 days ($END_DATE)" else ok "certificate valid until $END_DATE" fi else bad "certificate has EXPIRED ($END_DATE)" fi ISSUER="$(printf '%s' "$CERT" | openssl x509 -noout -issuer 2>/dev/null | sed 's/^issuer=//')" printf ' %sinfo issuer: %s%s\n' "$C_DIM" "$ISSUER" "$C_OFF" if printf '%s' "$CERT" | openssl x509 -noout -ext subjectAltName 2>/dev/null | grep -q "$HOST"; then ok "hostname appears in subjectAltName" else warn "hostname not visibly in subjectAltName (wildcard match not checked here)" fi # --- https response headers ---------------------------------------------- if command -v curl >/dev/null 2>&1; then HDRS="$(curl -sSI --max-time 10 "https://$HOST:$PORT/" 2>/dev/null | tr -d '\r')" if [ -n "$HDRS" ]; then hdr() { printf '%s\n' "$HDRS" | grep -i "^$1:" | head -1; } [ -n "$(hdr strict-transport-security)" ] \ && ok "HSTS header present" \ || warn "no Strict-Transport-Security header" [ -n "$(hdr x-content-type-options)" ] \ && ok "X-Content-Type-Options present" \ || warn "no X-Content-Type-Options: nosniff" if [ -n "$(hdr content-security-policy)" ] || [ -n "$(hdr x-frame-options)" ]; then ok "clickjacking protection present (CSP or X-Frame-Options)" else warn "no Content-Security-Policy or X-Frame-Options" fi SERVER_HDR="$(hdr server | cut -d' ' -f2-)" case "$SERVER_HDR" in *[0-9].[0-9]*) warn "Server header discloses a version: $SERVER_HDR" ;; esac else warn "could not fetch HTTPS response headers" fi else warn "curl not found — skipped response-header checks" fi # --- summary -------------------------------------------------------------- printf '\n%d pass, %d warn, %d fail\n' "$PASS" "$WARN" "$FAIL" if [ "$FAIL" -gt 0 ]; then VERDICT="needs attention"; COLOR="$C_BAD" elif [ "$WARN" -gt 0 ]; then VERDICT="decent, could be tighter"; COLOR="$C_WARN" else VERDICT="looking sharp"; COLOR="$C_OK"; fi printf '%sverdict: %s%s\n\n' "$COLOR" "$VERDICT" "$C_OFF" printf '%sTLS certificate lifetimes drop to 100 days in March 2027 and 47 days\nby 2029 — every renewal is a chance to silently fail.\nFull scans and expiry watching: https://sitereliability.sh%s\n\n' "$C_DIM" "$C_OFF" [ "$FAIL" -eq 0 ]