Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Debug TLS Issues with openssl s_client

By Kokil Thapa | Last reviewed: September 2026

HTTPS failures often look like vague browser errors or API timeouts. To debug TLS issues with openssl s_client, you connect from the command line and read the handshake, certificate chain, and cipher details directly. That beats guessing from nginx logs alone. I've used this workflow on Linux production servers when Laravel apps, payment webhooks, or legal portals suddenly stopped talking to upstream APIs after a cert renewal.

What is openssl s_client and why use it to debug TLS?

openssl s_client is OpenSSL's built-in TLS client. It opens a raw TLS session to any host and port. You see exactly what the server presents during the handshake. Browsers cache intermediates and hide details. Load balancers may terminate TLS on a different node than you expect. s_client removes that noise.

On a real client project, a webhook kept failing with "SSL certificate problem." curl returned the same error. One s_client run showed an expired intermediate, not the leaf cert the client was staring at. The fix took ten minutes once the chain was visible.

Use s_client when you need to answer concrete questions:

  • Does the server present a complete certificate chain?
  • Which TLS version and cipher actually negotiate?
  • Does SNI return the correct certificate on a shared IP?
  • Is the server's CA trusted by your OpenSSL build?
  • Does mTLS client auth reject your certificate?

For broader context on protocol differences, read the TLS 1.2 vs TLS 1.3 comparison. For nginx-specific tuning after you find the fault, see TLS 1.3 vs 1.2 configuration for nginx.

Debug TLS Issues with openssl s_clientSymptomBrowser or API fails_clientHandshake probeRead outputChain, verify, cipherFixCert or configCommon failure categoriesExpired certBad chainSNI mismatchTLS versionPair with dig and traceroute when DNS or routing is suspectValidate fix by re-running the same s_client command
End-to-end workflow to debug TLS issues with openssl s_client before touching production config

How do you run openssl s_client for a basic HTTPS check?

Start with the simplest command that mirrors what a browser does. Replace example.com with your hostname.

openssl s_client -connect example.com:443 -servername example.com < /dev/null

The -connect flag sets host and port. The -servername flag sends SNI. Without SNI, many shared hosts return a default certificate. That looks like a name mismatch even when the right vhost exists.

Pipe empty input with < /dev/null so s_client exits after the handshake. Otherwise it waits for HTTP input on stdin.

Read the output you actually care about

Scroll past the certificate PEM blocks. Focus on these lines near the bottom:

  1. Protocol and cipher — confirms negotiated TLS version and algorithm.
  2. Verify return code — zero means OpenSSL trusts the chain with its default store.
  3. Certificate chain depth — count how many certs the server sent.
  4. subject= and issuer= — confirm hostname and signing CA.

A healthy result looks like this pattern:

SSL handshake has read 4521 bytes and written 401 bytes
---
Protocol  : TLSv1.3
Cipher    : TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)
---

Non-zero verify codes map to real fixes. Code 10 means the cert expired. Code 21 means unable to verify the local issuer — usually a missing intermediate. The OpenSSL verify documentation explains each code in detail at docs.openssl.org.

If DNS might be wrong, cross-check with the dig and traceroute troubleshooting guide before you chase certificate ghosts.

Which openssl s_client flags matter most for TLS debugging?

Defaults hide useful detail. These flags cover most production scenarios I've hit on Ubuntu 22/24 servers and client APIs.

FlagPurposeWhen to use it
-servername HOSTSends SNI extensionShared IP, CDN, or multi-site nginx/apache
-showcertsPrints full chain PEMExport intermediates, compare with browser chain
-tls1_2 / -tls1_3Forces protocol versionIsolate legacy client failures
-CAfile pathCustom trust anchorPrivate CA, corporate proxy, mTLS roots
-cert / -keyClient certificateDebug mutual TLS on APIs or webhooks
-verify_hostname HOSTChecks CN/SAN matchConfirm cert matches expected hostname
-briefShorter outputQuick smoke tests in CI or cron jobs
-starttls smtp|ftp|...Upgrade plain connectionMail servers and non-443 TLS

For a legal-tech portal I maintain, a payment gateway callback failed only on the production server. Forcing TLS 1.2 with -tls1_2 reproduced the error. The provider had disabled an old cipher the server's OpenSSL build preferred. Upgrading the system CA bundle and tightening nginx ciphers fixed it. The SSL/TLS certificates explained article covers chain anatomy if PEM order still confuses you.

s_client Handshake SequenceClient HelloServer Hello+ cert chainKey ExchangeDoneWhat s_client prints at each stage1. Cipher list offered and selected2. Certificate PEM blocks with subject/issuer3. OCSP stapling status if present4. Verify return code after chain build5. Session ticket and ALPN when usedFailure stops before step 4 — note the alert
TLS handshake stages visible when you debug TLS issues with openssl s_client against port 443

How do you diagnose certificate chain and SNI problems?

Missing intermediates are the most common TLS bug I see after Let's Encrypt renewals or sloppy manual installs. Browsers often fetch missing intermediates automatically. Server-side HTTP clients and older OpenSSL builds do not.

Export and inspect the full chain

openssl s_client -connect example.com:443 -servername example.com -showcerts < /dev/null 2>/dev/null \
  | awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/{print}' > chain.pem

openssl crl2pkcs7 -nocrl -certfile chain.pem | openssl pkcs7 -print_certs -noout

Count the certificates. A typical public site needs at least two: leaf plus intermediate. If you only see one, nginx or Apache is not sending the full chain. Fix the ssl_certificate bundle, not the leaf alone.

Test SNI explicitly

Compare default vs named vhost responses on the same IP:

openssl s_client -connect 203.0.113.10:443 < /dev/null 2>&1 | grep "subject="
openssl s_client -connect 203.0.113.10:443 -servername api.example.com < /dev/null 2>&1 | grep "subject="

Different subjects mean SNI routing works. Same wrong subject on the second command means the vhost or CDN host header mapping is broken. I've seen this on sister sites sharing one EC2 box — the fix was a missing ServerName in Apache.

Verify hostname against certificate SANs

openssl s_client -connect example.com:443 -servername example.com \
  -verify_hostname example.com < /dev/null 2>&1 | tail -5

Hostname verification is separate from chain trust. You can have verify code 0 and still serve the wrong cert for the hostname if SNI is misconfigured.

Sites like Notary Kathmandu and other law portals on shared deploy pipelines depend on correct per-domain certs. One wrong default vhost breaks trust for every form submission on that IP.

How do you debug TLS handshake failures and protocol mismatches?

Handshake failures show as alerts in s_client output: sslv3 alert handshake failure, protocol version, or certificate unknown. Treat the alert as a category, then narrow with forced versions.

Step-by-step isolation workflow

  1. Run the baseline command with SNI and note the verify code.
  2. Retry with -tls1_3 only, then -tls1_2 only.
  3. If both fail, test port reachability with nc -zv host 443.
  4. Compare cipher with -cipher 'ECDHE' vs default.
  5. Check server clock — skew breaks validity windows silently.
  6. Re-test after each config change with the same command.

Force TLS 1.2 when debugging legacy integrations:

openssl s_client -connect legacy-api.example.com:443 -servername legacy-api.example.com -tls1_2 < /dev/null

Force TLS 1.3 to confirm modern path:

openssl s_client -connect example.com:443 -servername example.com -tls1_3 < /dev/null

Mozilla's SSL Configuration Generator documents cipher and protocol baselines for nginx, Apache, and HAProxy at ssl-config.mozilla.org. Match your server config to what s_client actually negotiates, not what you think you enabled.

For Kubernetes ingress with cert-manager, handshake errors sometimes come from stale secrets rather than bad certs on the origin. The Kubernetes ingress and TLS with cert-manager guide pairs well with s_client checks against the public load balancer IP.

Verify Return Code Decision TreeVerify return code?Code 0Chain OKCode 21Missing intermediateCode 10Expired certCheck SNI + hostnameif verify OK but app failsAppend intermediate PEMto ssl_certificate bundleRenew with certbotreload php-fpm or nginxRe-run s_client until verify code stays 0
Map openssl s_client verify return codes to concrete certificate and config fixes

How do you debug mTLS, STARTTLS, and non-443 TLS ports?

Not every TLS endpoint listens on 443. SMTP, IMAP, LDAP, and internal APIs use other ports or STARTTLS upgrades. s_client handles both patterns.

Mutual TLS with client certificates

openssl s_client -connect api.example.com:8443 \
  -servername api.example.com \
  -cert client.crt -key client.key \
  -CAfile ca-bundle.crt < /dev/null

If the server rejects your cert, s_client shows Acceptable client certificate CA names. Compare that list with your client cert's issuer. Mismatch here blocked a webhook on a production Laravel app until the client PEM was reissued from the provider's CA.

STARTTLS for mail debugging

openssl s_client -starttls smtp -connect mail.example.com:587 -crlf < /dev/null

Add -crlf for SMTP line endings. For IMAP use -starttls imap on port 143. These commands expose the same chain and verify issues as HTTPS.

Connect to a specific IP behind a CDN

When Cloudflare or another CDN masks the origin, test the origin directly:

openssl s_client -connect 198.51.100.22:443 \
  -servername origin.example.com < /dev/null

If origin cert is fine but edge fails, the problem is CDN SSL mode or edge certificate provisioning — not your server block. Our domain and hosting service often starts with this split test when clients migrate DNS.

Need to decode a base64 cert snippet from a ticket? Use the base64 encoder and decoder tool before you paste PEM into files.

What production mistakes show up only under openssl s_client?

Some TLS bugs never appear in local dev. Staging may use a different cert or skip HTTPS entirely. These patterns repeat across client servers I maintain.

  • Incomplete chain after certbot renew — reload skipped, old bundle still served.
  • Wrong cert order in PEM — leaf must come first, then intermediates.
  • IPv6-only breakage — AAAA record points to a host with no vhost TLS config.
  • opcache or proxy cache — rare, but stale upstream config persists until reload.
  • Payment gateway IP allowlists — TLS OK from your laptop, blocked from server IP.

After every deploy on symlinked releases, I run one s_client check against the public hostname. It takes five seconds. It catches cert regressions before users hit checkout or document upload flows on Court Marriage In Nepal and similar lead-capture sites.

Pair TLS checks with application logs. The Laravel production debugging guide covers safe log access when Guzzle reports SSL errors but s_client looks fine — often a PHP curl CA bundle issue, not the server cert.

Chain Fix: Before vs AfterBeforeLeaf cert onlyVerify code: 21API clients rejectBrowsers may works_client shows 1 certFixAfterLeaf + intermediatein fullchain.pemVerify code: 0All clients trustWebhooks succeeds_client shows 2+ certs
Typical before-and-after result when you debug TLS issues with openssl s_client and fix an incomplete chain

Let's Encrypt documents fullchain.pem vs cert.pem differences at letsencrypt.org. Always point nginx ssl_certificate at the full chain file, not the leaf alone.

For ongoing monitoring, wrap s_client in a cron script that alerts on non-zero verify codes. Our support and maintenance service includes cert expiry checks on sites where downtime costs real leads — often Rs 50,000+ (~USD 370) in missed inquiries during a Dashain outage.

If you work with JSON API payloads after TLS is fixed, the JSON formatter tool helps validate responses separately from transport errors.

Key Takeaways

  • Always pass -servername when you debug TLS issues with openssl s_client on shared hosting or CDNs.
  • Trust Verify return code: 0 as your primary pass/fail signal before chasing application bugs.
  • Use -showcerts to export chains and confirm intermediates are actually sent by the server.
  • Force -tls1_2 and -tls1_3 separately to isolate protocol and cipher mismatches.
  • Re-run the exact same s_client command after every cert or nginx change to confirm the fix.
  • Combine s_client with DNS checks and application logs — TLS can be fine while PHP curl uses a stale CA file.

People Also Ask

What does "verify return code: 21" mean in openssl s_client?

Code 21 means OpenSSL cannot verify the certificate chain to a trusted root. The server usually sent only the leaf certificate. Append the missing intermediate to your server bundle, reload the web server, and re-test until the code drops to 0.

Do I need -servername when debugging my own domain?

Yes, whenever multiple HTTPS sites share one IP address. Without SNI, s_client receives the default vhost certificate. That produces misleading subject names and false "certificate mismatch" conclusions.

Can openssl s_client test TLS 1.3?

Modern OpenSSL builds negotiate TLS 1.3 by default on supported servers. Add -tls1_3 to force it, or -tls1_2 to exclude 1.3. The negotiated Protocol line in output confirms what actually connected.

How is openssl s_client different from curl for TLS debugging?

Both use OpenSSL under the hood on most Linux systems. s_client shows raw handshake detail and PEM chains with less HTTP noise. curl adds -v for verbose TLS info but mixes it with request headers. Use s_client for chain inspection; use curl to test full HTTP flows after TLS passes.

Fix TLS faster with the right diagnostic habit

When HTTPS breaks in production, reach for s_client before you restart services or reissue certs blindly. One command with -connect, -servername, and -showcerts tells you whether the fault is chain, hostname, protocol, or trust. That is how you debug TLS issues with openssl s_client efficiently in 2026.

If your team lacks time to harden cert renewals, monitoring, and nginx TLS config across multiple domains, see our testing and optimization service or browse the portfolio for examples of production sites kept online through cert and deploy discipline. For hands-on help with a stuck handshake, contact us with your s_client output pasted verbatim — the verify code and first alert line are usually enough to start.

Frequently Asked Questions

openssl s_client is OpenSSL's built-in TLS client. It opens a raw TLS session to any host and port and shows exactly what the server presents during the handshake. Browsers cache intermediates and hide details; load balancers may terminate TLS on a different node than you expect. s_client removes that noise. Use it when you need concrete answers: complete chain, negotiated TLS version and cipher, SNI correctness on shared IPs, CA trust, or mTLS rejection.

Start with openssl s_client -connect example.com:443 -servername example.com < /dev/null. The -connect flag sets host and port; -servername sends SNI so shared hosts return the correct certificate instead of a default vhost cert. Pipe empty input with < /dev/null so s_client exits after the handshake — otherwise it waits for HTTP input on stdin. Scroll past PEM blocks and read Protocol, Cipher, Verify return code, and subject= lines near the bottom.

Code 21 means OpenSSL cannot verify the certificate chain to a trusted root — usually a missing intermediate. The server sent only the leaf certificate. Append the missing intermediate to your server bundle, reload nginx or Apache, and re-test until the code drops to 0.

Verify return code 0 means OpenSSL trusts the chain with its default CA store. Treat it as your primary pass or fail signal before chasing application bugs. Non-zero codes map to real fixes: code 10 means the certificate expired; code 21 means unable to verify the local issuer, typically a missing intermediate. Hostname verification is separate — you can have code 0 and still serve the wrong cert if SNI is misconfigured.

Yes, whenever multiple HTTPS sites share one IP address. Without SNI, s_client receives the default vhost certificate, producing misleading subject names and false certificate mismatch conclusions. Compare responses with and without -servername on the same IP: different subjects mean SNI routing works; the same wrong subject on the named command means vhost or CDN host header mapping is broken.

-servername sends SNI on shared IPs and CDNs. -showcerts prints the full chain PEM for export and comparison. -tls1_2 and -tls1_3 force protocol versions to isolate legacy client failures. -CAfile sets a custom trust anchor for private CAs or corporate proxies. -cert and -key pass client certificates for mTLS. -verify_hostname checks CN and SAN match. -brief gives shorter output for CI smoke tests. -starttls smtp or imap upgrades plain connections on mail ports.

Yes. Modern OpenSSL builds negotiate TLS 1.3 by default on supported servers. Add -tls1_3 to force it, or -tls1_2 to exclude 1.3. The negotiated Protocol line in output confirms what actually connected.

Both use OpenSSL under the hood on most Linux systems. s_client shows raw handshake detail and PEM chains with less HTTP noise. curl adds -v for verbose TLS info but mixes it with request headers. Use s_client for chain inspection and handshake isolation; use curl to test full HTTP flows after TLS passes. If s_client looks fine but Guzzle or PHP curl still fails, the fault is often a stale PHP curl CA bundle, not the server certificate.

Run openssl s_client -connect example.com:443 -servername example.com -showcerts < /dev/null, export PEM blocks to chain.pem, then count certificates with openssl crl2pkcs7. A typical public site needs at least two: leaf plus intermediate. If you only see one, nginx or Apache is not sending the full chain. Fix ssl_certificate to point at the full chain bundle — Let's Encrypt fullchain.pem, not cert.pem alone — then reload the web server.

Handshake failures appear as alerts such as sslv3 alert handshake failure, protocol version, or certificate unknown. Run the baseline command with SNI and note the verify code. Retry with -tls1_3 only, then -tls1_2 only. If both fail, test port reachability with nc -zv host 443. Check server clock skew, compare ciphers, and re-test after each config change with the same command. Match nginx or Apache settings to what s_client actually negotiates, not what you think you enabled.

Compare default versus named vhost on the same IP by running s_client with and without -servername and grepping subject= lines. Then run openssl s_client -connect example.com:443 -servername example.com -verify_hostname example.com < /dev/null and read the last few lines. Hostname verification is separate from chain trust — verify code 0 does not guarantee the correct certificate for your hostname when SNI routing is broken on shared hosting or CDN edge nodes.

Run openssl s_client -connect api.example.com:8443 -servername api.example.com -cert client.crt -key client.key -CAfile ca-bundle.crt < /dev/null. If the server rejects your certificate, s_client shows Acceptable client certificate CA names. Compare that list with your client cert issuer. Mismatch here blocked a webhook on a production Laravel app until the client PEM was reissued from the provider's CA. Use -CAfile when the server chain is signed by a private or corporate root.

For SMTP use openssl s_client -starttls smtp -connect mail.example.com:587 -crlf < /dev/null. Add -crlf for correct SMTP line endings. For IMAP use -starttls imap on port 143. These expose the same chain and verify issues as HTTPS on port 443. Not every TLS endpoint listens on 443 — internal APIs, LDAP, and mail servers use other ports or STARTTLS upgrades that s_client handles with the same inspection workflow.

When Cloudflare or another CDN masks the origin, test the origin directly: openssl s_client -connect 198.51.100.22:443 -servername origin.example.com < /dev/null. If the origin certificate is fine but the edge fails, the problem is CDN SSL mode or edge certificate provisioning, not your server block. Always pass -servername even when connecting by IP so the correct vhost certificate is presented on shared infrastructure.

Incomplete chain after certbot renew when reload was skipped, wrong PEM order with leaf not first, IPv6 AAAA records pointing to hosts without vhost TLS config, and payment gateway failures where TLS works from your laptop but not the server IP. After every deploy on symlinked releases, run one s_client check against the public hostname — it takes five seconds and catches cert regressions before checkout or document upload flows break. Wrap s_client in a cron script that alerts on non-zero verify codes for ongoing monitoring.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: