HackBlue

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.

10 min read · practice only with authorization

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.

Snippet
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.

Snippet
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?