Skip to content

필사 모드: The Complete Guide to TLS Certificates: Handling Them End to End with openssl

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

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.

FormatContentsCommon extensions
PEMBase64 text with header lines.pem .crt .cer .key
DERBinary encoding.der .cer
PKCS#12Certificate and private key in one file.p12 .pfx
PKCS#7Certificate bundle (no key).p7b .p7c
PKCS#8Standard 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 certificate
  • BEGIN CERTIFICATE REQUEST: a CSR
  • BEGIN PRIVATE KEY: a PKCS#8 private key
  • BEGIN RSA PRIVATE KEY: a traditional RSA private key
  • BEGIN 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.

  • -noout suppresses the encoded original output and shows only what you asked for.
  • -text prints the whole certificate in human-readable form.
  • -subject and -issuer show the subject and the issuer, and -dates shows the validity start and end times. You can also see them individually with -startdate and -enddate.
  • -fingerprint computes the digest of the DER encoding.
  • -ext prints 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
  • -CAfile specifies the trust anchor (the root).
  • -untrusted supplies intermediate certificates.
  • -show_chain displays 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.

  • -connect is the connection target.
  • -servername puts SNI in the ClientHello. In a virtual hosting environment, without this you get the wrong default certificate. Always include it when diagnosing.
  • -showcerts shows 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.
  • -status requests an OCSP stapling response.
  • -brief prints only a connection summary.
  • -starttls sends 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

SymptomCommand to checkLikely cause
Works in the browser, fails with curls_client -showcertsMissing intermediate
Only one domain failsx509 -ext subjectAltNameMissing SAN
Server fails to start, key errorCompare modulus or pubkey digestsKey and certificate mismatch
Renewed but the old certificate showss_client -servernameMissing reload, wrong virtual host
Only old clients fails_client -tls1_2Protocol or cipher suite mismatch
Private CA certificate is rejectedverify -CAfileCA not in the trust store
No alert before expiryx509 -checkendNo 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


Further reading

현재 단락 (1/235)

Certificate problems are always urgent. You missed a renewal and the service stopped, or you install...

작성 글자: 0원문 글자: 19,682작성 단락: 0/235