
September 12, 2026
12 min read
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.
openssl s_client -connect host:443 -servername host, then inspect the certificate chain, verify return code, protocol version, and cipher. Add -showcerts for chain export and -tls1_2 or -tls1_3 to isolate version problems.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.
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:
- Protocol and cipher — confirms negotiated TLS version and algorithm.
- Verify return code — zero means OpenSSL trusts the chain with its default store.
- Certificate chain depth — count how many certs the server sent.
- 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.
| Flag | Purpose | When to use it |
|---|---|---|
-servername HOST | Sends SNI extension | Shared IP, CDN, or multi-site nginx/apache |
-showcerts | Prints full chain PEM | Export intermediates, compare with browser chain |
-tls1_2 / -tls1_3 | Forces protocol version | Isolate legacy client failures |
-CAfile path | Custom trust anchor | Private CA, corporate proxy, mTLS roots |
-cert / -key | Client certificate | Debug mutual TLS on APIs or webhooks |
-verify_hostname HOST | Checks CN/SAN match | Confirm cert matches expected hostname |
-brief | Shorter output | Quick smoke tests in CI or cron jobs |
-starttls smtp|ftp|... | Upgrade plain connection | Mail 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.
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
- Run the baseline command with SNI and note the verify code.
- Retry with
-tls1_3only, then-tls1_2only. - If both fail, test port reachability with
nc -zv host 443. - Compare cipher with
-cipher 'ECDHE'vs default. - Check server clock — skew breaks validity windows silently.
- 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.
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.
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
-servernamewhen you debug TLS issues with openssl s_client on shared hosting or CDNs. - Trust
Verify return code: 0as your primary pass/fail signal before chasing application bugs. - Use
-showcertsto export chains and confirm intermediates are actually sent by the server. - Force
-tls1_2and-tls1_3separately 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
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.

