All articles
·7 min read

Check an SSL certificate with OpenSSL: the complete command reference

To check an SSL certificate with OpenSSL you usually reach for two subcommands: openssl s_client to talk to a live server and openssl x509 to read a certificate file. This page is the deep reference for both, organized by task so you can find the exact incantation you need: inspect a live host, read a file, list SANs, walk the chain, verify trust, match a key to its certificate, decode a CSR, probe TLS versions, handle STARTTLS, check revocation, and convert between formats. Each command is copy-pasteable, with a short note on why it works. If you only care about the expiry date, the focused five ways to check an SSL certificate's expiration date is a quicker read.

Check a live server's certificate

The canonical way to verify a certificate on a running server is s_client, which performs a real TLS handshake and prints the certificate the server sends.

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -text

Three details matter every time you run this:

  • -servername example.com sets SNI (Server Name Indication). Most hosts serve many certificates from one IP, and the server picks which one to present based on the SNI value in the handshake. Omit it and you may get back a default certificate that isn't the one you wanted to check.
  • echo | feeds an empty line so s_client closes the connection instead of hanging, waiting for you to type an HTTP request. </dev/null does the same thing.
  • 2>/dev/null hides the handshake noise (the verbose chain dump and connection details) so only the parsed certificate reaches x509.

To see just the negotiation summary — protocol, cipher, verify result — without piping into x509, drop the pipe and read s_client's own output:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | grep -E 'subject=|issuer=|Protocol|Cipher|Verify'

Read a certificate file fully

When you have a .pem, .crt, or .cer file on disk, no network is needed. x509 -text decodes the entire certificate into human-readable form.

openssl x509 -in cert.pem -noout -text

-noout suppresses the re-encoded PEM blob (you want the parsed text, not the raw certificate echoed back). The full dump is verbose, so most of the time you want targeted fields instead. Each of these flags can be combined in one call:

openssl x509 -in cert.pem -noout -dates       # notBefore / notAfter
openssl x509 -in cert.pem -noout -subject     # who the cert is for
openssl x509 -in cert.pem -noout -issuer      # which CA signed it
openssl x509 -in cert.pem -noout -serial      # serial number (unique per CA)
openssl x509 -in cert.pem -noout -fingerprint -sha256   # SHA-256 fingerprint

A typical combined call and its output:

openssl x509 -in cert.pem -noout -subject -issuer -dates -serial
subject=CN=example.com
issuer=C=US, O=Let's Encrypt, CN=R11
notBefore=Jan  1 00:00:00 2026 GMT
notAfter=Apr  1 23:59:59 2026 GMT
serial=03A1B2C3D4E5F6...

The fingerprint is useful for comparing two certificates without diffing their entire contents: if the SHA-256 fingerprints match, the certificates are byte-for-byte identical. That's how you confirm the file on disk is the same one a server is serving.

List Subject Alternative Names

Modern clients ignore the legacy Common Name entirely and validate the hostname against the Subject Alternative Name (SAN) list. If the host you connect to isn't in the SAN list, the connection is rejected with an ERR_TLS_CERT_ALTNAME_INVALID-style error even when the certificate is otherwise valid. Pull the SANs from a file:

openssl x509 -in cert.pem -noout -ext subjectAltName
X509v3 Subject Alternative Name:
    DNS:example.com, DNS:www.example.com, DNS:api.example.com

Or read them straight off a live host:

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

A wildcard entry shows as DNS:*.example.com. Wildcards match exactly one label, so *.example.com covers api.example.com but not example.com itself or a.b.example.com — see wildcard SSL certificates explained for the matching rules.

Show the full served chain

A server must present the leaf certificate and the intermediate(s) that link it to a trusted root. Browsers sometimes hide a missing intermediate by fetching it via the AIA extension, but many API clients and mobile runtimes do not — producing intermittent failures that are painful to reproduce. -showcerts prints every certificate the server actually sends:

echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null

Each certificate appears as a numbered block (0 is the leaf, then each intermediate). Read the s: (subject) and i: (issuer) lines: the issuer of each certificate should be the subject of the next one up. If the chain stops at the leaf with no intermediate, you have an incomplete chain. To inspect a single certificate from the dump, paste its -----BEGIN/END CERTIFICATE----- block into a file and run x509 -text on it. For the full mental model, read the SSL certificate chain explained.

Verify a certificate against its chain and root

Printing the chain tells you what the server sends; openssl verify tells you whether that chain actually validates up to a trusted root.

openssl verify -CAfile chain.pem cert.pem
cert.pem: OK

-CAfile points at a PEM bundle containing the intermediate(s) and root needed to build the path. If you only have the intermediates separately from the trusted root, use -untrusted intermediates.pem together with -CAfile root.pem. The error you'll hit most often is:

error 20 at 0 depth lookup: unable to get local issuer certificate

That means OpenSSL couldn't find the certificate that signed your leaf — almost always a missing intermediate in the bundle, not a problem with the leaf itself. The same root cause produces the unable to verify the first certificate / leaf-signature error in Node and other clients. Add the correct intermediate to your -CAfile (or fix the chain your server serves) and re-run.

Match a private key, certificate, and CSR

When you're installing or renewing a certificate, the private key, the certificate, and the CSR must all belong together. They do if and only if they share the same public key, which you can confirm by comparing the modulus of each. Hashing the modulus keeps the output short and easy to eyeball:

openssl x509 -noout -modulus -in cert.pem | openssl md5     # certificate
openssl rsa  -noout -modulus -in key.pem  | openssl md5     # private key
openssl req  -noout -modulus -in req.csr  | openssl md5     # CSR

If all three hashes match, the key, certificate, and CSR are a set. A mismatch between key and certificate is the classic cause of a server that loads its config but refuses TLS connections — you installed a certificate against the wrong key. For elliptic-curve keys, swap openssl rsa for openssl ec (or use openssl pkey -noout -modulus for either type on newer builds). A handshake that fails on key mismatch is one of the cases covered in SSL handshake failed: causes and fixes.

Decode a CSR

Before you submit a certificate signing request to a CA, confirm it contains exactly the subject and SANs you intended. -verify additionally checks that the CSR's self-signature is valid, proving it was signed by the matching private key.

openssl req -in req.csr -noout -text -verify
verify OK
Certificate Request:
    Data:
        Subject: CN=example.com
        ...
        Requested Extensions:
            X509v3 Subject Alternative Name:
                DNS:example.com, DNS:www.example.com

A common surprise: many CAs ignore the CN and SANs in the CSR and use the values from your dashboard or ACME order instead. Decoding the CSR is still worth it to catch a typo before you've paid for or issued the wrong certificate.

Check the negotiated TLS version and ciphers

A connection can succeed and still be misconfigured. Force a specific protocol version to confirm what the server accepts and rejects:

openssl s_client -connect example.com:443 -tls1_3 </dev/null   # should succeed
openssl s_client -connect example.com:443 -tls1_2 </dev/null   # should succeed
openssl s_client -connect example.com:443 -tls1_1 </dev/null   # should FAIL on a good server

A non-zero exit on the TLS 1.3 probe means the server doesn't offer the current protocol — a configuration regression worth flagging. A successful TLS 1.0/1.1 handshake means the server still accepts deprecated protocols you should have disabled. (If the terms keep tripping you up, SSL vs TLS: the difference clears it up.) To test whether a specific cipher is offered, name it explicitly:

openssl s_client -connect example.com:443 -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 </dev/null

A handshake failure here means the server doesn't support that cipher under TLS 1.2 — which is the answer you wanted. For TLS 1.3 cipher suites, use -ciphersuites instead of -cipher.

Connect to STARTTLS endpoints

Mail and database protocols often start as plaintext and upgrade to TLS mid-session with a STARTTLS command, so a plain s_client -connect to those ports won't complete a handshake. The -starttls flag speaks the protocol's upgrade handshake for you:

# SMTP submission
openssl s_client -starttls smtp -connect mail.example.com:587 -servername mail.example.com </dev/null

# IMAP
openssl s_client -starttls imap -connect mail.example.com:143 -servername mail.example.com </dev/null

# POP3
openssl s_client -starttls pop3 -connect mail.example.com:110 -servername mail.example.com </dev/null

s_client also supports -starttls for ftp, xmpp, ldap, postgres, and mysql, among others. Pipe any of these into openssl x509 -noout -dates to read the certificate exactly as you would for HTTPS. Note that implicit-TLS ports (465 for SMTPS, 993 for IMAPS, 995 for POP3S) take a plain -connect with no -starttls, because TLS starts immediately.

Check OCSP revocation and stapling

A certificate can be revoked before it expires — for a leaked key or a mis-issuance — and a valid expiry date says nothing about revocation. The quickest revocation signal is the server's stapled OCSP response, which -status requests during the handshake:

echo | openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null \
  | grep -A 17 "OCSP Response Data"

A healthy staple shows OCSP Response Status: successful and a Cert Status: good with This Update / Next Update timestamps. OCSP response: no response sent means the server isn't stapling, so clients must do their own live OCSP lookup — extra latency and a soft-fail dependency. To query a CA's OCSP responder directly instead of relying on the staple:

openssl ocsp -issuer chain.pem -cert cert.pem \
  -url http://ocsp.example-ca.com -no_nonce -text

You need the issuing intermediate (-issuer chain.pem) because OCSP requests are keyed to the certificate that signed your leaf.

Convert between certificate formats

Different platforms expect different encodings. The two you'll meet most are PEM (Base64 text, -----BEGIN CERTIFICATE-----) and DER (raw binary, common on Windows and Java). Convert with -inform/-outform:

# PEM -> DER
openssl x509 -in cert.pem -outform der -out cert.der

# DER -> PEM
openssl x509 -inform der -in cert.der -out cert.pem

A PKCS#12 (.pfx/.p12) file bundles a certificate, its chain, and the private key into one password-protected blob — the format Windows/IIS and many load balancers want. Build one from PEM files:

openssl pkcs12 -export -out bundle.pfx \
  -inkey key.pem -in cert.pem -certfile chain.pem

And go the other way — extract everything from a .pfx back to PEM:

openssl pkcs12 -in bundle.pfx -nodes -out all.pem

-nodes ("no DES") leaves the extracted private key unencrypted, which is convenient for a server that loads the key on boot but means the output file must be tightly permissioned. The IIS-specific import/export path is covered in the IIS guide.

Quick troubleshooting

  • s_client hangs after the handshake. It's waiting for application data. Prefix the command with echo | or append </dev/null so it sends end-of-input and closes.
  • You got the wrong certificate. The host served its default cert because no SNI was sent. Add -servername <host> so the server presents the right one.
  • unable to load certificate. The file is almost certainly DER-encoded, not PEM. Add -inform der to the x509 command (or convert it as shown above).
  • unable to get local issuer certificate from verify. Your -CAfile bundle is missing the intermediate that signed the leaf — the same root cause behind the leaf-signature trust error. Add the intermediate and re-verify.
  • A handful of clients fail while browsers are fine. Usually an incomplete served chain. Confirm with s_client -showcerts and compare against the chain explainer. For a quick outside-in check without any local tooling, paste a hostname into the SSL check tool.

Monitor it automatically

These commands are perfect for a one-off investigation, but certificates expire and rotate on a schedule, and running openssl by hand is not a monitoring strategy. SSLNudge runs the same checks — expiry, chain completeness, hostname/SAN match, and TLS configuration — against your domains every day and alerts you with real lead time, so a missed renewal becomes a notice instead of an outage. Sign in to start monitoring and let the daily check watch them for you.

Stop tracking expiry dates by hand

SSLNudge checks your certificates daily and alerts you before they expire.

Start free