OpenSSL is the canonical tool for inspecting TLS certificates: what hostnames a cert covers, when it expires, whether the chain trusts to a public root, whether the key on disk matches the certificate on disk, and whether the server is actually serving the cert you think it is. Almost every TLS troubleshooting workflow eventually boils down to two or three openssl invocations — knowing the right ones turns a 30-minute hunt through dashboards into a 30-second answer.
This guide is a commands-and-output reference for the common openssl check certificate workflows: inspecting a local file, inspecting a live endpoint, verifying chain and hostname, decoding fingerprints, converting formats, and reading the most important fields. Every section gives you the exact invocation, the expected output, and what the fields actually mean.
The examples assume OpenSSL 1.1+ (default on Ubuntu 18.04+, RHEL 8+, macOS via Homebrew). On macOS, the system
opensslis LibreSSL — install GNU OpenSSL viabrew install openssland use/opt/homebrew/opt/openssl@3/bin/opensslfor compatibility with the syntax below.
Inspect a certificate file on disk
The single most useful command — dump everything human-readable from a PEM file:
openssl x509 -in example.com.crt -noout -text
The output is long. The fields you usually care about:
Certificate:
Data:
Version: 3 (0x2)
Serial Number: ...
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = Let's Encrypt, CN = R3
Validity
Not Before: Feb 10 08:14:32 2026 GMT
Not After : May 11 08:14:31 2026 GMT
Subject: CN = example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:example.com, DNS:www.example.com
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
...
Quick variants when you only want one piece of information:
# Subject and issuer only
openssl x509 -in example.com.crt -noout -subject -issuer
# Validity dates
openssl x509 -in example.com.crt -noout -dates
# notBefore=Feb 10 08:14:32 2026 GMT
# notAfter=May 11 08:14:31 2026 GMT
# Just the SANs
openssl x509 -in example.com.crt -noout -ext subjectAltName
# X509v3 Subject Alternative Name:
# DNS:example.com, DNS:www.example.com
# Days until expiry — exit code 0 if more than N days, non-zero if less
openssl x509 -in example.com.crt -noout -checkend $((30*86400))
# Certificate will not expire ← 30 days or more remaining
# Serial number
openssl x509 -in example.com.crt -noout -serial
# serial=03ABC...
# Fingerprint (for matching against a known good)
openssl x509 -in example.com.crt -noout -fingerprint -sha256
-checkend is particularly useful in scripts — wrap it in a cron job to fail loudly when expiry is within your renewal window.
Inspect the live certificate served by a server
What's on disk and what's actually being served are not always the same thing. Always check the live endpoint:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
The -servername flag sends an SNI extension — without it, you'll get whatever certificate the server returns when no SNI is sent (usually a default/fallback cert that may not match the hostname you expected). Always pass -servername for any modern HTTPS endpoint.
To see the full chain the server is sending (leaf + intermediates):
echo | openssl s_client -servername example.com -connect example.com:443 -showcerts 2>/dev/null
Count the certificates in the response — there should be at least two (leaf + at least one intermediate). One is a misconfigured server; clients with strict trust stores will reject it.
echo | openssl s_client -servername example.com -connect example.com:443 -showcerts 2>/dev/null \
| grep -c 'BEGIN CERTIFICATE'
# 3 ← leaf + 2 intermediates: good
# 1 ← leaf only: missing chain (will break some clients)
For non-HTTPS protocols that use STARTTLS (SMTP, IMAP, FTP), pass -starttls:
openssl s_client -starttls smtp -servername mail.example.com -connect mail.example.com:587
openssl s_client -starttls imap -servername mail.example.com -connect mail.example.com:143
openssl s_client -starttls ftp -servername ftp.example.com -connect ftp.example.com:21
Verify the certificate matches the private key
A renewal mistake that wastes more time than it should: deploying a leaf certificate that doesn't match the key on disk. Nginx will refuse to start (or load fine but fail every TLS handshake). Confirm the match before reloading:
# RSA
diff <(openssl rsa -modulus -noout -in example.com.key | openssl md5) \
<(openssl x509 -modulus -noout -in example.com.crt | openssl md5)
# ECDSA
diff <(openssl pkey -in example.com.key -pubout -outform DER | openssl md5) \
<(openssl x509 -in example.com.crt -pubkey -noout | \
openssl pkey -pubin -outform DER | openssl md5)
If the two MD5 outputs match, the key and cert pair up. If they don't, you have mismatched files — do not deploy.
You can also confirm the CSR matches the same key (useful before submitting to a CA):
openssl req -modulus -noout -in example.com.csr | openssl md5
openssl rsa -modulus -noout -in example.com.key | openssl md5
Verify the chain trusts to a root
The browser tells you "secure" or "not secure", but openssl verify tells you why:
openssl verify -CAfile chain.pem example.com.crt
# example.com.crt: OK
Where chain.pem is the intermediate certificate(s) concatenated. If you don't have the intermediate file separately, build it from the live endpoint:
# Capture leaf + chain from a live server into a single file
echo | openssl s_client -servername example.com -connect example.com:443 -showcerts 2>/dev/null \
| awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/' > served.pem
# Verify it against the system's root store
openssl verify -CAfile served.pem served.pem
Common verify failures and what they mean:
| Error | Cause |
|---|---|
unable to get local issuer certificate |
Intermediate is missing from the chain you supplied |
self signed certificate in certificate chain |
A non-public CA's root is in the chain (typical for internal PKI — fine if intentional) |
certificate has expired |
notAfter is in the past |
certificate is not yet valid |
notBefore is in the future (clock skew on the verifying host?) |
unable to verify the first certificate |
The leaf is not signed by anything in your -CAfile (or by the system trust store) |
Hostname mismatch |
The cert's CN/SANs do not include the hostname you connected to (only with -verify_hostname) |
To verify against a hostname explicitly (mirrors what a browser does):
openssl verify -CAfile chain.pem -verify_hostname example.com example.com.crt
Check SAN coverage for a hostname
The cert needs to list every hostname you serve via that endpoint, either as the Common Name or in the SAN extension. Modern clients (every browser since 2017) ignore the CN entirely — they only look at SANs.
openssl x509 -in example.com.crt -noout -ext subjectAltName \
| tr ',' '\n' | grep -oE 'DNS:[^,[:space:]]+'
# DNS:example.com
# DNS:www.example.com
Or against the live endpoint:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -ext subjectAltName \
| tr ',' '\n' | grep -oE 'DNS:[^,[:space:]]+'
If a hostname is missing, you have two options: re-issue with the SAN added, or run a separate cert and rely on SNI to pick the right one.
Check OCSP and OCSP stapling
OCSP (Online Certificate Status Protocol) lets a client check whether a cert has been revoked since issuance. Two angles to check:
Is the server stapling OCSP responses?
echo | openssl s_client -servername example.com -connect example.com:443 -status 2>/dev/null \
| grep -A 17 'OCSP response:'
If you see OCSP Response Status: successful and Cert Status: good, the server is stapling correctly — the client can validate revocation without reaching out to the CA. If you see OCSP response: no response sent, stapling is off (or broken). For Nginx, enable it with:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
Manually query the OCSP responder
# Get the OCSP URL from the cert
OCSP_URL=$(openssl x509 -in example.com.crt -noout -ocsp_uri)
# Build a request and send it
openssl ocsp -issuer chain.pem -cert example.com.crt \
-url "$OCSP_URL" -no_nonce \
-header "Host=$(echo "$OCSP_URL" | awk -F/ '{print $3}')"
Expected output:
Response verify OK
example.com.crt: good
This Update: ...
Next Update: ...
good means not revoked. revoked is what you don't want to see.
Convert between certificate formats
The four formats you'll encounter:
| Format | Extension | Contents | Used by |
|---|---|---|---|
| PEM | .pem, .crt, .cer, .key |
Base64 ASCII, -----BEGIN...----- |
Nginx, Apache, HAProxy, almost everything Unix |
| DER | .der, .cer |
Binary | Windows, Java, some CDN consoles |
| PKCS#12 / PFX | .p12, .pfx |
Binary, key + cert + chain in one password-protected file | IIS, macOS, Java |
| PKCS#7 | .p7b, .p7c |
Binary, cert(s) only — no private key | Windows, Java |
PEM ↔ DER
# PEM → DER
openssl x509 -in example.com.crt -outform DER -out example.com.der
# DER → PEM
openssl x509 -in example.com.der -inform DER -out example.com.pem
Build a PFX (PEM + key → PKCS#12)
openssl pkcs12 -export \
-in fullchain.pem \
-inkey example.com.key \
-out example.com.pfx \
-name "example.com"
# Enter Export Password: ...
Extract from PFX
# Cert only (no key)
openssl pkcs12 -in example.com.pfx -clcerts -nokeys -out example.com.crt
# Key only (decrypted)
openssl pkcs12 -in example.com.pfx -nocerts -nodes -out example.com.key
# Full chain (CA certs from inside the PFX)
openssl pkcs12 -in example.com.pfx -cacerts -nokeys -chain -out chain.pem
-nodes writes the private key unencrypted; this is what most Unix services want. Treat the resulting file as a secret.
PKCS#7 → PEM
openssl pkcs7 -in chain.p7b -inform DER -print_certs -out chain.pem
Generate a new CSR (briefly)
Renewing or rotating? Generate a new key + CSR in one shot:
openssl req -new -newkey rsa:2048 -nodes \
-keyout example.com.key \
-out example.com.csr \
-subj "/C=US/ST=California/L=San Francisco/O=Example Inc/CN=example.com" \
-addext "subjectAltName = DNS:example.com,DNS:www.example.com"
Verify:
openssl req -in example.com.csr -noout -text | grep -E 'Subject:|DNS:'
For the full renewal walkthrough — DV/OV validation, chain assembly, install on Nginx/Apache/IIS — see How to renew an SSL certificate (step-by-step).
SNI: when "the wrong cert" is being served
A common surprise: you curl https://example.com/ and get a certificate for default.example.net. Cause: the server is hosting multiple TLS sites and your client (or your openssl invocation) didn't send SNI, so the server returned its default cert.
Confirm by running the s_client command with and without -servername:
# Without SNI — server returns default cert
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -noout -subject
# With SNI — server returns the cert for example.com
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -subject
If the two outputs differ, your server uses SNI-based selection and any client that can't send SNI will see the wrong cert. Modern clients (every browser, every common HTTP library since ~2014) send SNI by default, but custom embedded clients sometimes do not.
Read a CSR
When the CA support team pushes back on your CSR, dump it before re-arguing:
openssl req -in example.com.csr -noout -text
Key fields:
Subject: C = US, ST = California, L = SF, O = Example Inc, CN = example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:example.com, DNS:www.example.com
Signature Algorithm: sha256WithRSAEncryption
Confirm: subject correct, SANs correct, key size ≥ 2048 (or ECDSA), signature algorithm SHA-256+ (not SHA-1).
Decode a single-line cert (e.g. from a JSON field)
Cloud APIs and Kubernetes secrets often store certs as base64-encoded single-line strings:
# From a Kubernetes secret
kubectl get secret tls-cert -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -text
# From an arbitrary base64 blob
echo "<paste-blob-here>" | base64 -d | openssl x509 -noout -text
If the file is already PEM (with -----BEGIN CERTIFICATE----- headers visible), drop the base64 -d step and pipe directly.
Operational tips
- Always pass
-servernametos_client. If you don't, you get the server's default cert, which is rarely the one you wanted to inspect. Forgetting this is the #1 source of "openssl says one thing, my browser says another" confusion. -noouteverywhere by default. Without it,openssl x509andopenssl reqprint the original PEM at the bottom of every command — fine occasionally, irritating in pipelines.echo |befores_clientmatters.s_clientblocks waiting for stdin otherwise. Theecho |(or< /dev/null) closes stdin so the connection completes andopensslexits cleanly.- Build a one-liner you can paste anywhere. A single command that prints subject, dates, and SANs covers 80% of triage:
alias certinfo='openssl x509 -noout -subject -issuer -dates -ext subjectAltName -in' certinfo example.com.crt - For one-shot live checks, this script-friendly invocation is the right shape:
check_cert() { local host="$1" port="${2:-443}" echo | openssl s_client -servername "$host" -connect "$host:$port" 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates -ext subjectAltName } check_cert example.com - When verifying chains, pin down the system trust store you're checking against.
openssl verifyuses-CApath /etc/ssl/certsby default on Debian/Ubuntu; on RHEL/CentOS it's/etc/pki/tls/certs. If your host has a stripped or non-standard trust store, you'll see "untrusted" errors that don't reflect what a browser would see. - OpenSSL 3.x deprecations.
openssl s_clientwarns about a number of SHA-1 and small-key algorithms by default in OpenSSL 3.x. Treat the warnings as signal — they're flagging certs or chains that would be rejected by real clients soon.
Catch silent cert problems before they break traffic
The reason to know openssl in this much depth is to debug fast when something breaks. The reason to not rely on running these commands by hand is that nobody runs them on a schedule, and silent cert problems — auto-renewals that succeeded but didn't reload, intermediates that quietly disappeared, hostname mismatches after a SAN change — only surface when traffic breaks.
Continuous SSL monitoring runs the same checks the commands above run, but every few minutes from multiple regions. The right setup alerts on:
- Days to expiry (multi-tier: 30 / 14 / 7 days).
- Chain validity — the served chain trusts to a public root, in the right order.
- Hostname coverage — the cert's SANs still include every hostname you serve.
- TLS protocol and cipher — TLS 1.0/1.1 deprecation, weak ciphers, expired CA roots in the chain.
- OCSP stapling status — silent staple failure leads to slow client handshakes and intermittent errors.
Xitoring's SSL certificate monitoring does all of these and pages on the first failure, with the same dashboard surfacing HTTPS uptime so you see whether the endpoint is up and whether the TLS layer is healthy. Pair it with the SSL renewal guide and the SSL monitoring overview for the full operational picture.
Summary
The openssl check certificate workflows worth committing to muscle memory:
openssl x509 -in cert.pem -noout -text— dump everything from a file.echo | openssl s_client -servername host -connect host:443 -showcerts— inspect a live endpoint, full chain, with SNI.openssl rsa -modulus -noout -in key.pem | openssl md5vs the same on the cert — confirm the key and cert match before deploying.openssl verify -CAfile chain.pem cert.pem— confirm chain trusts to a root; failures map to specific config bugs.openssl x509 -noout -checkend N— script-friendly expiry check (exit-code based).openssl x509 -noout -ext subjectAltName— confirm SANs cover every hostname you serve.openssl s_client -status— confirm OCSP stapling is healthy.openssl pkcs12 -export | -in— convert between PEM and PFX for IIS / Java / macOS keychains.
These eight commands cover the vast majority of TLS troubleshooting. Once they're in a shell alias or a small cert-tools script, "is this cert healthy?" becomes a five-second answer instead of a five-minute one. Wire the same checks into continuous monitoring so the silent failure modes (renewal didn't reload, intermediate disappeared, a SAN got dropped) are caught before they reach the support inbox.