Chapter 7 of 8

Debugging handshake failures

A repeatable method, the tools to use, and a symptom-to-cause table.

About 11 minutes of reading

By the end of this chapter you can

  • Apply a structured diagnosis instead of guesswork
  • Use openssl s_client to inspect what a server actually sends
  • Map common client errors to root causes
  • Know when a failure is client-side or server-side

01A method, not a guess

  1. 1

    Reproduce from the client's position

    Same hostname, same network path, same client software. A laptop on the corporate network with a proxy is a different client.

  2. 2

    Read the exact error

    'Unable to get local issuer certificate' is not the same as 'unknown authority'. Each points somewhere different.

  3. 3

    Inspect the presented chain

    Confirm the leaf, the intermediates and their order.

  4. 4

    Check names and dates

    SAN coverage and validity windows.

  5. 5

    Check trust store

    Is the root present in this runtime's store, and is the system clock sane?

  6. 6

    Only then change configuration

    Change one variable at a time and re-test.

02The tools

# 1. What does the server send?
echo | openssl s_client -connect host:443 -servername host -showcerts 2>/dev/null

# 2. What does the leaf say?
openssl s_client -connect host:443 -servername host </dev/null 2>/dev/null | openssl x509 -noout -text

# 3. What protocol and suite were negotiated?
openssl s_client -connect host:443 -servername host </dev/null 2>/dev/null | grep -E "Protocol|Cipher"

# 4. Does a specific trust store accept the chain?
openssl s_client -connect host:443 -servername host -CAfile ca-bundle.pem -verify_return_error </dev/null
The four commands that solve most incidents

For deeper work, Wireshark shows the handshake as a sequence of records, which makes downgrade and negotiation problems visible. For client-side issues in Java or Python, remember they carry their own trust stores.

03Symptom to cause

MessageLikely causeWhere to look
Unable to get local issuer certificateIntermediate missing from the bundleServer certificate chain
Certificate has expiredRenewal or reload did not happenServed certificate serial and file dates
Hostname mismatchSAN does not contain the requested nameThe exact hostname the client uses
Self-signed certificate in chainPrivate root not installedClient trust store
Unknown authority / PKIX path failureRoot absent in this runtimeRuntime CA bundle
Protocol version mismatchServer disabled a version the client needsEnabled protocol list on the server
Handshake failure with no detailNo shared cipher suiteCipher configuration on both sides

04Worked example

A Python service fails with 'certificate verify failed', while curl on the same host succeeds. curl uses the system store and follows AIA to fetch a missing intermediate; the Python client uses its own bundle and does not. The fix is not to relax verification in Python, but to correct the chain the server sends.

A browser works but a container fails. First hypothesis?

Incomplete chain or a different trust store inside the container image.

Renewal ran and the file looks correct, but clients still see the old certificate. Why?

The service was not reloaded, or a load balancer/edge is terminating TLS with its own copy.

Why check the system clock when debugging?

An incorrect clock makes valid certificates look expired or not yet valid.

What to remember

  • Diagnose from the client's position with the client's exact hostname
  • openssl s_client answers most questions in one command
  • Most failures map to chain, name, dates or trust store
  • Never resolve a certificate error by disabling verification