OpenSSL s_client: The TLS Debugging Tool I Reach For Every Day

If I had to keep only one command-line tool for TLS troubleshooting, it would be openssl s_client.

Not because it is pretty. It is not. The output is noisy, old-fashioned, and easy to misread when you are in a hurry. But it connects directly to the service, shows you the certificate chain, reveals the negotiated protocol and cipher, lets you test SNI, OCSP stapling, client certificates, STARTTLS, and version-specific behavior, and it works almost everywhere.

I use it when a browser error is too vague, when a load balancer might be serving the wrong certificate, when a mail server STARTTLS path looks suspicious, when someone swears “the cert is fine,” or when I want proof of what the remote endpoint is really doing.

This is the practical guide I wish I had at the beginning.

The basic connection test

The smallest useful command is:

openssl s_client -connect example.com:443

That opens a TLS connection to example.com on port 443 and prints handshake details.

It is enough to confirm that:

  • the port is speaking TLS
  • a certificate is being presented
  • the handshake completed or failed
  • a protocol and cipher were negotiated

But on multi-site hosts, that command is often incomplete because it does not always send the hostname for SNI the way you expect. For most public HTTPS testing, I use -servername explicitly.

Always remember -connect host:port

The -connect argument is the anchor of the tool:

openssl s_client -connect api.example.com:8443

Use it whenever you want to test a non-default TLS port. I use this a lot for:

  • admin panels
  • HTTPS services on alternate ports
  • LDAPS
  • internal APIs
  • backend listeners behind a load balancer

A surprising number of “TLS version mismatch” issues are actually “wrong port” issues. If you point s_client at a port speaking plain HTTP or some other protocol, the error text can be misleading.

Why -servername matters for SNI

On modern hosting, many domains share one IP. Without SNI, the server may return the default certificate instead of the one you care about.

Use this form:

openssl s_client -connect example.com:443 -servername example.com

If I am troubleshooting the wrong certificate on a site, I immediately compare these two commands:

openssl s_client -connect example.com:443
openssl s_client -connect example.com:443 -servername example.com

If the certificates differ, the problem is already half diagnosed.

For a fuller discussion of that behavior, this SNI guide explains why it happens.

How to read the output without getting lost

s_client dumps a lot of text. The main sections I pay attention to are consistent.

Certificate chain

You will usually see something like:

Certificate chain
 0 s:CN = example.com
   i:C = US, O = Example CA, CN = Example Intermediate CA
 1 s:C = US, O = Example CA, CN = Example Intermediate CA
   i:C = US, O = Example Root, CN = Example Root CA

This tells you:

  • certificate 0 is the leaf presented by the server
  • higher numbers are intermediates sent by the server
  • the s: field is the subject
  • the i: field is the issuer

This is where I check whether the chain is incomplete, unexpected, or served in the wrong order.

If chain behavior is confusing, this OpenSSL chain verification guide goes deeper.

Verification result

Near the end, you often see:

Verify return code: 0 (ok)

That is what you want.

If it is not 0, the text usually points toward:

  • hostname mismatch
  • unknown CA
  • missing intermediate
  • expired certificate
  • self-signed certificate without trust anchor

Negotiated protocol and cipher

Look for lines like:

New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

or

Protocol  : TLSv1.2
Cipher    : ECDHE-RSA-AES128-GCM-SHA256

That tells you what actually got negotiated, which matters more than what the config file claims.

Session details

The session section is not always needed, but it can be useful for confirming ticket behavior, resumption details, and general handshake state.

Use -showcerts when you want the whole chain

This is one of the most useful flags:

openssl s_client -connect example.com:443 -servername example.com -showcerts

It prints all certificates the server sends, each in PEM format. That helps when you want to:

  • save the chain for later inspection
  • compare intermediates between nodes
  • extract the leaf certificate cleanly

To pipe the first certificate into openssl x509:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

I use that pipeline constantly because it turns a wall of output into the details I actually need.

Fail hard on verification errors with -verify_return_error

By default, s_client may continue far enough to show you useful data even when verification failed. That is helpful, but sometimes you want the command to abort on verification problems.

Use:

openssl s_client -connect example.com:443 -servername example.com -verify_return_error

This is useful when you are scripting checks or want a more decisive result during chain debugging.

Testing specific TLS versions

Version-specific testing matters a lot during protocol hardening.

TLS 1.2

openssl s_client -connect example.com:443 -servername example.com -tls1_2

TLS 1.3

openssl s_client -connect example.com:443 -servername example.com -tls1_3

If one succeeds and the other fails, that tells you something concrete about server support, middlebox behavior, or client compatibility.

Trying to force deprecated versions like TLS 1.0 or 1.1 can also be informative in legacy audits, but for a modern public service I expect them to fail.

If you are cleaning up old protocol support, this TLS 1.3 vs TLS 1.2 article is relevant.

Testing with a specific cipher

When I want to prove whether a server still accepts a specific TLS 1.2 cipher suite, I force it.

Example:

openssl s_client -connect example.com:443 -servername example.com \
  -tls1_2 -cipher 'ECDHE-RSA-AES128-GCM-SHA256'

If it connects, that cipher is enabled. If it fails with no shared cipher, your server rejected it.

This is handy after cipher hardening changes. It pairs well with this guide to cipher suites and hardening.

For TLS 1.3, use -ciphersuites rather than -cipher:

openssl s_client -connect example.com:443 -servername example.com \
  -tls1_3 -ciphersuites 'TLS_AES_128_GCM_SHA256'

That distinction trips people up all the time.

Checking OCSP stapling with -status

If the server is supposed to staple an OCSP response, this is the flag you want:

openssl s_client -connect example.com:443 -servername example.com -status

In the output, look for an OCSP response section and the certificate status.

Typical healthy output includes:

OCSP Response Status: successful
Cert Status: good

If there is no stapled response, s_client will usually say so clearly.

That is the quickest live check I know for OCSP stapling. It is especially useful after Nginx or Apache changes.

For the broader revocation context, this CRL vs OCSP guide covers why stapling matters.

Checking certificate expiry from live output

To see expiry dates on a live endpoint without saving files:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates

That returns:

notBefore=Jul  1 00:00:00 2026 GMT
notAfter=Sep 29 23:59:59 2026 GMT

For quick monitoring scripts, I often add subject and SAN output too:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -dates -ext subjectAltName

Testing client certificate authentication

If the server requires mutual TLS, s_client can present a client certificate and key.

openssl s_client -connect mtls.example.com:443 -servername mtls.example.com \
  -cert client.crt -key client.key

If the client key is encrypted, you will be prompted for the passphrase.

This is one of the fastest ways to prove whether the client certificate itself is accepted before you start debugging application logic.

When mTLS is involved, I also check the certificate chain for the client cert and whether the server requests a certificate during handshake. For broader design context, the mTLS guide is worth reading.

Using STARTTLS for mail servers

Mail protocols are where s_client becomes especially valuable.

A server on port 25 or 587 may start in plaintext and upgrade to TLS only after a protocol command. That is what -starttls handles.

SMTP

openssl s_client -connect mail.example.com:25 -starttls smtp

Submission (587)

openssl s_client -connect mail.example.com:587 -starttls smtp

IMAP

openssl s_client -connect mail.example.com:143 -starttls imap

POP3

openssl s_client -connect mail.example.com:110 -starttls pop3

Without -starttls, you may get confusing handshake errors because the server is waiting for protocol commands, not a raw TLS ClientHello.

This is a classic cause of the dreaded “wrong version number” output.

Testing through a proxy

Sometimes you need to reach a TLS endpoint through an HTTP proxy. Recent OpenSSL builds support -proxy.

openssl s_client -connect example.com:443 -servername example.com \
  -proxy proxy.example.net:8080

That is useful in restricted networks or when reproducing a client environment that cannot connect directly.

If the OpenSSL build on a box does not support -proxy, I usually fall back to other tooling or test from a host with a newer build.

Reading certificate details directly from the output

I rarely want to manually scan the PEM block. I pipe it into openssl x509.

Subject, issuer, and SANs

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -ext subjectAltName

Full parsed certificate

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -text

That is how I inspect key usage, extended key usage, AIA, CRL distribution points, and other extensions on a live server.

Common error messages and what they usually mean

s_client is powerful, but the error text can be blunt.

wrong version number

Despite the name, this often means:

  • you connected to a non-TLS port
  • you forgot -starttls for a mail protocol
  • a reverse proxy is returning plaintext
  • a backend path is miswired

self signed certificate

Usually means the chain leads to a self-signed root that your local trust store does not trust, or the server is incorrectly presenting a self-signed leaf.

unable to get local issuer certificate

Often means:

  • missing intermediate in the server chain
  • your local trust store is incomplete
  • you are testing a private PKI without providing the CA

no peer certificate available

The server did not complete the TLS handshake far enough to present a cert. That can mean the service is not TLS at all, is failing immediately, or requires some other protocol negotiation first.

handshake failure

This is generic, but common causes include:

  • protocol mismatch
  • no shared cipher
  • client certificate required but not provided
  • server policy rejecting the handshake early

Saving what the server sent for later analysis

During longer incidents, I often want more than live output on the screen. I want the certificate or chain saved so I can compare nodes, attach evidence to a ticket, or inspect extensions without reconnecting repeatedly.

The simplest pattern is to capture the leaf certificate from the live handshake:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -outform PEM > live-leaf.pem

Then inspect it normally:

openssl x509 -in live-leaf.pem -noout -subject -issuer -dates -fingerprint -sha256

If I need the full chain, I usually run -showcerts and split the PEM blocks manually into separate files. It is not glamorous, but it works everywhere. This is especially helpful when different load balancer nodes may be serving different intermediates or when a vendor-managed edge says it updated the certificate and I want proof of exactly what came back over the wire.

One practical warning: s_client shows what that connection received. If a service sits behind anycast, geo-routing, or a mixed node pool, reconnect several times or test from multiple networks before assuming the result is globally consistent.

A short troubleshooting workflow I reuse constantly

When a TLS endpoint is failing, this is my usual sequence.

1. Confirm the service speaks TLS

openssl s_client -connect example.com:443

2. Add explicit SNI

openssl s_client -connect example.com:443 -servername example.com

3. Inspect live certificate details

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

4. Force protocol versions

openssl s_client -connect example.com:443 -servername example.com -tls1_2
openssl s_client -connect example.com:443 -servername example.com -tls1_3

5. Check stapling

openssl s_client -connect example.com:443 -servername example.com -status

6. Force or rule out a specific cipher if needed

openssl s_client -connect example.com:443 -servername example.com \
  -tls1_2 -cipher 'ECDHE-RSA-AES128-GCM-SHA256'

That sequence solves a large percentage of TLS incidents before I need heavier tools.

Deprecated habits vs current practice

Deprecated habit: testing HTTPS without SNI

On shared hosting or CDN-backed sites, that often gives you the wrong certificate and a false diagnosis.

Deprecated assumption: s_client output is only for experts

It is verbose, but the key sections are consistent. Once you learn what to scan for, it becomes one of the fastest troubleshooting tools available.

Current practice: pipe s_client into openssl x509

That reduces noise and gives you exactly the certificate details you care about.

Conclusion

openssl s_client is not elegant, but it is one of the most useful TLS troubleshooting tools on any Linux box. It lets you test the exact endpoint, force SNI, inspect the chain, verify protocol support, check stapled OCSP responses, present client certificates, and debug STARTTLS flows without needing a browser or a big scanning platform.

The key is to use it intentionally. I almost always start with -connect, add -servername, and then pipe the certificate into openssl x509 for the specific fields I need. Once that becomes muscle memory, a lot of TLS problems stop looking mysterious.

I still keep broader scanners around, but s_client is usually the first thing I run because it shows the raw conversation with very little abstraction. When TLS is broken, fewer layers usually means a faster answer.

That habit has saved me countless log-diving sessions during production incidents and awkward maintenance windows alike. Simple tools usually expose the truth faster.

Scroll to Top