OpenSSL and TLS Cheat Sheet: Inspect, Verify, Convert and Debug Certificates
OpenSSL commands for SSL emergencies: test a live server with s_client, read certificates, verify chains, match keys to certs, create CSRs with SANs, convert formats and debug with curl and certbot.
When a browser says “Your connection is not private”, the answer is almost always in the certificate: it expired, it does not cover the host name, the chain is incomplete, or the server is presenting the wrong one. OpenSSL lets you see exactly what a server sends and what a certificate file contains. Commands here were written for OpenSSL 3.x and work on 1.1.1 unless noted. Check yours with openssl version.
Placeholders: HOST is the host name being tested (for example www.example.com). IP is a server address. CERT.pem is a certificate file, KEY.pem its private key, CHAIN.pem the intermediate certificates, CSR.pem a certificate signing request, ROOT.pem a trusted root certificate. FILE.pfx is a PKCS#12 bundle. CERT_NAME is a certbot certificate name. WEBROOT is the folder the site is served from. Never send a private key to anyone or paste it into a website.
s_client: test a live server
| Command | What it does | Example | Notes |
|---|---|---|---|
openssl s_client -connect HOST:443 -servername HOST |
Opens a TLS connection and prints the handshake, certificate and verification result | openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null |
</dev/null makes it exit instead of waiting for input. Read Verify return code: 0 (ok) near the end. |
-servername HOST |
Sends SNI, telling the server which site you want | -connect IP:443 -servername HOST |
Essential when connecting by IP or on shared hosting; without SNI you may get the server’s default certificate. |
-showcerts |
Prints every certificate the server sends, not just the first | openssl s_client -connect HOST:443 -servername HOST -showcerts </dev/null |
Use it to spot a missing intermediate (only one certificate in the chain). |
-brief |
Short summary: protocol, cipher, peer certificate, verification | openssl s_client -connect HOST:443 -servername HOST -brief </dev/null |
Good first look. |
-tls1_2 / -tls1_3 |
Forces one protocol version | openssl s_client -connect HOST:443 -servername HOST -tls1_3 </dev/null |
A handshake failure means that version is not offered. TLS 1.0 and 1.1 are deprecated and may be disabled in your OpenSSL build too. |
-status |
Requests a stapled OCSP response | openssl s_client -connect HOST:443 -servername HOST -status </dev/null |
Look for “OCSP Response Status: successful”. Let’s Encrypt shut down OCSP in 2025, so its certificates show “no response sent”. That is expected. |
-alpn h2 |
Offers HTTP/2 via ALPN | openssl s_client -connect HOST:443 -servername HOST -alpn h2 </dev/null |
Check for “ALPN protocol: h2”. |
-verify_return_error |
Aborts the handshake if verification fails | openssl s_client -connect HOST:443 -servername HOST -verify_return_error </dev/null |
Makes failures obvious in scripts. |
-CAfile ROOT.pem |
Verifies against a specific trust store | -CAfile /etc/ssl/certs/ca-certificates.crt |
Path differs by OS. |
-starttls smtp |
Upgrades a plain SMTP session to TLS | openssl s_client -connect mail.example.com:587 -starttls smtp -servername mail.example.com |
Also imap, pop3, ftp. Implicit TLS ports (465, 993, 995) need no -starttls. |
| Pipe into x509 | Extracts the server certificate for inspection | openssl s_client -connect HOST:443 -servername HOST </dev/null 2>/dev/null | openssl x509 -noout -dates -subject -issuer |
Quickest live expiry check. |
Common s_client verify return codes
| Code | Message | Usual cause |
|---|---|---|
| 0 | ok | Chain verified. Host name matching is a separate check (see -verify_hostname). |
| 2 | unable to get issuer certificate | Intermediate missing from what the server sends. |
| 10 | certificate has expired | Renew it, then reload the web server. |
| 18 | self-signed certificate | Default or placeholder certificate is being served. |
| 19 | self-signed certificate in certificate chain | A root that your trust store does not contain, or an interception proxy. |
| 20 | unable to get local issuer certificate | Missing intermediate, or your trust store is out of date. |
| 21 | unable to verify the first certificate | Server sends only the leaf certificate, no intermediate. |
| 62 | hostname mismatch | Shown with -verify_hostname HOST when the certificate does not cover that name. |
x509: read a certificate
| Command | What it shows | Example | Notes |
|---|---|---|---|
openssl x509 -in CERT.pem -noout -text |
Everything, decoded | openssl x509 -in CERT.pem -noout -text | less |
-noout stops it re-printing the encoded certificate. |
-dates |
notBefore and notAfter |
openssl x509 -in CERT.pem -noout -dates |
Times are in GMT. |
-enddate |
Expiry only | openssl x509 -in CERT.pem -noout -enddate |
Easy to parse. |
-checkend SECONDS |
Exit code 1 if the certificate expires within that many seconds | openssl x509 -in CERT.pem -noout -checkend 2592000 |
2592000 seconds is 30 days. Good for monitoring scripts. |
-subject |
Who the certificate is for | openssl x509 -in CERT.pem -noout -subject |
Browsers ignore the CN for matching; the SAN list is what counts. |
-issuer |
Who signed it | openssl x509 -in CERT.pem -noout -issuer |
Issuer equal to subject means self-signed. |
-ext subjectAltName |
The host names the certificate covers | openssl x509 -in CERT.pem -noout -ext subjectAltName |
OpenSSL 1.1.1 or later. Several extensions at once: -ext subjectAltName,keyUsage,extendedKeyUsage. |
-serial |
Serial number | openssl x509 -in CERT.pem -noout -serial |
Needed for revocation and CA support requests. |
-fingerprint -sha256 |
SHA-256 fingerprint | openssl x509 -in CERT.pem -noout -fingerprint -sha256 |
Compare two files, or what a server sends, without reading every field. |
-pubkey |
The public key | openssl x509 -in CERT.pem -noout -pubkey |
Used for key matching below. |
-ocsp_uri |
OCSP responder URL, if any | openssl x509 -in CERT.pem -noout -ocsp_uri |
Empty output means the certificate has no OCSP URL. |
openssl req -in CSR.pem -noout -text |
Contents of a CSR | openssl req -in CSR.pem -noout -text -verify |
Confirm the SAN list before submitting it to a CA. |
openssl crl2pkcs7 -nocrl -certfile CHAIN.pem | openssl pkcs7 -print_certs -noout |
Subject and issuer of every certificate in a bundle file | Same | x509 alone only reads the first certificate in a file. |
Verify a chain and match a key
| Task | Command | Notes |
|---|---|---|
| Verify a certificate against the system trust store plus intermediates | openssl verify -untrusted CHAIN.pem CERT.pem |
Prints CERT.pem: OK on success. -untrusted supplies intermediates without trusting them as roots. |
| Verify against a specific root | openssl verify -CAfile ROOT.pem -untrusted CHAIN.pem CERT.pem |
Useful for private CAs. |
| Check a key matches a certificate (any key type) | openssl x509 -in CERT.pem -noout -pubkey | openssl sha256, then openssl pkey -in KEY.pem -pubout | openssl sha256 |
The two hashes must be identical. Works for RSA and EC keys. |
| Check a CSR matches the key | openssl req -in CSR.pem -noout -pubkey | openssl sha256 |
Compare with the key hash above. |
| RSA only: compare modulus | openssl x509 -in CERT.pem -noout -modulus | openssl sha256, then openssl rsa -in KEY.pem -noout -modulus | openssl sha256 |
The older method. Does not work for EC keys. |
| Check a private key is intact | openssl pkey -in KEY.pem -noout -check |
Asks for the passphrase if the key is encrypted. |
Generate a key and CSR with SANs
| Task | Command | Notes |
|---|---|---|
| RSA key and CSR in one step | openssl req -new -newkey rsa:2048 -nodes -keyout example.com.key -out example.com.csr -subj "/CN=example.com" -addext "subjectAltName=DNS:example.com,DNS:www.example.com" |
-addext needs OpenSSL 1.1.1 or later. -nodes leaves the key unencrypted so the web server can read it; OpenSSL 3 also accepts -noenc. |
| EC (P-256) key | openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out example.com.key |
Smaller and faster than RSA, widely supported. |
| CSR from an existing key | openssl req -new -key example.com.key -out example.com.csr -subj "/CN=example.com" -addext "subjectAltName=DNS:example.com,DNS:www.example.com" |
Reuse the key when a CA or host requires it. |
| Self-signed certificate for testing | openssl req -x509 -newkey rsa:2048 -nodes -keyout test.key -out test.crt -days 30 -subj "/CN=test.local" -addext "subjectAltName=DNS:test.local" |
Browsers will warn. Never use on a public site. |
| Protect the key file | chmod 600 example.com.key |
Readable only by its owner. |
Add IP addresses to a SAN with IP:192.0.2.10. Wildcards are written DNS:*.example.com and cover one label only: www.example.com but not example.com or a.b.example.com.
Convert formats
| From | To | Command | Notes |
|---|---|---|---|
| PEM | DER | openssl x509 -in CERT.pem -outform der -out CERT.der |
PEM is Base64 text between BEGIN and END lines. DER is the same data in binary (often .der or .cer). |
| DER | PEM | openssl x509 -in CERT.der -inform der -out CERT.pem |
Use when a CA hands you a binary file. |
| PEM key, cert and chain | PFX / P12 | openssl pkcs12 -export -out FILE.pfx -inkey KEY.pem -in CERT.pem -certfile CHAIN.pem |
For Windows and IIS. Prompts for an export password. OpenSSL 3 encrypts with AES by default; add -legacy if an older Windows system refuses to import the file. |
| PFX | PEM certificate | openssl pkcs12 -in FILE.pfx -clcerts -nokeys -out CERT.pem |
The leaf certificate only. |
| PFX | PEM chain | openssl pkcs12 -in FILE.pfx -cacerts -nokeys -out CHAIN.pem |
Intermediates. |
| PFX | PEM key | openssl pkcs12 -in FILE.pfx -nocerts -nodes -out KEY.pem |
Writes an unencrypted key. Protect it. If OpenSSL 3 reports an unsupported algorithm on an old file, add -legacy. |
| PFX | Inspect | openssl pkcs12 -in FILE.pfx -info -noout |
Shows the encryption used. |
| Encrypted key | Unencrypted key | openssl pkey -in KEY.pem -out KEY-plain.pem |
Web servers usually need the key without a passphrase. |
| Cert and chain | Full chain | cat CERT.pem CHAIN.pem > fullchain.pem |
Leaf first, then intermediates. Do not include the root. |
curl for TLS debugging
| Command | What it does | Notes |
|---|---|---|
curl -v https://HOST/ -o /dev/null |
Shows the TLS handshake, certificate subject, SAN match and expiry | Look for “SSL certificate verify ok” and “subjectAltName: host matched”. |
curl -vI https://HOST/ |
Same, with response headers only | Quick combined TLS and HTTP check. |
curl -v --resolve HOST:443:IP https://HOST/ |
Connects to a chosen IP while sending the right host name and SNI | Test a new server, or the origin behind a CDN, before DNS changes. |
curl -v --connect-to HOST:443:OTHER_HOST:443 https://HOST/ |
Like --resolve but takes a host name |
Handy for load balancers. |
curl --tlsv1.2 --tls-max 1.2 https://HOST/ |
Limits the protocol version | Check that older clients can still connect. |
curl -s -o /dev/null -w '%{ssl_verify_result}\n' https://HOST/ |
Prints the verification result number | 0 means success. |
curl -k https://HOST/ |
Skips certificate verification | Danger: it hides exactly the problem you are diagnosing and makes interception invisible. Use only to reach a page while you fix the certificate, never in scripts or cron jobs. |
Let’s Encrypt and certbot
| Command | What it does | Notes |
|---|---|---|
certbot certificates |
Lists certificates certbot manages, with domains, expiry and file paths | Run with sudo. First command in any certbot problem. |
certbot renew --dry-run |
Tests renewal against the staging environment | Does not touch real certificates or count against production rate limits. |
certbot renew |
Renews certificates close to expiry | Normally run by a systemd timer or cron job. Check with systemctl list-timers | grep certbot. |
certbot renew --cert-name CERT_NAME --force-renewal |
Renews one certificate now | Use sparingly because of rate limits. |
certbot certonly --webroot -w WEBROOT -d example.com -d www.example.com |
Gets a certificate using HTTP-01 through files in the web root | Does not edit web server config. |
certbot --nginx -d example.com or certbot --apache -d example.com |
Gets a certificate and configures the web server | Needs the matching certbot plugin. |
certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d example.com |
Wildcard certificate using a DNS TXT record you add by hand | Manual certificates do not renew automatically. Use a DNS provider plugin for automation. |
certbot delete --cert-name CERT_NAME |
Removes a certificate and its renewal config | Make sure no server config still points at its files. |
certbot revoke --cert-name CERT_NAME |
Revokes a certificate | For compromised keys. |
| Files | /etc/letsencrypt/live/CERT_NAME/fullchain.pem and privkey.pem |
Point the server at fullchain.pem, not cert.pem, or the intermediate is missing. Log: /var/log/letsencrypt/letsencrypt.log. |
ACME challenge types
| Challenge | How it proves control | Port or record | Wildcards? | Typical failure |
|---|---|---|---|---|
| HTTP-01 | Serves a token at http://DOMAIN/.well-known/acme-challenge/TOKEN |
Port 80 must be reachable | No | Port 80 blocked, a redirect or security rule intercepting /.well-known/, DNS (including AAAA) pointing at another server. |
| DNS-01 | Publishes a TXT record at _acme-challenge.DOMAIN |
DNS only | Yes | Record added at the wrong DNS provider, not yet visible on all authoritative servers, or a CAA record blocking the CA. |
| TLS-ALPN-01 | Presents a special certificate during a TLS handshake using the acme-tls/1 ALPN protocol |
Port 443 | No | A proxy or CDN in front that terminates TLS itself. |
Let’s Encrypt certificates have been valid for 90 days, and Let’s Encrypt has announced a staged move to shorter lifetimes (45 days by 2028). It also stopped sending expiry reminder emails in June 2025. Automated renewal plus your own expiry monitoring is now essential.
Common recipes
“Your connection is not private”: find out why
openssl s_client -connect HOST:443 -servername HOST -brief </dev/null
openssl s_client -connect HOST:443 -servername HOST </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
openssl s_client -connect HOST:443 -servername HOST -showcerts </dev/null | grep -E ' s:| i:'
Expired dates, a SAN list without HOST, a self-signed issuer or a single s: line (no intermediate) each point to a different fix.
Renewed but the browser still shows the old certificate
openssl x509 -in /etc/letsencrypt/live/CERT_NAME/fullchain.pem -noout -enddate
openssl s_client -connect HOST:443 -servername HOST </dev/null 2>/dev/null | openssl x509 -noout -enddate
sudo nginx -t && sudo systemctl reload nginx # or: sudo apachectl configtest && sudo systemctl reload apache2
If the file is new but the server sends the old one, the web server was not reloaded, the config points at a different file, or a CDN or load balancer holds its own copy.
Installing a certificate from a commercial CA
openssl x509 -in CERT.pem -noout -pubkey | openssl sha256
openssl pkey -in KEY.pem -pubout | openssl sha256
openssl verify -untrusted CHAIN.pem CERT.pem
cat CERT.pem CHAIN.pem > fullchain.pem
Expiry check for several sites
for h in example.com www.example.com shop.example.com; do
printf '%s ' "$h"
openssl s_client -connect "$h:443" -servername "$h" </dev/null 2>/dev/null | openssl x509 -noout -enddate
done
Test the origin behind a CDN
openssl s_client -connect ORIGIN_IP:443 -servername HOST -brief </dev/null
curl -vI --resolve HOST:443:ORIGIN_IP https://HOST/
ORIGIN_IP is the real server address. This is the check for Cloudflare 525 and 526 errors.
Check TLS on a mail server
openssl s_client -connect mail.example.com:587 -starttls smtp -servername mail.example.com -brief </dev/null
openssl s_client -connect mail.example.com:993 -servername mail.example.com -brief </dev/null
Gotchas
- Forgetting
-servername. Recent OpenSSL sends SNI from the-connecthost name, but not when you connect by IP. Always pass it to be sure you see the right certificate. Verify return code: 0is not a host name check. s_client verifies the chain, not that the certificate covers the name. Read the SAN list or add-verify_hostname HOST.- Missing intermediates hide on your own machine. Desktop browsers may cache or fetch intermediates, so a site looks fine to you and fails on phones, older devices and API clients. Test with s_client.
- cert.pem vs fullchain.pem. With certbot, pointing the server at
cert.pemleaves out the intermediate. - Order matters in bundles. Leaf first, then the intermediate that signed it, and so on up. Do not add the root.
- Mixed content is not a certificate problem. A padlock warning on a valid certificate usually means the page loads images or scripts over
http://. - Clock skew. A device with the wrong date sees valid certificates as expired or not yet valid.
- Stale AAAA records break HTTP-01. Let’s Encrypt prefers IPv6 when an AAAA record exists. If it points at an old server, validation fails.
- Old PFX files and OpenSSL 3. Files encrypted with legacy algorithms need
-legacyto open, and new files may need-legacyto import on old Windows versions. - Never use
curl -kor disable verification in code to “fix” an error. It removes the protection TLS exists to provide.
Did this fix it?
Still stuck? Start again from what you can see in the Emergency Room.
Need the full fix?
Cheat sheets are quick reference. The field guides walk you through each emergency step by step.
Open the Emergency Room