OPENSSL: TLS AND CERTIFICATE CHECKS
A working reference for checking a TLS server and its certificates with the openssl command line (OpenSSL 3.x): what the server sends, whether the chain and hostname verify, when it expires, which protocols and ciphers it accepts, and how to read certificate files. Every command here only reads or connects; nothing changes the server. Set the target once, then paste the lines below as they are: H=example.com; PORT=443 Files used below: cert.pem (a certificate), key.pem (its private key), csr.pem (a signing request), chain.pem (intermediates), ca.pem (a trusted root). Add 2>/dev/null to hide the progress lines openssl prints.
1. CONNECT AND SHOW WHAT THE SERVER SENDS#
Inspect:
openssl s_client -connect $H:$PORT -servername $H -brief </dev/null
openssl s_client -connect $H:$PORT -servername $H -showcerts </dev/null
# subject and issuer of each certificate the server sent (leaf first):
openssl s_client -connect $H:$PORT -servername $H -showcerts \
</dev/null 2>/dev/null | openssl crl2pkcs7 -nocrl -certfile /dev/stdin \
| openssl pkcs7 -print_certs -noout
# save the leaf certificate for the checks in section 2:
openssl s_client -connect $H:$PORT -servername $H </dev/null \
2>/dev/null | openssl x509 -out cert.pem
Notes: -servername sends SNI. openssl sets it from a host NAME in -connect
by itself, but not from an IP address, so always give it when connecting
by IP or through a load balancer. </dev/null makes s_client exit after the
handshake instead of waiting for input. -brief writes to stderr: add 2>&1
before piping it to grep.
Flags: "Verification error" in -brief output; only one certificate sent
for a publicly trusted site (missing intermediate); a certificate for a
different name (the default certificate of the server, often a sign that
SNI was missing).
2. READ A CERTIFICATE#
Inspect:
openssl x509 -in cert.pem -noout -subject -issuer -serial -dates
openssl x509 -in cert.pem -noout -ext subjectAltName
openssl x509 -in cert.pem -noout \
-ext keyUsage,extendedKeyUsage,basicConstraints
openssl x509 -in cert.pem -noout -text | grep -E 'Public-Key|Signature Al'
openssl x509 -in cert.pem -noout -fingerprint -sha256
openssl x509 -in cert.pem -noout -ocsp_uri
openssl x509 -in cert.pem -noout -ext crlDistributionPoints
Notes: "No extensions in certificate" means that extension is absent. The
name must be in subjectAltName. RFC 9525 says the CN must not be used and
browsers ignore it; openssl -verify_hostname still falls back to the CN,
but only when the certificate has no DNS name in its SAN, so an openssl
"OK" on a SAN-less certificate does not mean browsers accept it. Dates are
in GMT. Publicly trusted server certificates issued on or after 2026-03-15
may be valid for at most 200 days; from 2027-03-15 at most 100 days, from
2029-03-15 at most 47 days (CA/Browser Forum Baseline Requirements 6.3.2).
Minimum key: RSA 2048 bits or ECDSA P-256/384/521.
Flags: the hostname is not in subjectAltName (or there is no SAN at
all); notAfter close or past; RSA under 2048 bits; sha1WithRSAEncryption
or md5 in the signature algorithm; CA:TRUE on a server certificate; no
serverAuth in extendedKeyUsage; a public certificate valid for longer
than the limits above.
3. VERIFY THE CHAIN AND THE HOSTNAME#
Inspect:
# against the system trust store, checking the name too:
openssl s_client -connect $H:$PORT -servername $H \
-verify_hostname $H -verify_return_error -brief </dev/null
echo "exit code: $?"
# files: leaf, intermediates (untrusted) and your own root:
openssl verify -CAfile ca.pem -untrusted chain.pem cert.pem
openssl verify -CAfile ca.pem -untrusted chain.pem \
-purpose sslserver -verify_hostname www.example.com cert.pem
openssl verify -show_chain -CAfile ca.pem -untrusted chain.pem cert.pem
Notes: without -verify_hostname, "Verification: OK" only means the chain
is valid; it says nothing about the name. Without -verify_return_error,
s_client exits 0 even when verification fails, so scripts must add it.
-untrusted certificates are used to build the chain but are not trusted
themselves; only -CAfile / -CApath (or the default store) are anchors.
Verify error codes you will see (the number after num= or code):
2 unable to get issuer certificate (for example an intermediate as
the only -CAfile; add -partial_chain only if you mean to trust it)
10 certificate has expired
18 self-signed certificate (a leaf signed by itself)
19 self-signed certificate in certificate chain (unknown root)
20 unable to get local issuer certificate (missing intermediate
or a CA you do not trust)
21 unable to verify the first certificate (server sent the leaf only)
62 hostname mismatch
Flags: any code other than 0 on a public site; 20 or 21 from a server
that "works in the browser" (browsers may fetch or cache the missing
intermediate, other clients fail).
4. KEY, CERTIFICATE AND CSR MATCH#
Inspect (the three hashes must be identical; works for RSA and EC): openssl x509 -in cert.pem -noout -pubkey | openssl sha256 openssl pkey -in key.pem -pubout | openssl sha256 openssl req -in csr.pem -noout -pubkey | openssl sha256 Read a signing request before you submit it: openssl req -in csr.pem -noout -verify -subject openssl req -in csr.pem -noout -text | grep -A1 'Alternative Name' Notes: -verify checks the CSR's self-signature. Comparing the modulus (-modulus) works for RSA only; the public-key hash works for all types. Flags: different hashes (wrong key installed: the server fails to start or clients get handshake errors); a CSR with no subjectAltName; a CSR signature that does not verify.
5. PROTOCOLS AND CIPHERS#
Inspect (each line either connects or fails; read the first lines):
openssl s_client -connect $H:$PORT -servername $H -tls1_3 -brief \
</dev/null
openssl s_client -connect $H:$PORT -servername $H -tls1_2 -brief \
</dev/null
# old protocols: openssl 3 refuses them at its default security level
openssl s_client -connect $H:$PORT -servername $H -tls1_1 \
-cipher 'DEFAULT:@SECLEVEL=0' -brief </dev/null
# one TLS 1.2 cipher, one TLS 1.3 suite:
openssl s_client -connect $H:$PORT -servername $H -tls1_2 \
-cipher ECDHE-RSA-AES128-GCM-SHA256 -brief </dev/null
openssl s_client -connect $H:$PORT -servername $H -tls1_3 \
-ciphersuites TLS_AES_128_GCM_SHA256 -brief </dev/null
openssl ciphers -v 'HIGH:!aNULL' | head # what your openssl offers
Notes: TLS 1.2 ciphers are set with -cipher, TLS 1.3 suites with
-ciphersuites (RFC 8446 defines only five). A failed line ends with an
error such as "no protocols available" (your client refused) or "alert
handshake failure" / "alert protocol version" (the server refused). Read
which side said no before you report it.
Flags: TLSv1 or TLSv1.1 accepted (deprecated by RFC 8996); RC4, 3DES,
EXPORT, NULL or anonymous (aNULL) ciphers accepted; no forward secrecy
(no ECDHE/DHE) for TLS 1.2; no TLS 1.3 on a modern server.
6. REVOCATION: OCSP AND CRL#
Inspect:
# stapled OCSP response sent during the handshake:
openssl s_client -connect $H:$PORT -servername $H -status \
</dev/null 2>/dev/null | grep -A4 -i 'OCSP response'
# ask the CA's OCSP responder (needs the issuer certificate):
URL=$(openssl x509 -in cert.pem -noout -ocsp_uri)
openssl ocsp -issuer chain.pem -cert cert.pem -url "$URL" \
-CAfile ca.pem
# a downloaded CRL (DER is the usual format):
openssl crl -in crl.der -inform DER -noout -lastupdate -nextupdate
openssl crl -in crl.der -inform DER -noout -text | grep -c 'Serial'
Notes: OCSP is optional for public CAs since Baseline Requirements 2.0.1
(2024); CRLs are required. Many certificates now have no OCSP URL, so an
empty -ocsp_uri is not an error by itself. "OCSP response: no response
sent" means the server does not staple. The answer from ocsp is
"good", "revoked" (with a reason and time) or "unknown".
Flags: "revoked"; "unknown" from the issuing CA's own responder; a
response whose This/Next Update is in the past; a CRL past its
nextUpdate.
7. STARTTLS: MAIL AND OTHER PROTOCOLS#
Inspect:
M=mail.example.com
openssl s_client -starttls smtp -connect $M:25 -servername $M -brief \
</dev/null
openssl s_client -starttls smtp -connect $M:587 -servername $M -brief \
</dev/null
openssl s_client -starttls imap -connect $M:143 -servername $M -brief \
</dev/null
openssl s_client -connect $M:993 -servername $M -brief </dev/null
Notes: ports 25, 587, 143 and 110 start in plain text and upgrade with
STARTTLS; 465, 993 and 995 are TLS from the first byte (no -starttls).
-starttls also knows pop3, ftp, xmpp, xmpp-server, ldap, postgres,
mysql, lmtp, nntp, sieve and irc. Many home and cloud networks block
outgoing port 25.
Flags: "Didn't find STARTTLS in server response" (the server does not
offer it); a certificate for another name on a mail server listed in MX;
the same checks as sections 3 and 5.
8. FILE FORMATS: PEM, DER, PKCS#12#
Inspect:
head -1 cert.pem # "-----BEGIN CERTIFICATE-----" = PEM
openssl x509 -in cert.der -inform DER -noout -subject -dates
openssl x509 -in cert.pem -outform DER -out cert.der # PEM -> DER
# PKCS#12 (.p12 / .pfx): list without writing keys anywhere:
openssl pkcs12 -in bundle.p12 -info -nokeys
openssl pkcs12 -in bundle.p12 -nokeys -clcerts \
| openssl x509 -noout -subject -dates
Notes: openssl asks for the import password. A .p12 whose certificates
are encrypted with RC2 (the old default of many tools) fails in OpenSSL 3
with "Error outputting keys and certificates" and an "unsupported"
algorithm error; add -legacy to read it. 3DES bundles still open. Do
not put passwords on the command line on shared systems (they show in ps
and shell history).
Flags: a .p12 that contains a different leaf than you expected; a
bundle that holds the private key when only the certificate was
needed; keys in files readable by other users.
9. EXPIRY MONITORING#
Inspect:
# exit 0 if still valid in 30 days, exit 1 if it expires before:
openssl x509 -in cert.pem -noout -checkend $((30*24*3600))
# the same for a live server:
openssl s_client -connect $H:$PORT -servername $H </dev/null \
2>/dev/null | openssl x509 -noout -checkend $((30*24*3600))
# days left (GNU date):
END=$(openssl x509 -in cert.pem -noout -enddate | cut -d= -f2)
echo $(( ($(date -d "$END" +%s) - $(date +%s)) / 86400 )) days left
Notes: -checkend takes seconds. With 200-day and shorter certificates,
renewal must be automated (ACME); set the alert window from the
certificate's lifetime, not a fixed 30 days. Check every name and
every server behind a load balancer: they can serve different
certificates.
Flags: exit code 1; days left below your renewal window; different
expiry dates for the same name on different servers.
QUICK CHECK ORDER#
1. s_client -brief with -servername: protocol, cipher, verification. 2. -showcerts: does the server send the intermediates (section 1)? 3. Save the leaf; read names, dates, key and signature (section 2). 4. -verify_hostname with -verify_return_error; note the code (section 3). 5. Key matches certificate before you install a renewal (section 4). 6. TLS 1.0/1.1 refused, TLS 1.3 offered, no weak ciphers (section 5). 7. Revocation: stapling, OCSP or CRL as the CA provides (section 6). 8. Mail servers: STARTTLS on 25/587/143, TLS on 465/993 (section 7). 9. Monitor with -checkend from more than one place (section 9). Commands checked against the OpenSSL 3 manual pages (s_client, x509, verify, req, pkey, ocsp, crl, pkcs12, ciphers, crl2pkcs7, pkcs7) and run with OpenSSL 3.0.13 against a test root, intermediate and leaf and against example.com. Standards: RFC 5280 (certificates and CRLs), RFC 6960 (OCSP), RFC 8446 (TLS 1.3), RFC 8996 (TLS 1.0/1.1 deprecated), RFC 9525 (service identity); certificate validity and key sizes per CA/Browser Forum Baseline Requirements version 2.3.1.
OPENSSL: TLS AND CERTIFICATE CHECKS
===================================
A working reference for checking a TLS server and its certificates with
the openssl command line (OpenSSL 3.x): what the server sends, whether the
chain and hostname verify, when it expires, which protocols and ciphers it
accepts, and how to read certificate files. Every command here only reads
or connects; nothing changes the server.
Set the target once, then paste the lines below as they are:
H=example.com; PORT=443
Files used below: cert.pem (a certificate), key.pem (its private key),
csr.pem (a signing request), chain.pem (intermediates), ca.pem (a trusted
root). Add 2>/dev/null to hide the progress lines openssl prints.
1. CONNECT AND SHOW WHAT THE SERVER SENDS
-----------------------------------------
Inspect:
openssl s_client -connect $H:$PORT -servername $H -brief </dev/null
openssl s_client -connect $H:$PORT -servername $H -showcerts </dev/null
# subject and issuer of each certificate the server sent (leaf first):
openssl s_client -connect $H:$PORT -servername $H -showcerts \
</dev/null 2>/dev/null | openssl crl2pkcs7 -nocrl -certfile /dev/stdin \
| openssl pkcs7 -print_certs -noout
# save the leaf certificate for the checks in section 2:
openssl s_client -connect $H:$PORT -servername $H </dev/null \
2>/dev/null | openssl x509 -out cert.pem
Notes: -servername sends SNI. openssl sets it from a host NAME in -connect
by itself, but not from an IP address, so always give it when connecting
by IP or through a load balancer. </dev/null makes s_client exit after the
handshake instead of waiting for input. -brief writes to stderr: add 2>&1
before piping it to grep.
Flags: "Verification error" in -brief output; only one certificate sent
for a publicly trusted site (missing intermediate); a certificate for a
different name (the default certificate of the server, often a sign that
SNI was missing).
2. READ A CERTIFICATE
---------------------
Inspect:
openssl x509 -in cert.pem -noout -subject -issuer -serial -dates
openssl x509 -in cert.pem -noout -ext subjectAltName
openssl x509 -in cert.pem -noout \
-ext keyUsage,extendedKeyUsage,basicConstraints
openssl x509 -in cert.pem -noout -text | grep -E 'Public-Key|Signature Al'
openssl x509 -in cert.pem -noout -fingerprint -sha256
openssl x509 -in cert.pem -noout -ocsp_uri
openssl x509 -in cert.pem -noout -ext crlDistributionPoints
Notes: "No extensions in certificate" means that extension is absent. The
name must be in subjectAltName. RFC 9525 says the CN must not be used and
browsers ignore it; openssl -verify_hostname still falls back to the CN,
but only when the certificate has no DNS name in its SAN, so an openssl
"OK" on a SAN-less certificate does not mean browsers accept it. Dates are
in GMT. Publicly trusted server certificates issued on or after 2026-03-15
may be valid for at most 200 days; from 2027-03-15 at most 100 days, from
2029-03-15 at most 47 days (CA/Browser Forum Baseline Requirements 6.3.2).
Minimum key: RSA 2048 bits or ECDSA P-256/384/521.
Flags: the hostname is not in subjectAltName (or there is no SAN at
all); notAfter close or past; RSA under 2048 bits; sha1WithRSAEncryption
or md5 in the signature algorithm; CA:TRUE on a server certificate; no
serverAuth in extendedKeyUsage; a public certificate valid for longer
than the limits above.
3. VERIFY THE CHAIN AND THE HOSTNAME
------------------------------------
Inspect:
# against the system trust store, checking the name too:
openssl s_client -connect $H:$PORT -servername $H \
-verify_hostname $H -verify_return_error -brief </dev/null
echo "exit code: $?"
# files: leaf, intermediates (untrusted) and your own root:
openssl verify -CAfile ca.pem -untrusted chain.pem cert.pem
openssl verify -CAfile ca.pem -untrusted chain.pem \
-purpose sslserver -verify_hostname www.example.com cert.pem
openssl verify -show_chain -CAfile ca.pem -untrusted chain.pem cert.pem
Notes: without -verify_hostname, "Verification: OK" only means the chain
is valid; it says nothing about the name. Without -verify_return_error,
s_client exits 0 even when verification fails, so scripts must add it.
-untrusted certificates are used to build the chain but are not trusted
themselves; only -CAfile / -CApath (or the default store) are anchors.
Verify error codes you will see (the number after num= or code):
2 unable to get issuer certificate (for example an intermediate as
the only -CAfile; add -partial_chain only if you mean to trust it)
10 certificate has expired
18 self-signed certificate (a leaf signed by itself)
19 self-signed certificate in certificate chain (unknown root)
20 unable to get local issuer certificate (missing intermediate
or a CA you do not trust)
21 unable to verify the first certificate (server sent the leaf only)
62 hostname mismatch
Flags: any code other than 0 on a public site; 20 or 21 from a server
that "works in the browser" (browsers may fetch or cache the missing
intermediate, other clients fail).
4. KEY, CERTIFICATE AND CSR MATCH
---------------------------------
Inspect (the three hashes must be identical; works for RSA and EC):
openssl x509 -in cert.pem -noout -pubkey | openssl sha256
openssl pkey -in key.pem -pubout | openssl sha256
openssl req -in csr.pem -noout -pubkey | openssl sha256
Read a signing request before you submit it:
openssl req -in csr.pem -noout -verify -subject
openssl req -in csr.pem -noout -text | grep -A1 'Alternative Name'
Notes: -verify checks the CSR's self-signature. Comparing the modulus
(-modulus) works for RSA only; the public-key hash works for all types.
Flags: different hashes (wrong key installed: the server fails to start
or clients get handshake errors); a CSR with no subjectAltName; a CSR
signature that does not verify.
5. PROTOCOLS AND CIPHERS
------------------------
Inspect (each line either connects or fails; read the first lines):
openssl s_client -connect $H:$PORT -servername $H -tls1_3 -brief \
</dev/null
openssl s_client -connect $H:$PORT -servername $H -tls1_2 -brief \
</dev/null
# old protocols: openssl 3 refuses them at its default security level
openssl s_client -connect $H:$PORT -servername $H -tls1_1 \
-cipher 'DEFAULT:@SECLEVEL=0' -brief </dev/null
# one TLS 1.2 cipher, one TLS 1.3 suite:
openssl s_client -connect $H:$PORT -servername $H -tls1_2 \
-cipher ECDHE-RSA-AES128-GCM-SHA256 -brief </dev/null
openssl s_client -connect $H:$PORT -servername $H -tls1_3 \
-ciphersuites TLS_AES_128_GCM_SHA256 -brief </dev/null
openssl ciphers -v 'HIGH:!aNULL' | head # what your openssl offers
Notes: TLS 1.2 ciphers are set with -cipher, TLS 1.3 suites with
-ciphersuites (RFC 8446 defines only five). A failed line ends with an
error such as "no protocols available" (your client refused) or "alert
handshake failure" / "alert protocol version" (the server refused). Read
which side said no before you report it.
Flags: TLSv1 or TLSv1.1 accepted (deprecated by RFC 8996); RC4, 3DES,
EXPORT, NULL or anonymous (aNULL) ciphers accepted; no forward secrecy
(no ECDHE/DHE) for TLS 1.2; no TLS 1.3 on a modern server.
6. REVOCATION: OCSP AND CRL
---------------------------
Inspect:
# stapled OCSP response sent during the handshake:
openssl s_client -connect $H:$PORT -servername $H -status \
</dev/null 2>/dev/null | grep -A4 -i 'OCSP response'
# ask the CA's OCSP responder (needs the issuer certificate):
URL=$(openssl x509 -in cert.pem -noout -ocsp_uri)
openssl ocsp -issuer chain.pem -cert cert.pem -url "$URL" \
-CAfile ca.pem
# a downloaded CRL (DER is the usual format):
openssl crl -in crl.der -inform DER -noout -lastupdate -nextupdate
openssl crl -in crl.der -inform DER -noout -text | grep -c 'Serial'
Notes: OCSP is optional for public CAs since Baseline Requirements 2.0.1
(2024); CRLs are required. Many certificates now have no OCSP URL, so an
empty -ocsp_uri is not an error by itself. "OCSP response: no response
sent" means the server does not staple. The answer from ocsp is
"good", "revoked" (with a reason and time) or "unknown".
Flags: "revoked"; "unknown" from the issuing CA's own responder; a
response whose This/Next Update is in the past; a CRL past its
nextUpdate.
7. STARTTLS: MAIL AND OTHER PROTOCOLS
-------------------------------------
Inspect:
M=mail.example.com
openssl s_client -starttls smtp -connect $M:25 -servername $M -brief \
</dev/null
openssl s_client -starttls smtp -connect $M:587 -servername $M -brief \
</dev/null
openssl s_client -starttls imap -connect $M:143 -servername $M -brief \
</dev/null
openssl s_client -connect $M:993 -servername $M -brief </dev/null
Notes: ports 25, 587, 143 and 110 start in plain text and upgrade with
STARTTLS; 465, 993 and 995 are TLS from the first byte (no -starttls).
-starttls also knows pop3, ftp, xmpp, xmpp-server, ldap, postgres,
mysql, lmtp, nntp, sieve and irc. Many home and cloud networks block
outgoing port 25.
Flags: "Didn't find STARTTLS in server response" (the server does not
offer it); a certificate for another name on a mail server listed in MX;
the same checks as sections 3 and 5.
8. FILE FORMATS: PEM, DER, PKCS#12
----------------------------------
Inspect:
head -1 cert.pem # "-----BEGIN CERTIFICATE-----" = PEM
openssl x509 -in cert.der -inform DER -noout -subject -dates
openssl x509 -in cert.pem -outform DER -out cert.der # PEM -> DER
# PKCS#12 (.p12 / .pfx): list without writing keys anywhere:
openssl pkcs12 -in bundle.p12 -info -nokeys
openssl pkcs12 -in bundle.p12 -nokeys -clcerts \
| openssl x509 -noout -subject -dates
Notes: openssl asks for the import password. A .p12 whose certificates
are encrypted with RC2 (the old default of many tools) fails in OpenSSL 3
with "Error outputting keys and certificates" and an "unsupported"
algorithm error; add -legacy to read it. 3DES bundles still open. Do
not put passwords on the command line on shared systems (they show in ps
and shell history).
Flags: a .p12 that contains a different leaf than you expected; a
bundle that holds the private key when only the certificate was
needed; keys in files readable by other users.
9. EXPIRY MONITORING
--------------------
Inspect:
# exit 0 if still valid in 30 days, exit 1 if it expires before:
openssl x509 -in cert.pem -noout -checkend $((30*24*3600))
# the same for a live server:
openssl s_client -connect $H:$PORT -servername $H </dev/null \
2>/dev/null | openssl x509 -noout -checkend $((30*24*3600))
# days left (GNU date):
END=$(openssl x509 -in cert.pem -noout -enddate | cut -d= -f2)
echo $(( ($(date -d "$END" +%s) - $(date +%s)) / 86400 )) days left
Notes: -checkend takes seconds. With 200-day and shorter certificates,
renewal must be automated (ACME); set the alert window from the
certificate's lifetime, not a fixed 30 days. Check every name and
every server behind a load balancer: they can serve different
certificates.
Flags: exit code 1; days left below your renewal window; different
expiry dates for the same name on different servers.
QUICK CHECK ORDER
-----------------
1. s_client -brief with -servername: protocol, cipher, verification.
2. -showcerts: does the server send the intermediates (section 1)?
3. Save the leaf; read names, dates, key and signature (section 2).
4. -verify_hostname with -verify_return_error; note the code (section 3).
5. Key matches certificate before you install a renewal (section 4).
6. TLS 1.0/1.1 refused, TLS 1.3 offered, no weak ciphers (section 5).
7. Revocation: stapling, OCSP or CRL as the CA provides (section 6).
8. Mail servers: STARTTLS on 25/587/143, TLS on 465/993 (section 7).
9. Monitor with -checkend from more than one place (section 9).
Commands checked against the OpenSSL 3 manual pages (s_client, x509,
verify, req, pkey, ocsp, crl, pkcs12, ciphers, crl2pkcs7, pkcs7) and
run with OpenSSL 3.0.13 against a test root, intermediate and leaf and
against example.com. Standards: RFC 5280 (certificates and CRLs), RFC
6960 (OCSP), RFC 8446 (TLS 1.3), RFC 8996 (TLS 1.0/1.1 deprecated),
RFC 9525 (service identity); certificate validity and key sizes per CA/Browser
Forum Baseline Requirements version 2.3.1.
Defensive reference on CyberRamen. Offensive / red-team sheets live on OffensiveRamen.com.