Ahosting Logo
Knowledge Base

How to Check a Certificate from the Command Line

What a command-line check answers that a browser does notOne command, several answersWhich hostnames it coversso a mismatch is visible insteadof impliedWhether the chain is completethe intermediate the browser maybe cachingThe exact expiry datenot "expires soon"Which certificate is being servedwhen several exist on one serverWhat the server negotiatesprotocol and cipher actuallyusedFrom anywhereno browser cache involved at allBrowser warnings are deliberately vague so as not to teach people to click through them. This is where the detail is.

Browser certificate warnings are written to be understood by everyone, which means they say very little. One command shows what the server is actually sending.

The command

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

Four lines of output: which name the certificate is for, who issued it, and the valid-from and valid-to dates.

-servername is not optional. One address hosts many sites, and the server decides which certificate to send based on the name you asked for. Leave it out and you get the server's default certificate, then spend an hour investigating a mismatch you created yourself.

Every hostname it covers

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
 | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

This is the list that decides whether a given address is covered. Two things surprise people regularly.

example.com and www.example.com are separate names. A certificate with one and not the other produces a warning on exactly half your traffic.

A wildcard covers one level only. *.example.com matches shop.example.com and does not match a.b.example.com or the bare domain. Wildcard and multi-domain certificates goes into the details.

Expiry, on its own

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

Cheap enough to run from a monitoring script. A certificate that renews automatically still fails occasionally, and it fails silently. The first sign is usually a customer, so a check that emails you at fourteen days is worth the five minutes.

Managing AutoSSL in WHM deals with why automatic renewals fail, which is nearly always a validation request that could not be served.

The chain

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

Near the top, the verification result. Verify return code: 0 (ok) is what you want.

unable to get local issuer certificate means the intermediate certificates are not being sent. This produces the specific and confusing symptom where the site works in a desktop browser, which has the intermediate cached from another site, and fails on a phone, in an app, or from a script.

Anyone reporting a certificate problem you cannot reproduce is usually seeing this. Understanding the certificate chain deals with fixing it.

Check a file before installing it

openssl x509 -in certificate.crt -noout -subject -dates
openssl x509 -in certificate.crt -noout -modulus | openssl md5
openssl rsa -in private.key -noout -modulus | openssl md5

The last two must produce the same hash. If they differ, the certificate and key do not belong together, and installing them produces a broken site instead of an error at install time.

This is worth doing every time, because certificate and key files accumulate with similar names and mixing up a renewal is easy. Generating a CSR and managing private keys deals with keeping them straight.

Protocols and ciphers

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

Substitute -tls1_3 or older versions to see what the server accepts. Useful when a payment provider or an API requires a minimum version and reports only that the connection failed.

Understanding TLS versions and cipher suites goes over what to enable.

Mail certificates

openssl s_client -connect mail.example.com:993 -servername mail.example.com </dev/null

Mail uses certificates too, on different ports, and they are a frequent source of "your mail client cannot verify this server" messages that nobody connects to the website's certificate at all.

Port 993 for IMAP, 465 for submission. For port 587, add -starttls smtp, because the connection begins unencrypted and upgrades.

Test a certificate before the domain points at it

During a migration you want to confirm the new server's certificate works, before moving any traffic to it.

curl -sI --resolve example.com:443:203.0.113.99 https://example.com/ | head -3
openssl s_client -connect 203.0.113.99:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -dates

Both ask the new address for the certificate belonging to the name, without changing any DNS. The first also confirms the site responds; the second shows the certificate in detail.

This is the check that turns a cutover from a hope into a verification, and it costs nothing. Planning a DNS cutover explains where it fits in the sequence.

Confirm a renewal actually took effect

A renewal that ran successfully and was never loaded by the running service is a common and confusing state: the file on disk is new and the server is still presenting the old certificate.

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

The serial number changes with every issuance, so comparing it before and after a renewal is unambiguous in a way that the dates are not.

If the dates have not moved, the service needs reloading. If they have moved on one port and not another. The panel on 2083 renewed and mail on 993 did not. That service specifically needs restarting, which is a genuinely common state on a cPanel server.

Which name the server actually chose

openssl s_client -connect example.com:443 </dev/null 2>/dev/null | openssl x509 -noout -subject

Deliberately omitting -servername shows the server's default certificate, what a client that does not send the hostname would receive.

If that comes back as an unrelated domain, and some clients are reporting certificate errors while browsers are fine, this is the explanation: those clients are not sending the name and are being handed somebody else's certificate.

Fixing certificate errors in scripts and API clients goes into the client side of the same fault.

Checking several hosts in one pass

for h in example.com shop.example.com mail.example.com; do
 d=$(echo | openssl s_client -connect "$h":443 -servername "$h" 2>/dev/null \
 | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
 printf '%-28s %s\n' "$h" "${d:-NO CERTIFICATE}"
done

Run across every hostname an account serves, this finds the one that quietly stopped renewing, which is nearly always a subdomain nobody thinks about instead of the main site.

SSL for subdomains, addon domains and mail goes over why coverage is per name rather than per account.

Check the certificate the mail server is presenting

Web and mail are served by different processes and can present different certificates, so checking the website proves nothing about mail.

echo | openssl s_client -connect mail.example.com:993 -servername mail.example.com 2>/dev/null | openssl x509 -noout -subject -dates
echo | openssl s_client -connect mail.example.com:465 -servername mail.example.com 2>/dev/null | openssl x509 -noout -subject
echo | openssl s_client -connect mail.example.com:587 -starttls smtp 2>/dev/null | openssl x509 -noout -subject

The submission port needs the extra option because encryption is negotiated after the connection opens rather than before, which is why a plain check against it returns nothing and looks like a closed port.

Different dates on different ports mean one service was restarted after a renewal and another was not. That is the entire fault, and it is invisible from any interface that reports the certificate as installed.

Confirm the private key belongs to the certificate

Installing a certificate against the wrong key produces a refusal that gives no useful reason, and one comparison settles it before you try.

openssl x509 -noout -modulus -in cert.pem | openssl md5
openssl rsa  -noout -modulus -in key.pem  | openssl md5
openssl req  -noout -modulus -in csr.pem  | openssl md5

All three values must match. A mismatch means the certificate was issued from a different request, which happens easily when a request is generated twice while waiting for the issuer.

For modern keys the same check uses the public key rather than the modulus, and the principle is identical. Comparing before installing turns a confusing failure into a thirty second check. Generating a CSR and managing private keys goes into keeping them paired.

See it the way a client will

A certificate that verifies on your machine may not verify on somebody else's, and the difference is which authorities each trusts.

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | grep -E 'Verify return code|depth'
echo | openssl s_client -connect example.com:443 -CApath /etc/ssl/certs 2>/dev/null | grep 'Verify return'

A verification code of zero means the chain resolved completely using the trust store on this machine. Anything else names the reason, and the depth line says at which link it failed.

Testing from a second machine, ideally one with an older trust store, is what catches an intermediate certificate that your own system happens to have cached. That is the classic case where a site works for you and warns for a customer. Understanding the certificate chain deals with what to send.