openssl s_client is the standard way to see what a TLS server really sends. Its last lines contain a verdict, Verify return code: N (message), and that one line decides whether a client such as curl, Python or Java will trust the connection. This page explains how to run the command so the verdict is meaningful, and what each code means.
The command to run
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
-connect host:portopens the connection.-servernamesends SNI. Without it, a server hosting many sites may return a default certificate for a different name, and you will debug the wrong thing.-showcertsprints every certificate the server sends, in PEM.</dev/nullmakes the command exit right after the handshake instead of waiting for input.
To get a short summary, filter the output:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| grep -E 'depth|verify|^ *[0-9] s:|^ *i:|Verification|Verify return code'
A healthy result looks like this:
depth=2 C = US, O = Example Trust, CN = Example Root
verify return:1
depth=1 C = US, O = Example Trust, CN = Example Intermediate
verify return:1
depth=0 CN = example.com
verify return:1
0 s:CN = example.com
i:C = US, O = Example Trust, CN = Example Intermediate
1 s:C = US, O = Example Trust, CN = Example Intermediate
i:C = US, O = Example Trust, CN = Example Root
Verify return code: 0 (ok)
Reading it: depth=0 is the server's own certificate, higher depths are intermediates and the root. s: is the subject and i: the issuer of each certificate sent. The chain is right when each certificate's issuer matches the next one's subject.
Two things that make the verdict misleading
- s_client does not check the hostname by default. A certificate for the wrong name still ends with
Verify return code: 0 (ok)unless you add-verify_hostname example.com. Add it whenever you suspect a name problem. - It uses OpenSSL's trust store, not your browser's or your language's. The default location varies by system; see
openssl version -d. A result of 0 here does not guarantee that Java'scacertsor a Pythoncertifibundle trusts the chain. To test against a specific bundle, pass-CAfile /path/to/ca-bundle.pemor-CApath /etc/ssl/certs.
Add -verify_return_error to make the handshake abort on the first failure, which is what strict clients do.
What each code means
| Code | Message | Meaning and fix |
|---|---|---|
| 0 | ok | Chain validated against the trust store you used. |
| 9 | certificate is not yet valid | The certificate's start date is in the future, or the checking machine's clock is wrong. Check date. |
| 10 | certificate has expired | The certificate is past its end date. Renew it; see renewing and automating SSL. |
| 18 | self-signed certificate | The leaf certificate signs itself and is not in the trust store. Fine for a lab, not for production. |
| 19 | self-signed certificate in certificate chain | A certificate partway up the chain is self-signed and untrusted, typical of an inspecting proxy. See self signed certificate in certificate chain. |
| 20 | unable to get local issuer certificate | The issuer of a certificate is not available locally. See unable to get local issuer certificate. |
| 21 | unable to verify the first certificate | Usually the server sent only the leaf and not the intermediate. See incomplete certificate chain. |
| 62 | hostname mismatch | Appears with -verify_hostname. The name you asked for is not in the certificate. See certificate name mismatch and SAN. |
Other numbers exist. Look up any code with openssl verify -help and the X509_verify_cert manual page, which lists the full set.
Interpreting a real failure
Here is what a server that sends only its leaf certificate looks like when the root is trusted:
depth=0 CN = www.example.test
verify error:num=20:unable to get local issuer certificate
...
0 s:CN = www.example.test
i:CN = Test Intermediate
Verification error: unable to verify the first certificate
Verify return code: 21 (unable to verify the first certificate)
Only one certificate is listed (index 0), and its issuer is an intermediate that was never sent. That tells you the fix is on the server: serve the full chain.
Check the details of the certificate
Pipe the leaf into x509 for names and dates:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
Compare the subjectAltName list to the hostname, and notAfter to today's date. The SSL Checker reports the same facts, and the SSL Chain Debugger draws the chain.
Other useful flags
-tls1_2or-tls1_3forces a protocol version, to test what the server accepts. See also sslv3 alert handshake failure.-starttls smtp(orimap,pop3,ftp) tests mail and other services that upgrade to TLS.-briefprints a condensed summary on OpenSSL 1.1.1 and later.-proxy host:portconnects through an HTTP proxy.
Next steps
- Inspect any site's chain with the SSL Chain Debugger.
- Check expiry and names with the SSL Checker.
- Test accepted protocols with the TLS Version Checker.
- Read SSL/TLS certificate errors explained.
FAQ
Does "Verify return code: 0 (ok)" mean the site is correct?
It means the chain validated against the trust store you used. It does not check the hostname unless you pass -verify_hostname, and other clients may use different trust stores.
Why does s_client say "Can't use SSL_get_servername"?
That message is printed when you did not supply a usable server name, or the connection is to an IP address. Use the hostname with -servername so SNI is sent.
Why do I see several verify errors before the final code?
OpenSSL prints an error for each certificate in the chain as it checks it. The last line, the verdict, is what counts.
What does depth mean?
Depth 0 is the server certificate, depth 1 is the intermediate that signed it, and so on up to the root.
Get notified when this fix updates
Plus occasional practical tips on uptime and monitoring, nothing else. Unsubscribe anytime.
This page is general information written by our editorial team and updated when documentation changes. Servers and providers differ, so test changes safely first. See our editorial policy and disclaimer.