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
0is 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
-starttlsfor 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.