OpenSSL Cheatsheet

s_client and s_server

Use this OpenSSL reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Connect to a TLS service

s_client is a raw TLS client. It prints the handshake, the chain, and then hands you the socket.

openssl s_client -connect example.com:443 -servername example.com

Redirect stdin from /dev/null so it exits instead of waiting for you to type:

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

-servername is not implied by -connect. Without SNI a shared host returns its default certificate, which is why you sometimes see a completely unrelated domain in the output.

Reading the output

LineMeans
Certificate chainWhat the server sent, 0 s: is the leaf
Verify return code: 0 (ok)Chain validated against your trust store
Verify return code: 21unable to verify the first certificate, chain incomplete
Verify return code: 20unable to get local issuer certificate
Verify return code: 10Certificate expired
Protocol : TLSv1.3Negotiated version
Cipher : TLS_AES_256_GCM_SHA384Negotiated suite
Server Temp Key: X25519, 253 bitsKey exchange group
Peer signing digest / Peer signature typeCert key type in use
No client certificate CA names sentServer is not asking for mTLS
Early data was not sentNo TLS 1.3 0-RTT
# just the negotiated parameters
openssl s_client -connect example.com:443 -servername example.com \
  </dev/null 2>/dev/null | grep -E 'Protocol|Cipher|Verify'

# every certificate the server sent, PEM included
openssl s_client -connect example.com:443 -servername example.com \
  -showcerts </dev/null 2>/dev/null > chain.txt

Extract the leaf and inspect it in one pipe:

openssl s_client -connect example.com:443 -servername example.com \
  </dev/null 2>/dev/null |
  openssl x509 -noout -subject -issuer -dates -ext subjectAltName

Pin a version, group, or cipher

This is how you prove a server still speaks something it should not, or does not speak something it should.

openssl s_client -connect example.com:443 -servername example.com -tls1_3
openssl s_client -connect example.com:443 -servername example.com -tls1_2
openssl s_client -connect example.com:443 -servername example.com -no_tls1_3

# TLS 1.2 and below take -cipher, TLS 1.3 takes -ciphersuites
openssl s_client -connect example.com:443 -tls1_2 -cipher 'ECDHE-RSA-AES128-GCM-SHA256'
openssl s_client -connect example.com:443 -tls1_3 \
  -ciphersuites 'TLS_CHACHA20_POLY1305_SHA256'

openssl s_client -connect example.com:443 -groups X25519
openssl s_client -connect example.com:443 -alpn h2 -servername example.com
openssl s_client -connect example.com:443 -status -servername example.com

A rejected version comes back as a handshake alert, not a clean message:

40C7A1B901000000:error:0A000410:SSL routines:ssl3_read_bytes:
  ssl/tls alert handshake failure:...:SSL alert number 40

OpenSSL 3.x builds usually disable TLS 1.0 and 1.1 outright. -tls1 then fails locally even when the server supports it. Add -cipher 'DEFAULT@SECLEVEL=0' or test with a distro build that still allows it.

STARTTLS

Mail and database protocols start in plaintext and upgrade. -starttls runs the right upgrade command for you.

ValueUsual port
smtp25, 587
lmtp24
pop3110
imap143
ftp21
xmpp, xmpp-server5222, 5269
ldap389
mysql3306
postgres5432
nntp, sieve, irc, telnet119, 4190, 194, 23
openssl s_client -starttls smtp -connect mail.example.com:587 \
  -servername mail.example.com
openssl s_client -starttls imap -connect mail.example.com:143
openssl s_client -starttls postgres -connect db.example.com:5432

Ports 465 (SMTPS), 993 (IMAPS), and 995 (POP3S) are TLS from the first byte, so they take a plain -connect with no -starttls.

Throwaway server with s_server

s_server gives you a TLS endpoint to point a client at, with no nginx and no config file.

# defaults to *:4433
openssl s_server -cert cert.pem -key key.pem -accept 4433

# answer GET / with a handshake status page, handy in a browser
openssl s_server -cert cert.pem -key key.pem -accept 4433 -www

# serve files out of the current directory
openssl s_server -cert cert.pem -key key.pem -accept 4433 -WWW

# one connection then exit, quiet, TLS 1.3 only
openssl s_server -cert cert.pem -key key.pem -accept 4433 \
  -naccept 1 -quiet -tls1_3

Require a client certificate, which is the fastest way to test mTLS:

openssl s_server -cert server.crt -key server.key \
  -CAfile ca.crt -Verify 1 -accept 4433

openssl s_client -connect localhost:4433 \
  -cert client.crt -key client.key -CAfile ca.crt

-Verify (capital V) fails the handshake without a valid client cert. -verify (lowercase) asks for one but continues anyway.

Gotchas

- s_client hangs after the handshake. It is holding the socket open for you to type. </dev/null or -no_ign_eof ends it. - -servername and -connect are independent. Setting only -connect IP:443 sends no SNI and often returns the wrong certificate. - HTTP over s_client needs CRLF. Typing a request by hand usually fails because your terminal sends bare LF. Use printf 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n' | openssl s_client .... - Verify return code: 21 on a working browser means the server omitted an intermediate and the browser is caching it. Fix the server's chain file. - Fetch every certificate individually from -showcerts output with awk '/BEGIN CERT/,/END CERT/', since a plain x509 pipe reads only the first block. - s_server reads server.pem by default and will not tell you clearly that it did. Always pass -cert and -key.