- Introduction
- 1. Telling formats apart — start by confirming what you received
- 2. Reading a certificate — confirming what this file is
- 3. Checking whether the key and the certificate are a pair
- 4. Creating a CSR — always include the SAN
- 5. Chain verification — catching a missing intermediate
- 6. Diagnosing against a live server — s_client
- 7. Common errors and the order of diagnosis
- Quiz: check your understanding
- Closing
- References
- Further reading
Introduction
Certificate problems are always urgent. You missed a renewal and the service stopped, or you installed a new certificate and only some clients fail, or the file format is wrong and it will not load. What you need at that moment is not TLS handshake theory but the commands that tell you what this file is and what is missing from it right now.
This blog already has the complete guide to SSL/TLS certificates. That article covers the issuance and operations flow: Let's Encrypt issuance, Nginx configuration, automatic renewal. This article aims at something different. It is a reference for the openssl commands themselves. With a certificate file in front of you, it organizes by purpose what you should check and which command tells you which fact. The subject is diagnosis, not issuance.
Every command in this article is centered on reading and diagnosing. Reading a certificate wrong causes no damage, but commands that handle private keys are different. Print a key file to the screen or save it with the wrong permissions and it has leaked at that instant. So this article takes as its baseline the approach of comparing digests without printing key contents wherever possible.
The baseline is OpenSSL 3.x. OpenSSL 1.1.1 and 3.x differ in some options and default behavior. Also, the default environment on macOS and some distributions uses LibreSSL, where options may differ. Check the version before you run anything.
openssl version -a
1. Telling formats apart — start by confirming what you received
A large share of certificate incidents begins with a format mix-up. The extension is only a hint; it guarantees nothing about the contents.
| Format | Contents | Common extensions |
|---|---|---|
| PEM | Base64 text with header lines | .pem .crt .cer .key |
| DER | Binary encoding | .der .cer |
| PKCS#12 | Certificate and private key in one file | .p12 .pfx |
| PKCS#7 | Certificate bundle (no key) | .p7b .p7c |
| PKCS#8 | Standard private key format | .key .pem |
First look at whether the file is text or binary.
file server.crt
head -1 server.crt
If it is PEM, the first line carries a marker starting with -----BEGIN. What that marker says is the contents.
BEGIN CERTIFICATE: a certificateBEGIN CERTIFICATE REQUEST: a CSRBEGIN PRIVATE KEY: a PKCS#8 private keyBEGIN RSA PRIVATE KEY: a traditional RSA private keyBEGIN ENCRYPTED PRIVATE KEY: an encrypted private key
Format conversion works like this.
openssl x509 -in server.der -inform DER -out server.pem -outform PEM
openssl x509 -in server.pem -outform DER -out server.der
openssl pkcs12 -in bundle.pfx -nodes -out bundle.pem
openssl pkcs12 -export -inkey server.key -in server.crt -certfile chain.crt -out bundle.pfx
openssl pkcs7 -print_certs -in chain.p7b -out chain.pem
-inform and -outform specify the input and output encodings. In a PKCS#12 conversion, -nodes means the private key is written out unencrypted, so you must restrict the permissions on the resulting file immediately.
chmod 600 bundle.pem
Here is one example of how a format mix-up actually turns into an incident. You receive a .pfx file exported from a Windows server, point Nginx straight at it, and the server will not start. Nginx wants a PEM certificate and a PEM key as separate files. Conversely, hand a Java application a PEM pair and it refuses them because they are not in keystore format. When you receive a file, do not trust the extension; check the contents first — that saves you 30 minutes.
You can also pull out only the part you need from a PKCS#12 bundle. Situations where you have to separate out just the certificate, or just the key, come up often.
openssl pkcs12 -in bundle.pfx -clcerts -nokeys -out server.crt
openssl pkcs12 -in bundle.pfx -cacerts -nokeys -out chain.crt
openssl pkcs12 -in bundle.pfx -nocerts -nodes -out server.key
-clcerts outputs only the client (end-entity) certificate, -cacerts only the CA certificates, and -nocerts only the key with the certificates excluded.
2. Reading a certificate — confirming what this file is
Start with the commands you will use most.
openssl x509 -in server.crt -noout -text
openssl x509 -in server.crt -noout -subject -issuer -dates
openssl x509 -in server.crt -noout -serial -fingerprint -sha256
openssl x509 -in server.crt -noout -ext subjectAltName
Per the documentation, the options mean the following.
-nooutsuppresses the encoded original output and shows only what you asked for.-textprints the whole certificate in human-readable form.-subjectand-issuershow the subject and the issuer, and-datesshows the validity start and end times. You can also see them individually with-startdateand-enddate.-fingerprintcomputes the digest of the DER encoding.-extprints the X.509 extension you name.
In practice the thing you have to check most often is the SAN (Subject Alternative Name). Modern browsers and libraries ignore the CN and look only at the SAN. Even if the domain is in the CN, it fails when it is not in the SAN.
openssl x509 -in server.crt -noout -ext subjectAltName
Expiry can be judged down to the second. According to the documentation, -checkend checks whether the certificate expires within the given number of seconds.
openssl x509 -in server.crt -noout -checkend 0
openssl x509 -in server.crt -noout -checkend 2592000
echo "exit=$?"
An exit code of 0 means it does not expire within that window. Put in 30 days (2592000 seconds) and you have a renewal alerting script as-is.
There are fewer fields in the certificate body worth actually checking than you might think. Validity period and SAN are 80 percent of it, and the rest you only look at when something is wrong. If you had to pick what to watch for in the -text output, it would be these.
- Signature Algorithm: the signing algorithm. Modern clients reject old SHA-1 signatures.
- Public Key Algorithm and key length: short keys such as RSA 1024-bit are rejected.
- Basic Constraints: whether it is a CA. If a server certificate is marked as a CA, it was mis-issued.
- Key Usage and Extended Key Usage: server authentication has to be included. Install a certificate issued for client authentication on a server and it fails.
- Authority Information Access: it carries the issuer certificate and OCSP responder addresses.
It is worth checking these fields once when you adopt a new CA or change the issuance request form.
#!/usr/bin/env bash
set -uo pipefail
for CRT in /etc/pki/tls/certs/*.crt; do
if ! openssl x509 -in "$CRT" -noout -checkend 2592000 >/dev/null 2>&1; then
echo "EXPIRING SOON: $CRT"
openssl x509 -in "$CRT" -noout -subject -enddate
fi
done
3. Checking whether the key and the certificate are a pair
Half of all "I swapped the certificate and the server will not start" cases come down to the key and the certificate not being a pair. The way to tell is to compare the public key portion.
For an RSA key, compare the modulus.
openssl x509 -in server.crt -noout -modulus | openssl sha256
openssl rsa -in server.key -noout -modulus | openssl sha256
openssl req -in server.csr -noout -modulus | openssl sha256
If all three values match, the CSR, the certificate, and the key are one set. If they differ, they are not a pair.
Non-RSA keys (ECDSA, Ed25519) have no -modulus. In that case extract the public key itself and compare.
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
This works regardless of key type, so if you are going to memorize just one command, make it this one.
For information about the private key itself, use the following.
openssl pkey -in server.key -noout -text
openssl rsa -in server.key -check -noout
A leak warning in place of a destructive command warning: commands that print a private key leave it in terminal history and in screen shares. Instead of printing the full key contents, build the habit of comparing only digests as shown above.
4. Creating a CSR — always include the SAN
The most common mistake when generating a CSR is leaving out the SAN. Using a configuration file is the reliable way.
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[dn]
C = KR
ST = Seoul
O = Example Corp
CN = www.example.com
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.example.com
DNS.2 = example.com
DNS.3 = api.example.com
Save this file as csr.cnf and then generate.
openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr -config csr.cnf
openssl req -in server.csr -noout -text
openssl req -in server.csr -noout -verify
If you use an ECDSA key.
openssl ecparam -name prime256v1 -genkey -noout -out server-ec.key
openssl req -new -key server-ec.key -out server-ec.csr -config csr.cnf
After generating, always confirm that the SAN went in. Miss it here and you have to get the certificate reissued.
openssl req -in server.csr -noout -text | grep -A3 'Subject Alternative Name'
A private CA and a server certificate for internal testing are made like this.
openssl req -x509 -new -nodes -newkey rsa:4096 -sha256 -days 3650 \
-subj '/C=KR/O=Example Internal/CN=Example Internal Root CA' \
-keyout ca.key -out ca.crt
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 397 -sha256 -extfile csr.cnf -extensions req_ext
-CAcreateserial creates the serial number file if it does not exist. If you do not specify -extfile and -extensions, the SAN is not included in the signed result. That is the most common reason a certificate made by a private CA fails in the browser.
5. Chain verification — catching a missing intermediate
The standard cause of "it works in my browser but not with curl on the server" is a missing intermediate certificate. Browsers fill in the intermediate from cache or from AIA information, but command-line tools and server-to-server traffic do not.
Verify against local files.
openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile root.crt -untrusted intermediate.crt server.crt
openssl verify -show_chain -CAfile /etc/pki/tls/certs/ca-bundle.crt server.crt
-CAfilespecifies the trust anchor (the root).-untrustedsupplies intermediate certificates.-show_chaindisplays the chain that was built.
The system trust store path differs per distribution. On RHEL-family systems it is /etc/pki/tls/certs/ca-bundle.crt, and on Debian/Ubuntu systems it is /etc/ssl/certs/ca-certificates.crt.
Counting how many certificates are inside a certificate file is also useful.
grep -c 'BEGIN CERTIFICATE' fullchain.pem
The file you install on the server is normally concatenated with the server certificate first and the intermediate certificates after it. By convention the root is not included. If the order is reversed, some clients fail.
cat server.crt intermediate.crt > fullchain.pem
openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout
The last command lists the subject and issuer of each certificate in the bundle file. Checking whether the issuer of the preceding certificate matches the subject of the next one tells you whether the order is right.
Here is a summary of the parts of chain construction that are easy to confuse. What the server has to send is its own certificate plus every intermediate certificate needed to reach the root. The client already has the root certificate in its trust store, so there is no need to send it, and it is usually ignored if you do. Sending it does push unnecessary bytes on every handshake, which is why it is conventionally excluded.
There are also cases with multiple intermediates. Commercial CAs today sometimes use a two-level intermediate structure, so incidents happen where someone assembles the chain by hand instead of using the bundle attached to the issuance email and drops one certificate. If you assembled it, always verify afterwards.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt -untrusted intermediate.crt server.crt
If this command prints OK, the chain is complete as judged against the local trust store. If it fails, the error message says at which step the chain broke.
6. Diagnosing against a live server — s_client
These are the commands for checking what the actual server sends.
openssl s_client -connect example.com:443 -servername example.com
openssl s_client -connect example.com:443 -servername example.com -showcerts
openssl s_client -connect example.com:443 -servername example.com -brief
openssl s_client -connect example.com:443 -servername example.com -tls1_2
openssl s_client -connect example.com:443 -servername example.com -status
openssl s_client -connect smtp.example.com:587 -starttls smtp
The option meanings are per the documentation.
-connectis the connection target.-servernameputs SNI in the ClientHello. In a virtual hosting environment, without this you get the wrong default certificate. Always include it when diagnosing.-showcertsshows the certificate list the server sent, exactly as sent. As the documentation states explicitly, this is not a verified chain but what the server sent verbatim. That makes it exactly right for confirming a missing intermediate.-statusrequests an OCSP stapling response.-briefprints only a connection summary.-starttlssends the protocol-specific upgrade message. It supports smtp, imap, pop3, ftp, postgres, mysql, ldap, and others.
The command sits waiting for input, so in a script close standard input.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates
This one line is the standard idiom for checking remote certificate expiry. Use it as-is when batch-checking several hosts.
for H in www.example.com api.example.com admin.example.com; do
printf '%s ' "$H"
echo | openssl s_client -connect "$H:443" -servername "$H" 2>/dev/null \
| openssl x509 -noout -enddate
done
Check the chain verification result in the verify-related lines near the top of the output. If you see unable to get local issuer certificate, either the intermediate is missing or the root is not in the local trust store.
7. Common errors and the order of diagnosis
| Symptom | Command to check | Likely cause |
|---|---|---|
| Works in the browser, fails with curl | s_client -showcerts | Missing intermediate |
| Only one domain fails | x509 -ext subjectAltName | Missing SAN |
| Server fails to start, key error | Compare modulus or pubkey digests | Key and certificate mismatch |
| Renewed but the old certificate shows | s_client -servername | Missing reload, wrong virtual host |
| Only old clients fail | s_client -tls1_2 | Protocol or cipher suite mismatch |
| Private CA certificate is rejected | verify -CAfile | CA not in the trust store |
| No alert before expiry | x509 -checkend | No monitoring |
The order of diagnosis is always the same. First, confirm what the file is. Second, confirm it pairs with the key. Third, confirm the chain is complete. Fourth, confirm what the server actually sends. Follow this order and the cause usually surfaces at step two or three.
The way to register a private CA into the system trust store also differs per distribution.
# RHEL family
sudo cp internal-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract
# Debian family
sudo cp internal-ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates
Caution: the required file extension differs per distribution. Debian-family systems require a .crt extension and PEM format. After registering, confirm that it is actually trusted.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt
curl -sSI https://internal.example.com | head -1
The problem of a renewal not taking effect is also common. If you only replace the file and do not reload the service, the process keeps using the old certificate it loaded into memory. Comparing the file timestamp with the certificate actually being served identifies this immediately.
ls -l --time-style=long-iso /etc/pki/tls/certs/server.crt
openssl x509 -in /etc/pki/tls/certs/server.crt -noout -enddate
echo | openssl s_client -connect localhost:443 -servername www.example.com 2>/dev/null | openssl x509 -noout -enddate
If the file expiry date and the expiry date of the certificate the server actually serves differ, the reload was skipped. If you have set up automatic renewal, always confirm that the renewal hook includes a service reload. There are plenty of real cases where the renewal succeeded, it was never picked up, and the service went down on expiry.
Another thing that is frequently forgotten is the fact that the trust store differs per application. Java uses its own keystore, Python uses the certifi bundle, and Node.js uses a built-in list. If you registered the CA with the system and only one application still fails, you have to check that runtime's trust store separately.
Quiz: check your understanding
Quiz 1: It works in the browser but only server-to-server API calls throw certificate errors. What do you check first?
Answer: Check whether the server also sends the intermediate certificate
Explanation: Browsers fill in the intermediate from cache or AIA information, but command-line tools and most language runtimes do not.
echo | openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
If it prints 1, the server is sending only its own certificate, which means the intermediate is missing. Concatenate the file for the server with the server certificate first and the intermediate after it, then reinstall.
Quiz 2: What is a key-type-agnostic way to confirm that a certificate and a private key are a pair?
Answer: Extract the public key from both sides and compare the digests
Explanation: Comparing -modulus only works for RSA. For ECDSA or Ed25519, use the following method.
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
If the two values match, they are a pair. This method does not care about key type, so if you memorize only one, memorize this one. It is also safer in that it never prints the private key contents to the screen.
Quiz 3: A server certificate made with a private CA is rejected by the browser for a domain mismatch. What was left out?
Answer: The SAN extension was not included at signing time
Explanation: Even if you put the SAN in the CSR, if you do not name the extension when signing with openssl x509 -req, the SAN does not end up in the resulting certificate.
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 397 -sha256 -extfile csr.cnf -extensions req_ext
Always confirm after signing.
openssl x509 -in server.crt -noout -ext subjectAltName
Modern clients ignore the CN and look only at the SAN.
Quiz 4: Several domains live on one IP. How do you check a specific domain's certificate with s_client?
Answer: Specify SNI with -servername
Explanation: Without SNI, the server returns the certificate of the default virtual host, so you end up looking at a certificate other than the one for the domain you meant to check.
echo | openssl s_client -connect 203.0.113.10:443 -servername api.example.com 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
According to the documentation, omitting -servername makes the hostname from -connect the default, so when you connect directly by IP you must state it explicitly.
Quiz 5: How do you automatically find certificates that expire within 30 days?
Answer: Pass a value in seconds to -checkend and judge by the exit code
Explanation: According to the documentation, -checkend checks whether the certificate expires within the given number of seconds. 30 days is 2592000 seconds.
for CRT in /etc/pki/tls/certs/*.crt; do
openssl x509 -in "$CRT" -noout -checkend 2592000 >/dev/null 2>&1 \
|| echo "EXPIRING: $CRT"
done
Remote servers can be checked the same way.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -checkend 2592000
Quiz 6: You registered the internal CA in the system trust store, but only the Java application still fails. Why?
Answer: Because the application runtime uses its own trust store
Explanation: Java uses its own keystore, Python uses the certifi bundle, and Node.js uses a built-in CA list. Updating the OS trust store does not reach those runtimes.
First confirm that things are fine at the system level.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt
curl -sSI https://internal.example.com | head -1
If curl works and only the application fails, the cause is confirmed. Check the exact procedure for registering a CA with each runtime in that runtime's documentation.
Closing
Certificate diagnosis is not hard as long as you keep to the order. What the file is, whether it pairs with the key, whether the chain is complete, and what the server actually sends. Check those four in sequence and the cause reveals itself.
And a command you cannot recall in a hurry is useless. Memorize at least these three blocks.
# 1. Inspect the local file
openssl x509 -in server.crt -noout -text
# 2. Check the remote server
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates
# 3. Check that the key and certificate match
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
These three blocks cover 90 percent of certificate-related work.
Finally, an outage caused by expiry is a 100 percent preventable incident. Put a check script built from a single -checkend line in place before the day is out.
References
- openssl-x509 official documentation (verified 2026-08-15)
- openssl-s_client official documentation (verified 2026-08-15)
- openssl-req official documentation (verified 2026-08-15)
- openssl-verify official documentation (verified 2026-08-15)
Further reading
- Previous: The Complete Guide to Backup and Restore
- Next: The Complete Guide to File Descriptors and Inodes
- The Complete Guide to SSL/TLS Certificates — the issuance and automatic renewal flow
- The Complete Guide to SSH Operations — key management problems in the same family
- curl builder — build curl commands with TLS options attached
- Linux terminal — practice openssl commands
현재 단락 (1/235)
Certificate problems are always urgent. You missed a renewal and the service stopped, or you install...