Tools · Cryptography and passwords in practice · 03 / 20
Troubleshooting TLS with openssl s_client: SNI, chains, protocols, and verification errors
Connect to a TLS server with s_client, show the full chain, test specific protocol versions, and interpret verify return codes to fix real certificate problems.
Connecting correctly
Always pass -servername so the server knows which certificate to present (SNI). Use -showcerts to print every certificate the server sends. The verify return code at the end tells you whether the chain validated against your trust store: 0 means OK.
openssl s_client -connect lab.internal:443 -servername lab.internal -showcerts </dev/null openssl s_client -connect lab.internal:443 -servername lab.internal -brief </dev/null openssl s_client -connect lab.internal:443 -servername lab.internal -tls1_2 </dev/null openssl s_client -connect lab.internal:443 -servername lab.internal -tls1_3 -ciphersuites TLS_AES_128_GCM_SHA256 </dev/null
Verify return codes
Code 20, unable to get local issuer certificate: the server did not send the intermediate, or your trust store lacks the root. Code 10, certificate has expired. Code 18 or 19: self-signed certificate or self-signed certificate in chain, normal in a lab with a private CA unless you pass -CAfile. Code 62, hostname mismatch, appears when you add -verify_hostname.
Lab: break and fix a chain
Set up a lab CA, issue an intermediate and a server certificate, and configure nginx with only the leaf certificate. Connect with s_client -CAfile pointing at the root: you get code 20. Reconfigure nginx with a full chain file (leaf followed by intermediate) and reconnect: verify return code 0. Then use -verify_hostname with a wrong name and read the error.
cat server.pem intermediate.pem > fullchain.pem # nginx: ssl_certificate fullchain.pem; openssl s_client -connect lab.internal:443 -servername lab.internal -CAfile root.pem </dev/null 2>/dev/null | grep -E 'Verify return code|depth' openssl s_client -connect lab.internal:443 -servername lab.internal -CAfile root.pem -verify_hostname wrong.internal </dev/null 2>&1 | grep -i verify
Other useful checks
Use -status to see if the server staples an OCSP response, and -alpn h2 to test HTTP/2 negotiation. For STARTTLS services such as SMTP, add -starttls smtp. For a full configuration audit, use testssl.sh, covered in another lesson.
Diagram
Verify return codes you will see
The number names the problem.
- 0 ok
Chain and trust are fine.
- 10 certificate has expired
Renew; check server and client clocks.
- 18 / 19 self-signed
Expected with a private CA; pass -CAfile.
- 20 unable to get local issuer
Missing intermediate or untrusted root.
- 62 hostname mismatch
Name not in SAN (with -verify_hostname).
Check yourself
1. Which option sends SNI so the server presents the right certificate?
2. s_client reports verify return code 20. What is the most likely fix on the server?
3. How do you test TLS on an SMTP server that upgrades with STARTTLS?