TLS and certificates

Check a certificate with openssl

Fetch the certificate a server sends and read its names, issuer, and expiry date. Test the chain the way a client does, check a mail server over STARTTLS, and confirm that a key file belongs to a certificate.

Written againstOpenSSL 3.0 (Debian 12, Ubuntu 24.04)OpenSSL 3 (RHEL 9)
Reading time
5 min
Commands to copy
11
Last checked
24 Oct 2026

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

shell
$ 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
PartWhat it does
-servernameSends the host name in the handshake (SNI), so a server with many sites on one address picks the right certificate.
</dev/nullCloses the connection once the handshake is done, instead of waiting for input.
2>/dev/nullHides the connection details s_client prints to stderr.
-ext subjectAltNameThe 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:

shell
$ openssl s_client -connect 198.51.100.20:443 -servername www.example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -dates

Check 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.

shell
$ 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:

/usr/local/bin/check-cert.sh
#!/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.

shell
$ 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 codeUsual causeFix
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

shell
$ 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.

shell
$ 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:

shell
$ openssl s_client -connect mail.example.com:587 -starttls smtp -servername mail.example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -dates
PortServiceOption
25, 587SMTP relay and submission-starttls smtp
143IMAP-starttls imap
110POP3-starttls pop3
465, 993, 995SMTP, IMAP, and POP3 over TLS from the startNone: 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.