openssl s_client opens a TLS connection the way a browser or a mail client does and prints the certificate the server sends. Piped into openssl x509, it shows the names the certificate covers, who issued it, and the dates it’s valid between. Both are part of the openssl package, which Debian, Ubuntu, and RHEL install by default.
Read the certificate a server sends
$ openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltNamesubject=CN = www.example.comissuer=C = US, O = Let's Encrypt, CN = R11notBefore=Sep 30 08:12:44 2026 GMTnotAfter=Dec 29 08:12:43 2026 GMTX509v3 Subject Alternative Name: DNS:example.com, DNS:www.example.com
| Part | What it does |
|---|---|
-servername | Sends the host name in the handshake (SNI), so a server with many sites on one address picks the right certificate. |
</dev/null | Closes the connection once the handshake is done, instead of waiting for input. |
2>/dev/null | Hides the connection details s_client prints to stderr. |
-ext subjectAltName | The names the certificate is valid for. Browsers ignore the CN and check these. |
Since OpenSSL 1.1.1, s_client sends the name from -connect by itself. -servername matters when you connect to an address instead, for example to test a new server before DNS points at it:
$ openssl s_client -connect 198.51.100.20:443 -servername www.example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -datesCheck the expiry in a script
-checkend takes a number of seconds and sets the exit code: 0 if the certificate is still valid at that point, 1 if it has expired by then. 2592000 seconds is 30 days.
$ openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null | openssl x509 -noout -checkend 2592000Certificate will not expire$ echo $?0
A script that a monitor or a timer can run for each host:
#!/bin/sh# Exits 1 when the certificate of host $1 expires within 30 days, or can't be read at all.host="$1"if ! openssl s_client -connect "$host:443" -servername "$host" </dev/null 2>/dev/null | openssl x509 -noout -checkend 2592000 >/dev/null; then echo "$host: certificate expires within 30 days, or the server didn't send one" >&2 exit 1fi
A host that doesn’t answer fails the check too, which is what a monitor should report. To run it every morning, see Run a job on a schedule with systemd timers.
Check the chain and the host name
Without the pipe, s_client also verifies the certificate against the system’s trusted roots and prints the chain the server sent. -verify_hostname adds the check browsers make, that the name is in the certificate.
$ openssl s_client -connect www.example.com:443 -servername www.example.com -verify_hostname www.example.com </dev/null# trimmed to the chain and the resultCertificate chain 0 s:CN = www.example.com i:C = US, O = Let's Encrypt, CN = R11 1 s:C = US, O = Let's Encrypt, CN = R11 i:C = US, O = Internet Security Research Group, CN = ISRG Root X1---Verify return code: 0 (ok)
Entry 0 is the server’s own certificate, entry 1 the intermediate that signed it. Add -showcerts to get every certificate in the chain as PEM. Anything other than 0 (ok) names the problem:
| Verify return code | Usual cause | Fix |
|---|---|---|
20 (unable to get local issuer certificate) | The server sends its certificate without the intermediate. | Configure the full chain, for nginx the fullchain.pem file. |
21 (unable to verify the first certificate) | The same, seen from the first certificate. | As above. |
10 (certificate has expired) | notAfter has passed. | Renew, then reload the service so it reads the new file. |
62 (hostname mismatch) | The name isn’t in the Subject Alternative Name list. | Issue a certificate that includes it. |
18 (self-signed certificate) | No CA signed it. | Expected for internal tools; otherwise replace it. |
A browser that already has the intermediate cached still connects when the server leaves it out, so code 20 can break curl or an API client while every browser you try works. s_client doesn’t cache intermediates, so it shows what a client without them sees.
Read a certificate file
$ openssl x509 -in cert.pem -noout -subject -enddate$ openssl x509 -in cert.pem -noout -fingerprint -sha256$ openssl x509 -in cert.pem -noout -text
-text prints everything: the key type and size, every extension, and the signature. If the file starts with something other than -----BEGIN CERTIFICATE-----, it’s in another format. Convert between PEM, DER, and PFX shows how to tell.
Check that a key belongs to a certificate
A certificate holds the public half of a key pair. If the public key derived from the key file matches the one in the certificate, they belong together. This works for RSA and EC keys alike.
$ openssl x509 -in cert.pem -noout -pubkey | openssl sha256SHA2-256(stdin)= 6b1f0e9c2a7d4e53b8f1c09a3d6e2f7b4c5a8d9e0f1b2c3d4e5f60718293a4b5$ openssl pkey -in key.pem -pubout | openssl sha256SHA2-256(stdin)= 6b1f0e9c2a7d4e53b8f1c09a3d6e2f7b4c5a8d9e0f1b2c3d4e5f60718293a4b5
Different hashes mean the wrong key, and a web server given the pair refuses to start. nginx reports it as key values mismatch.
Check a mail server
On the submission and relay ports, mail servers start in plain text and switch to TLS with STARTTLS. Tell s_client which protocol to speak first:
$ openssl s_client -connect mail.example.com:587 -starttls smtp -servername mail.example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -dates| Port | Service | Option |
|---|---|---|
| 25, 587 | SMTP relay and submission | -starttls smtp |
| 143 | IMAP | -starttls imap |
| 110 | POP3 | -starttls pop3 |
| 465, 993, 995 | SMTP, IMAP, and POP3 over TLS from the start | None: connect as for HTTPS |
Last checked on 24 October 2026 against OpenSSL 3.0 (Debian 12, Ubuntu 24.04) and OpenSSL 3 (RHEL 9). If a step doesn’t match what you see, contact us with the command and what it printed.