Skip to content

OpenSSL: TLS Certificate Verification and Parsing in CLI

Certificates expiring on prod at the worst moment — a familiar story. OpenSSL answers TLS certificate questions faster than any marketplace checker. Here are the key scenarios without the fluff.

Basic Certificate Parsing

The first command for any diagnostics is the text dump:

openssl x509 -text -noout -in cert.pem

Output shows Subject, Issuer, validity dates, signature algorithm, and public key. For a quick summary without the wall of text:

# Subject only
openssl x509 -noout -subject -in cert.pem

# Issuer only
openssl x509 -noout -issuer -in cert.pem

# Fingerprint only (SHA-256)
openssl x509 -noout -fingerprint -sha256 -in cert.pem

The -in flag accepts a file path. Certificates downloaded from browsers usually come in PEM or DER format. OpenSSL handles both, but DER requires an extra flag:

openssl x509 -inform DER -in cert.der -text -noout
Note

-noout suppresses the base64 block from output. Useful when you need only structured data, not a copy of the certificate.

Expiration: dates and Overdue Checks

For monitoring, getting only the dates is more convenient:

openssl x509 -noout -dates -in cert.pem

Typical output:

notBefore=Jan 15 00:00:00 2024 GMT
notAfter=Jan 14 23:59:59 2025 GMT

For automation, extracting the timestamp and calculating the difference is cleaner:

# Days remaining until expiry
not_after=$(openssl x509 -noout -enddate -in cert.pem | cut -d= -f2)
days_left=$(( ($(date -d "$not_after" +%s) - $(date +%s)) / 86400 ))
echo "$days_left days left"

If days_left is negative, the certificate has already expired.

Tip

For checking multiple hosts from inventory, a one-liner works well:

for host in api.example.com admin.example.com; do
  echo -n "$host: "
  echo | openssl s_client -servername "$host" -connect "$host":443 2>/dev/null \
    | openssl x509 -noout -enddate
done

-servername sends SNI — without it some hosts return the default certificate.

Chain Verification: s_client and verify

Connect and display certificates:

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

Output includes the chain from leaf certificate to root CA. To filter certificates only:

openssl s_client -connect example.com:443 -showcerts </dev/null \
  | awk '/-----BEGIN/,/-----END/{if(/-----BEGIN/)a=1;a;if(/-----END/)a=0}' > chain.pem

Verify chain against the system store:

openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt chain.pem

If verification fails with error 20 at 0 depth lookup, the intermediate CA is missing. A common cause is incorrect chain configuration on the server.

Warning

openssl verify uses the system store by default. In Ubuntu this is /etc/ssl/certs/ca-certificates.crt, in Alpine it is a separate ca-certificates package. If verification fails, check that the package is installed.

Quick check without saving to file:

echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl verify

Output Verify return code: 0 (ok) means success.

To stay out of interactive mode and fail the process on a bad chain:

openssl s_client -connect example.com:443 -servername example.com \
  -quiet -verify_return_error </dev/null

Force a protocol or cipher when you need to confirm the server still accepts a specific handshake:

openssl s_client -connect example.com:443 -servername example.com \
  -tls1_2 -cipher ECDHE-RSA-AES256-GCM-SHA384 -quiet </dev/null

For an internal CA, pass the bundle explicitly. Without -CAfile, a private PKI typically returns Verify return code: 21 (unable to get local issuer certificate):

openssl s_client -connect example.com:443 -servername example.com \
  -CAfile /etc/ssl/certs/ca-bundle.crt -verify_return_error -quiet </dev/null

Extracting CN and SAN

Common Name extracts directly:

openssl x509 -noout -subject -in cert.pem | grep -oP '(?<=CN = )[^,]+'

But CN has not been sufficient for years — modern certificates use Subject Alternative Names (SAN). OpenSSL 1.1.1+ pulls them cleanly:

openssl x509 -noout -ext subjectAltName -in cert.pem

Output:

X509v3 Subject Alternative Name:
    DNS:example.com, DNS:www.example.com, DNS:api.example.com, IP:192.0.2.1

To get only the DNS names:

openssl x509 -noout -ext subjectAltName -in cert.pem \
  | grep -oP '(?<=DNS:)[^,]+'
Note

If SAN is missing (old certificate), browsers fall back to CN. When checking API endpoints, this explains why curl complains but the browser opens the page.

Comparing Expiration Across Multiple Hosts

A script for checking a host list serves as a practical monitoring foundation:

#!/bin/bash
# check-certs.sh — check certificate expiration dates

check_host() {
  local host=$1
  local port=${2:-443}
  
  echo | timeout 5 openssl s_client -servername "$host" -connect "$host:$port" 2>/dev/null \
    | openssl x509 -noout -enddate 2>/dev/null \
    | cut -d= -f2 \
    | while read date; do
        ts=$(date -d "$date" +%s)
        now=$(date +%s)
        days=$(( (ts - now) / 86400 ))
        printf "%-30s %3d days  %s\n" "$host" "$days" "$date"
      done
}

# Example usage
for h in api.example.com admin.example.com legacy.internal; do
  check_host "$h"
done

Typical output:

api.example.com                   45 days  Jan 14 23:59:59 2025 GMT
admin.example.com               -12 days  Dec  1 23:59:59 2024 GMT
legacy.internal                -120 days  Aug  5 23:59:59 2024 GMT

Negative values are expired certificates. In production, wrapping this in cron with chat notifications at a 30-day threshold is practical.

Quick Flag Reference

CommandFlagPurpose
x509-textFull text dump
x509-nooutSuppress base64 block
x509-datesNotBefore, NotAfter
x509-subjectSubject (CN, O, OU)
x509-issuerIssuing CA
x509-fingerprint -sha256Certificate fingerprint
x509-enddateExpiration date only
x509-ext subjectAltNameAlternative names
s_client-connect host:portTLS connection
s_client-servername nameSNI (required for vhost)
s_client-showcertsDisplay full chain
s_client-quietSkip interactive mode
s_client-tls1_2 / -tls1_3Force a TLS version
s_client-verify_return_errorNon-zero exit on validation failure
verify-CAfile pathTrusted CA file
verify-partial_chainAccept partial chain
Tip

openssl s_client does more than read certificates. With -starttls smtp or -starttls pop3 it checks mail servers. -http retrieves HTTP headers over TLS. Useful for diagnosing miTM filters.

All commands work out of the box in any Linux distribution. No dependencies beyond OpenSSL itself — the tool is present on every server. If missing, install in seconds: apt install openssl or apk add openssl.