SSL/TLS handshake failures are among the most frustrating issues to debug. The error messages are often opaque ("sslv3 alert certificate unknown"), and the handshake itself involves a dozen negotiation steps that can fail at any point.
This guide covers the repeatable diagnostic approach I use.
1. The Swiss Army Knife: openssl s_client
openssl s_client is the single most valuable TLS debugging tool:
# Basic connection test
openssl s_client -connect example.com:443
# Show full certificate chain
openssl s_client -connect example.com:443 -showcerts
# Debug a specific TLS version
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
# Specify SNI (critical for multi-tenant hosts)
openssl s_client -connect 1.2.3.4:443 -servername example.com
What the output tells you:
CONNECTED(00000003)— TCP connection succeededverify error:num=...— certificate validation failureSSL-Session:block — negotiated cipher, protocol version---— clean closure means the full handshake completed
2. Certificate Chain Problems
The most common class of TLS failure:
# Verify the full chain
openssl s_client -connect example.com:443 -showcerts -verify_return_error
# Download each certificate manually
openssl s_client -connect example.com:443 -showcerts 2>/dev/null </dev/null | awk '/^-----BEGIN CERTIFICATE-----/,/^-----END CERTIFICATE-----/' > chain.pem
# Verify the chain locally
openssl verify -CApath /etc/ssl/certs chain.pem
Common chain issues:
- Missing intermediate cert — the server only sends the leaf certificate. Fix: configure the full chain on the server.
- Wrong intermediate — the server sends an intermediate that doesn't chain to the leaf. Regenerate with the correct CA.
- Expired intermediate — check the validity period of each cert in the chain.
- Self-signed root in chain — roots should NOT be sent by the server; the client trusts them locally.
3. Cipher Suite Mismatch
When client and server can't agree on a cipher:
# List ciphers supported by your openssl version
openssl ciphers -v 'ALL:COMPLEMENTOFALL'
# Test a specific cipher
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384'
# See which ciphers the server advertises
openssl s_client -connect example.com:443 </dev/null 2>/dev/null | openssl ciphers -V 'ALL' | head -20
Diagnostic approach:
- Check the server's minimum TLS version (TLS 1.0/1.1 ciphers are different from 1.3)
- Check if the client's cipher list is restricted (Java
jdk.tls.disabledAlgorithms, Gocrypto/tls.Config.CipherSuites) - Test with a modern client to isolate whether it's a server or client configuration issue
4. Protocol Version Negotiation
# Test each protocol version explicitly
for ver in tls1 tls1_1 tls1_2 tls1_3; do
echo "=== $ver ==="
openssl s_client -connect example.com:443 -$ver </dev/null 2>&1 | grep -E '(CONNECTED|verify error|SSL handshake)'
done
Common version issues:
- Server only supports TLS 1.2, client requires 1.3
- Server has TLS 1.0/1.1 enabled (security audit finding)
- Middlebox (F5, AWS NLB) strips TLS 1.3 ClientHello due to byte 44 issue
5. Debugging with Wireshark/TShark
When OpenSSL succeeds but your application fails:
# Capture the handshake
tshark -i eth0 -f "tcp port 443" -w handshake.pcap -a duration:30
# Filter for TLS handshake packets
tshark -r handshake.pcap -Y "tls.handshake"
# View ClientHello and ServerHello details
tshark -r handshake.pcap -Y "tls.handshake.type == 1 || tls.handshake.type == 2" -T fields -e tls.handshake.ciphersuite -e tls.handshake.extensions_server_name
What to look for in the capture:
- ClientHello → ServerHello round-trip time (high latency = network issue)
- Certificate size (large certs > 8KB can cause fragmentation)
- ServerHello has no cipher overlap with ClientHello
6. OCSP Stapling
# Check if the server staples OCSP responses
openssl s_client -connect example.com:443 -status </dev/null 2>&1 | grep -A 20 'OCSP response:'
# A successful response looks like:
# OCSP Response Status: successful (0x0)
# Response Type: Basic OCSP Response
Missing OCSP stapling can cause slow page loads as the browser fetches revocation status separately.
7. Application-Layer TLS Issues
Sometimes TLS works mechanically but the application rejects the connection:
# Check SNI mismatch (server returns default cert)
openssl s_client -connect example.com:443 -servername wrong.example.com
# Check ALPN (Application-Layer Protocol Negotiation)
openssl s_client -connect example.com:443 -alpn http/1.1 </dev/null 2>&1 | grep -i 'alpn'
# Check for client certificate requirements
# (server sends CertificateRequest during handshake)
openssl s_client -connect example.com:443 -state 2>&1 | grep 'CertificateRequest'
Diagnostic Decision Tree
Handshake fails?
├── openssl s_client succeeds?
│ ├── Yes → Problem is in your application's TLS stack
│ │ └── Compare cipher lists, CA bundles, TLS versions
│ └── No → Server-side issue
│ ├── Certificate error?
│ │ └── Check chain, expiry, hostname match
│ ├── Protocol error?
│ │ └── Test each TLS version, check SNI
│ └── Connection timeout?
│ └── Firewall, load balancer, or routing issue
The key insight: always start with openssl s_client. If it connects, your application is the problem. If it fails, the server or network is the problem. Don't cross the streams.