openssl s_client -connect host:443 -servername host — shows the full chain,
expiry, and whether verification succeeded.openssl s_client -connect example.com:443 -servername example.com — shows certificate
chain, expiry date, issuer, and verification result.verify=False (Python), -k (curl),
rejectUnauthorized: false (Node.js) all disable TLS security and enable MITM attacks. Fix
the underlying cert issue instead. [src1, src2]*.example.com covers
www.example.com but NOT example.com (apex) and NOT
sub.api.example.com (multi-level). Use multi-SAN certs. [src3, src8]| # | Error | Browser Message | Root Cause | Fix |
|---|---|---|---|---|
| 1 | Expired certificate | ERR_CERT_DATE_INVALID | Past notAfter date |
Renew; automate with Let's Encrypt [src1, src4] |
| 2 | Untrusted CA | ERR_CERT_AUTHORITY_INVALID | CA not in trust store | Use trusted CA; install intermediate certs [src1, src3] |
| 3 | Self-signed | NET::ERR_CERT_INVALID | No CA chain | Use Let's Encrypt for production [src1, src7] |
| 4 | Hostname mismatch | ERR_CERT_COMMON_NAME_INVALID | CN/SAN doesn't match domain | Reissue cert with correct SANs [src1, src3] |
| 5 | Incomplete chain | ERR_CERT_AUTHORITY_INVALID | Missing intermediate cert | Install full chain on server [src1, src3] |
| 6 | Protocol mismatch | ERR_SSL_VERSION_OR_CIPHER_MISMATCH | TLS 1.0/1.1 only; no shared cipher | Enable TLS 1.2/1.3 on server [src2, src6] |
| 7 | Certificate revoked | ERR_CERT_REVOKED | CA revoked the cert (OCSP/CRL) | Reissue certificate [src1, src8] |
| 8 | Pinning failure | ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN | HPKP/cert pinning mismatch | Update pinned key or wait for pin expiry [src2] |
| 9 | Mixed content | Mixed Content warning | HTTPS page loads HTTP resources | Change all resource URLs to HTTPS [src1, src2] |
| 10 | Clock skew | ERR_CERT_DATE_INVALID | Client clock wrong | Sync system clock (NTP) [src1, src4] |
| 11 | Wildcard mismatch | ERR_CERT_COMMON_NAME_INVALID | *.example.com ≠ sub.sub.example.com |
Use multi-SAN cert [src3, src8] |
START — SSL/TLS error
├── openssl s_client -connect host:443 -servername host
│ ├── "certificate has expired" → renew cert; check client clock [src4]
│ ├── "unable to get local issuer certificate" → incomplete chain [src1, src3]
│ ├── "self signed certificate" → replace with CA-signed cert [src1, src7]
│ ├── "hostname mismatch" → reissue cert with correct SANs [src3]
│ ├── "no shared cipher" → enable TLS 1.2/1.3 on server [src2, src6]
│ └── "certificate has been revoked" → reissue from CA [src1, src8]
└── Works in browser but fails in code?
├── Python → check certifi; use ssl.create_default_context() [src2]
├── Java → import cert into JVM truststore (keytool) [src2]
├── curl → use --cacert or update ca-certificates [src2]
├── Node.js → set NODE_EXTRA_CA_CERTS [src2]
└── Go → add cert to system trust store or custom tls.Config [src2]
The single most powerful diagnostic tool for any TLS issue. [src1, src3, src5]
# Full connection inspection
openssl s_client -connect example.com:443 -servername example.com
# Check certificate dates
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates
# Check Subject Alternative Names
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -text | grep -A5 "Subject Alternative Name"
# Verify certificate chain
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt fullchain.pem
# Check TLS version support
openssl s_client -tls1_2 -connect example.com:443 -servername example.com
openssl s_client -tls1_3 -connect example.com:443 -servername example.com
Verify: openssl s_client ... | grep "Verify return code" → expected:
Verify return code: 0 (ok)
Free, automated, currently 90-day certificates (moving to 45-day by 2028). [src4, src5, src7]
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
sudo systemctl enable certbot.timer
Verify: sudo certbot certificates → shows domain, expiry date, and certificate
path
Missing intermediates are the #1 silent failure. [src1, src3]
# Check chain count (should be 2+)
openssl s_client -connect host:443 -servername host 2>/dev/null | grep -c "BEGIN CERTIFICATE"
# Build full chain
cat certificate.pem intermediate.pem > fullchain.pem
# nginx config
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
Verify: grep -c "BEGIN CERTIFICATE" → expected: 2 or
3
Certificate must include all hostnames in SANs. [src1, src3]
# Check what hostnames the cert covers
openssl x509 -noout -text -in cert.pem | grep -A2 "Subject Alternative Name"
# Reissue with Let's Encrypt — add all domains
sudo certbot --nginx -d example.com -d www.example.com -d api.example.com
Verify: openssl x509 ... | grep -A2 "Subject Alternative Name" → shows all
expected domains
Trust a specific CA cert rather than disabling verification. [src1, src2]
# curl — trust specific CA
curl --cacert /path/to/ca.pem https://internal.example.com
# Add CA to system trust store (Ubuntu/Debian)
sudo cp ca.pem /usr/local/share/ca-certificates/my-ca.crt
sudo update-ca-certificates
Verify: curl --cacert /path/to/ca.pem https://internal.example.com → no SSL
errors
Enable TLS 1.2/1.3; disable TLS 1.0/1.1. [src2, src6]
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000" always;
Verify: openssl s_client -tls1_3 -connect host:443 → expected:
Protocol : TLSv1.3
#!/bin/bash
HOST="$1"; PORT="${2:-443}"
[ -z "$HOST" ] && echo "Usage: $0 <hostname> [port]" && exit 1
echo "=== SSL/TLS Diagnostic: $HOST:$PORT ==="
CERT=$(echo | openssl s_client -connect "$HOST:$PORT" -servername "$HOST" 2>/dev/null)
CERT_PEM=$(echo "$CERT" | openssl x509 2>/dev/null)
echo "=== Certificate Details ==="
echo "$CERT_PEM" | openssl x509 -noout -subject -issuer -dates 2>/dev/null
echo "=== SANs ==="
echo "$CERT_PEM" | openssl x509 -noout -text 2>/dev/null | grep -A3 "Subject Alternative Name" | tail -2
echo "=== Chain Count ==="
CHAIN=$(echo "$CERT" | grep -c "BEGIN CERTIFICATE")
echo " $CHAIN certificate(s) in chain $([ "$CHAIN" -lt 2 ] && echo '⚠️ may be incomplete')"
echo "=== Expiry ==="
NOT_AFTER=$(echo "$CERT_PEM" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
echo " Expires: $NOT_AFTER"
echo "=== Verification ==="
echo "$CERT" | grep "Verify return code"
echo "=== TLS Support ==="
for VER in tls1_2 tls1_3; do
echo -n " $VER: "
echo | openssl s_client -"$VER" -connect "$HOST:$PORT" -servername "$HOST" 2>&1 \
| grep -E "Protocol|handshake failure" | head -1
done
#!/usr/bin/env python3
import ssl, socket, datetime
def check_ssl_cert(hostname: str, port: int = 443) -> dict:
context = ssl.create_default_context()
try:
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
tls_version = ssock.version()
except ssl.SSLCertVerificationError as e:
return {"error": str(e), "hostname": hostname}
except Exception as e:
return {"error": str(e), "hostname": hostname}
not_after = datetime.datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
days_left = (not_after - datetime.datetime.utcnow()).days
sans = [v for t, v in cert.get("subjectAltName", []) if t == "DNS"]
return {
"hostname": hostname,
"days_until_expiry": days_left,
"sans": sans,
"tls_version": tls_version,
"status": "ok" if days_left > 0 else "expired",
}
def monitor_certs(hostnames):
for host in hostnames:
r = check_ssl_cert(host)
if "error" in r:
print(f"FAIL {host}: {r['error']}")
elif r["days_until_expiry"] < 0:
print(f"EXPIRED {host}: {abs(r['days_until_expiry'])} days ago!")
elif r["days_until_expiry"] < 14:
print(f"CRITICAL {host}: {r['days_until_expiry']} days left")
elif r["days_until_expiry"] < 30:
print(f"WARNING {host}: {r['days_until_expiry']} days left")
else:
print(f"OK {host}: {r['days_until_expiry']} days, {r['tls_version']}")
monitor_certs(["example.com", "api.example.com"])
#!/bin/bash
DOMAIN="$1"; EMAIL="$2"; EXTRA="${3:-}"
[ -z "$DOMAIN" ] || [ -z "$EMAIL" ] && echo "Usage: $0 <domain> <email> [extra-domains]" && exit 1
command -v certbot &>/dev/null || apt-get install -y certbot python3-certbot-nginx
DOMAIN_ARGS="-d $DOMAIN"
for d in $EXTRA; do DOMAIN_ARGS="$DOMAIN_ARGS -d $d"; done
certbot --nginx --non-interactive --agree-tos --email "$EMAIL" $DOMAIN_ARGS --redirect
certbot renew --dry-run
# Auto-renewal cron (if systemd timer not available)
systemctl is-active certbot.timer &>/dev/null || \
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab -
echo "Certificate issued for $DOMAIN — renewal is automatic"
# ❌ BAD — disables all TLS security [src1, src2]
import requests
response = requests.get('https://api.example.com', verify=False)
# curl equivalent: curl -k https://api.example.com
# ✅ GOOD — trust a specific CA cert [src1, src2]
import requests
# Public CA (default)
response = requests.get('https://api.example.com')
# Private/internal CA
response = requests.get('https://api.example.com', verify='/path/to/ca-bundle.pem')
# ❌ BAD — missing intermediate cert [src1, src3]
ssl_certificate /etc/ssl/certs/example.com.crt; # Just the leaf
# Works in Chrome (downloads via AIA), fails in curl/Python/Java
# ✅ GOOD — full chain: leaf + intermediate(s) [src1, src3]
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
# Build: cat example.com.crt intermediate.crt > fullchain.pem
# ❌ BAD — *.example.com does NOT cover sub.api.example.com [src3, src8]
Certificate SANs: DNS:*.example.com
Accessing: https://sub.api.example.com
→ ERR_CERT_COMMON_NAME_INVALID
# ✅ GOOD — explicitly list all needed subdomains [src3, src8]
certbot --nginx -d example.com -d www.example.com -d api.example.com -d sub.api.example.com
# ❌ BAD — assuming 398-day certs are still available after March 2026 [src6]
# Buying a 1-year cert after March 15, 2026 → max 200 days, not 398
# Manual renewal will not scale with 47-day certs by 2029
# ✅ GOOD — fully automated certificate management [src6, src7]
sudo certbot --nginx -d example.com --non-interactive --agree-tos --email [email protected]
sudo systemctl enable certbot.timer
# ACME Renewal Information (ARI) supported by certbot 2.8+
date on the client before debugging the cert. [src1, src4]-servername to
openssl s_client. Without it, you may get the wrong certificate. [src2, src3]
Strict-Transport-Security, it refuses HTTP for that domain. If your cert breaks, users
can't bypass it. Test HSTS carefully before setting long max-age. [src2]*.example.com covers
www.example.com but NOT example.com itself. Include both in your SAN list. [src3, src8]# Full TLS inspection
openssl s_client -connect host:443 -servername host
# Certificate dates
openssl s_client -connect host:443 -servername host 2>/dev/null | openssl x509 -noout -dates
# Subject Alternative Names
openssl s_client -connect host:443 -servername host 2>/dev/null \
| openssl x509 -noout -text | grep -A3 "Subject Alternative Name"
# Chain count (should be 2+)
openssl s_client -connect host:443 -servername host 2>/dev/null | grep -c "BEGIN CERTIFICATE"
# Verify chain
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt fullchain.pem
# TLS version testing
openssl s_client -tls1_2 -connect host:443 -servername host
openssl s_client -tls1_3 -connect host:443 -servername host
# curl verbose
curl -v https://host 2>&1 | grep -E "SSL|TLS|certificate|expire"
curl --cacert /path/to/ca.pem https://internal-host
# Days until expiry
echo | openssl s_client -connect host:443 -servername host 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2
# OCSP revocation check
openssl s_client -connect host:443 -servername host -status 2>/dev/null | grep -A3 "OCSP"
# Online tools:
# SSL Labs: https://www.ssllabs.com/ssltest/
# Chain check: https://whatsmychaincert.com/
# CT logs: https://crt.sh/
| Version / Standard | Status | Notes |
|---|---|---|
| TLS 1.3 (RFC 8446, 2018) | Current best | Faster handshake; forward secrecy mandatory; 93% of Cloudflare traffic (2025) [src2] |
| TLS 1.2 (RFC 5246, 2008) | Required minimum | Widely supported; still secure with modern ciphers [src2] |
| TLS 1.1 (RFC 4346, 2006) | Deprecated | Disabled in Chrome 84+, Firefox 78+, Safari 14+ [src2] |
| TLS 1.0 (RFC 2246, 1999) | Deprecated | Disabled in all modern browsers (2020) [src2] |
| SSL 3.0 | Broken (POODLE) | Never use; disabled everywhere [src2] |
| Let's Encrypt (2015+) | Free, automated | 90-day certs now; 45-day certs by Feb 2028 [src4, src7] |
| Chrome 58+ (2017) | CN deprecated | Only checks SANs, ignores Common Name [src3] |
| Chrome 80+ (2020) | TLS 1.0/1.1 blocked | ERR_SSL_VERSION_OR_CIPHER_MISMATCH for old TLS [src2] |
| CA/B Forum SC-081 (2025) | Validity shrinking | 200 days (Mar 2026) → 100 days (2027) → 47 days (2029) [src6] |
| Chrome 138+ (June 2026) | ClientAuth EKU removed | Public TLS certs can no longer support client authentication [src6] |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Browser shows certificate error | CORS error (different issue) | Fix CORS headers on server |
openssl s_client shows verify error |
Connection refused (port closed) | Check firewall/service |
| Certificate expired or expiring | DNS resolution failure | Fix DNS records |
| Hostname mismatch in error | HTTP 4xx/5xx errors | Debug application logic |
| "Works in browser, fails in code" | WebSocket errors after handshake | Debug application-level WS protocol |
verify=False or -k in production: It disables all
TLS security and makes MITM attacks trivial. Fix the underlying certificate issue instead. [src1, src2]must-staple extension, your
server must provide a valid OCSP staple or clients will reject the connection. [src8]CAA DNS records to restrict which CAs can issue
certificates for your domain. Example: example.com. CAA 0 issue "letsencrypt.org". [src8]$JAVA_HOME/lib/security/cacerts, not the OS trust store. Add custom CAs with
keytool -importcert. [src2]