Uptime & SSL14 min read

    How to check and verify SSL certificates with OpenSSL

    By DanaServer Monitoring & Linux
    Share

    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 openssl is LibreSSL — install GNU OpenSSL via brew install openssl and use /opt/homebrew/opt/openssl@3/bin/openssl for 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 -servername to s_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.
    • -noout everywhere by default. Without it, openssl x509 and openssl req print the original PEM at the bottom of every command — fine occasionally, irritating in pipelines.
    • echo | before s_client matters. s_client blocks waiting for stdin otherwise. The echo | (or < /dev/null) closes stdin so the connection completes and openssl exits 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 verify uses -CApath /etc/ssl/certs by 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_client warns 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:

    1. openssl x509 -in cert.pem -noout -text — dump everything from a file.
    2. echo | openssl s_client -servername host -connect host:443 -showcerts — inspect a live endpoint, full chain, with SNI.
    3. openssl rsa -modulus -noout -in key.pem | openssl md5 vs the same on the cert — confirm the key and cert match before deploying.
    4. openssl verify -CAfile chain.pem cert.pem — confirm chain trusts to a root; failures map to specific config bugs.
    5. openssl x509 -noout -checkend N — script-friendly expiry check (exit-code based).
    6. openssl x509 -noout -ext subjectAltName — confirm SANs cover every hostname you serve.
    7. openssl s_client -status — confirm OCSP stapling is healthy.
    8. 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.