Verification and testing
openssl s_client: test TLS connections, SNI and STARTTLS
openssl s_client is the universal TLS test client: it shows the chain, protocol version, cipher suite, key exchange group and verification result for any TLS endpoint, including STARTTLS mail and database servers. Outputs come from OpenSSL 3.6.1; options are the same in 3.0 to 3.6 unless stated. For a browser-based check use the SSL Checker.
Get a one-screen summary of a connection
-brief prints only protocol, cipher, certificate, verification and key exchange group.
openssl s_client -connect www.example.com:443 -servername www.example.com -brief < /dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=www.example.com
Hash used: SHA256
Signature type: ecdsa_secp256r1_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE
Negotiated TLS1.3 group: X25519MLKEM768 means a hybrid post-quantum key exchange was used; OpenSSL 3.5 and later offer it by default. Against a server without it you see Peer Temp Key: X25519, 253 bits instead. Without -brief the full output adds the chain, session details and the session ticket.
Send the right hostname with -servername (SNI)
Since OpenSSL 1.1.1, s_client sends SNI automatically when -connect uses a hostname; with an IP address you must pass -servername.
openssl s_client -connect 203.0.113.10:443 -servername www.example.com -brief < /dev/null
openssl s_client -connect 203.0.113.10:443 -noservername -brief < /dev/null
Peer certificate: CN=www.example.com
Verification: OK
...
Peer certificate: CN=localhost
Verification error: self-signed certificate
The second command sends no SNI, so the server falls back to its default certificate. That is exactly what old clients without SNI see. On OpenSSL 1.1.0 and earlier, SNI was only sent with -servername, which is why many older tutorials always include it. See SNI.
Test a specific origin IP while keeping the hostname
Connect to the IP, send the real name in SNI and in the HTTP Host header; useful behind a CDN or load balancer.
printf 'HEAD / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n' \
| openssl s_client -quiet -connect 203.0.113.10:443 -servername www.example.com
depth=2 C=US, O=Example CA, CN=Example Root R1
verify return:1
depth=1 C=US, O=Example CA, CN=Example TLS Issuing CA
verify return:1
depth=0 CN=www.example.com
verify return:1
HTTP/1.1 200 OK
-quiet hides the session dump and implies -ign_eof, so s_client waits until the server closes; Connection: close makes that happen right after the response. This is how you test a new origin server before switching DNS.
Show the full chain the server sends
-showcerts prints every certificate the server sends, in order, as PEM.
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts < /dev/null
Certificate chain
0 s:CN=www.example.com
i:C=CN, O=Example Corp, CN=Example Intermediate CA
a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
v:NotBefore: Nov 9 18:16:48 2026 GMT; NotAfter: Dec 11 18:16:48 2027 GMT
-----BEGIN CERTIFICATE-----
MIICXzCCAgWgAwIBAgICEAAwCgYIKoZIzj0EAwIw...
-----END CERTIFICATE-----
1 s:C=CN, O=Example Corp, CN=Example Intermediate CA
i:C=CN, O=Example Corp, CN=Example Root CA
...
s: is the subject and i: the issuer; recent releases also print a: (key and signature algorithm) and v: (validity). Each i: should equal the next s:. A single entry means the intermediate is missing; see verifying chains.
Force TLS 1.2 or TLS 1.3
-tls1_2 and -tls1_3 pin the protocol version, so you can confirm both are enabled.
openssl s_client -connect www.example.com:443 -tls1_2 -brief < /dev/null
openssl s_client -connect www.example.com:443 -tls1_3 -brief < /dev/null
Protocol version: TLSv1.2
Ciphersuite: ECDHE-ECDSA-AES256-GCM-SHA384
...
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
TLS 1.2 cipher names use OpenSSL style (ECDHE-ECDSA-AES256-GCM-SHA384); TLS 1.3 uses IANA names. To test TLS 1.0 or 1.1 on OpenSSL 3.x you must also lower the security level, as shown in ciphers and protocols.
Check ALPN and HTTP/2 support
-alpn offers application protocols; the server's choice appears as ALPN protocol.
openssl s_client -connect www.example.com:443 -alpn h2,http/1.1 < /dev/null 2>/dev/null | grep -i alpn
ALPN protocol: h2
No ALPN negotiated means the server ignored ALPN, so browsers will fall back to HTTP/1.1. HTTP/3 runs over QUIC, not TCP; s_client gained a -quic option in OpenSSL 3.2, but most people test HTTP/3 with curl --http3.
Exit cleanly in scripts
Feed s_client an empty stdin so it exits after the handshake instead of waiting for input.
openssl s_client -connect www.example.com:443 < /dev/null
echo | openssl s_client -connect www.example.com:443
openssl s_client -connect www.example.com:443 -no-interactive # OpenSSL 3.2+
...
Verify return code: 0 (ok)
---
DONE
DONE means s_client closed the connection on its own. In a pipeline, redirect stderr (2>/dev/null) to drop the depth= lines. In interactive mode, a line starting with Q quits and R renegotiates (TLS 1.2 only), which can surprise you when you type an HTTP request; use -ign_eof or -quiet in that case.
Test SMTP with STARTTLS
-starttls smtp sends EHLO and STARTTLS first, then starts TLS on the same connection.
openssl s_client -connect smtp.example.com:587 -starttls smtp -brief < /dev/null
openssl s_client -connect smtp.example.com:25 -starttls smtp -name mx-test.example.com < /dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=smtp.example.com
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE
-name sets the hostname used in EHLO (also used for LMTP and XMPP). Port 465 (SMTPS) uses implicit TLS, so connect without -starttls. Many residential and cloud networks block outbound port 25; test 587 or test from the mail server itself. The STARTTLS Checker runs this from outside your network.
Test IMAP, POP3, FTP, databases and LDAP
The same -starttls option covers the other common protocols; implicit-TLS ports need no -starttls.
openssl s_client -connect mail.example.com:143 -starttls imap < /dev/null
openssl s_client -connect mail.example.com:110 -starttls pop3 < /dev/null
openssl s_client -connect ftp.example.com:21 -starttls ftp < /dev/null
openssl s_client -connect db.example.com:5432 -starttls postgres < /dev/null
openssl s_client -connect db.example.com:3306 -starttls mysql < /dev/null
openssl s_client -connect ldap.example.com:389 -starttls ldap < /dev/null
openssl s_client -connect mail.example.com:993 -brief < /dev/null
Didn't find STARTTLS in server response, trying anyway...
...:error:0A000126:SSL routines::unexpected eof while reading:...
The output above is what you get when the server does not advertise STARTTLS: s_client tries anyway and the server drops the connection. Supported keywords in OpenSSL 3.x are smtp, pop3, imap, ftp, xmpp, xmpp-server, irc, postgres, mysql, lmtp, nntp, sieve and ldap.
| Protocol | STARTTLS port | Implicit TLS port |
|---|---|---|
| SMTP submission | 587 | 465 |
| IMAP | 143 | 993 |
| POP3 | 110 | 995 |
| LDAP | 389 | 636 |
Check OCSP stapling
-status asks the server to staple an OCSP response and prints it if one arrives.
openssl s_client -connect www.example.com:443 -servername www.example.com -status < /dev/null 2>/dev/null \
| sed -n '/OCSP response/,/Next Update/p'
OCSP responses: number of responses: 1
======================================
OCSP Response Data:
OCSP Response Status: successful (0x0)
Response Type: Basic OCSP Response
...
Cert Status: good
This Update: Nov 9 04:50:45 2026 GMT
Next Update: Nov 16 04:50:45 2026 GMT
OCSP responses: no responses sent is normal for certificates without an OCSP URL, which includes all Let's Encrypt certificates since 2025. Older OpenSSL versions print OCSP response: no response sent. More in OCSP and CRL commands.
Present a client certificate (mutual TLS)
Pass -cert and -key (and -cert_chain for intermediates) when the server asks for a client certificate.
openssl s_client -connect api.example.com:443 -servername api.example.com \
-cert client.crt -key client.key -cert_chain intermediate.pem -brief < /dev/null
Acceptable client certificate CA names
C=CN, O=Example Corp, CN=Example Intermediate CA
C=CN, O=Example Corp, CN=Example Root CA
...
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE
The Acceptable client certificate CA names list (shown in full output) tells you which CAs the server accepts. Without a certificate, a TLS 1.3 server fails after the handshake with tlsv13 alert certificate required ... SSL alert number 116; a certificate from the wrong CA gives tlsv1 alert unknown ca ... SSL alert number 48. See mTLS.
Test session resumption
Save a session with -sess_out, reconnect with -sess_in, and look for Reused.
sleep 1 | openssl s_client -connect www.example.com:443 -sess_out sess.pem 2>/dev/null | grep -E '^(New|Reused)'
sleep 1 | openssl s_client -connect www.example.com:443 -sess_in sess.pem 2>/dev/null | grep -E '^(New|Reused)'
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Reused, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
TLS 1.3 tickets arrive after the handshake, so sleep 1 keeps the connection open long enough to receive them. -reconnect (five reconnections with the same session) works well for TLS 1.2 but usually reports New every time on TLS 1.3 for the same reason. See session resumption.
Connect through an HTTP proxy
-proxy host:port tunnels through an HTTP CONNECT proxy, as in many corporate networks.
openssl s_client -proxy proxy.example.com:3128 -connect www.example.com:443 \
-servername www.example.com -brief < /dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=www.example.com
Use -proxy_user and -proxy_pass for authenticated proxies. If the peer certificate comes from a corporate CA you do not recognise, the proxy is intercepting TLS; that explains unknown-authority errors inside such networks.
Debug the handshake message by message
-msg lists each handshake message, -trace decodes them, and -keylogfile writes secrets Wireshark can use.
openssl s_client -connect www.example.com:443 -msg < /dev/null 2>&1 | grep -E '^(>>>|<<<)'
openssl s_client -connect www.example.com:443 -keylogfile keys.log < /dev/null
>>> TLS 1.3, Handshake [length 0608], ClientHello
<<< TLS 1.3, Handshake [length 04ba], ServerHello
<<< TLS 1.3, Handshake [length 000a], EncryptedExtensions
<<< TLS 1.3, Handshake [length 0d7d], Certificate
<<< TLS 1.3, Handshake [length 004f], CertificateVerify
<<< TLS 1.3, Handshake [length 0034], Finished
The ClientHello here is 1,544 bytes (0x608) because it carries a 1,216-byte X25519MLKEM768 key share. -trace needs OpenSSL built with trace support (Homebrew's build has it) and shows every extension, including key_share and server_name. Treat keys.log like a private key and delete it afterwards. The message flow is explained in the TLS handshake.
FAQ
Why does s_client show a different certificate than my browser?
Usually SNI. Browsers always send the hostname; s_client sends it only when -connect uses a hostname or when you pass -servername. Connecting by IP without -servername makes many servers and CDNs return a default certificate. Add -servername www.example.com and compare again.
How do I stop s_client from hanging after it connects?
Give it no input: append < /dev/null or pipe echo into it. On OpenSSL 3.2 and later you can also use -no-interactive. Do not combine -quiet with an open terminal, because -quiet enables -ign_eof and waits for the server to close.
Does s_client check the hostname?
Not by default. It verifies the chain but not the name unless you add -verify_hostname www.example.com. Add -verify_return_error as well if you want the handshake to abort on any verification error, like a real client.