The -showcerts flag makes s_client print every certificate the server sends โ not just the leaf. Pipe through openssl x509 to decode human-readable fields.
# Dump every cert the server sends, plus handshake info echo | openssl s_client -showcerts -connect google.com:443 -servername google.com 2>/dev/null
# Just the subject, issuer, and expiry of the leaf cert echo | openssl s_client -connect google.com:443 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates
# Full decoded text of the leaf cert (verbose) echo | openssl s_client -connect google.com:443 2>/dev/null \ | openssl x509 -noout -text
echo | prefix sends an empty string to close stdin immediately after the handshake, so the command doesn't hang waiting for input.With -showcerts, the output contains all certs separated by -----BEGIN/END CERTIFICATE----- markers. Cert #1 is always the leaf โ everything after is CA chain. Use awk to skip the first block.
echo | openssl s_client -showcerts -connect google.com:443 2>/dev/null \ | awk ' BEGIN { n=0; p=0 } /-----BEGIN CERTIFICATE-----/ { n++; if (n>1) p=1 } p { print } /-----END CERTIFICATE-----/ { p=0 } ' > google.com-ca.pem # Verify it saved correctly openssl x509 -in google.com-ca.pem -noout -subject -issuer
echo | openssl s_client -showcerts -connect google.com:443 2>/dev/null \ | awk ' BEGIN { n=0 } /-----BEGIN CERTIFICATE-----/ { n++; file="cert-"n".pem" } { print > file } ' # cert-1.pem = leaf, cert-2.pem = first CA, cert-3.pem = second CAโฆ openssl x509 -in cert-2.pem -noout -subject -issuer
awk filter above will produce an empty file. That's your signal that the server is leaf-only and you need to fetch the CA separately.# Subject, issuer, validity window openssl x509 -in ca.pem -noout -subject -issuer -dates # SHA-256 fingerprint openssl x509 -in ca.pem -noout -fingerprint -sha256 # Is it a CA cert? (look for CA:TRUE) openssl x509 -in ca.pem -noout -text | grep -A2 "Basic Constraints" # Subject Alternative Names (SANs) openssl x509 -in ca.pem -noout -text | grep -A1 "Subject Alternative" # Check if cert expires within 30 days (exit 1 if so) openssl x509 -in ca.pem -checkend 2592000 || echo "Expires within 30 days!"
When a server sends only the leaf cert (or you need the root CA which servers never send), the AIA (Authority Information Access) extension in the leaf cert contains a URL pointing to the issuing CA's certificate. It's almost always in DER format, so you need to convert it.
# Step 1: get the AIA CA Issuers URL from the leaf cert AIA_URL=$(echo | openssl s_client -connect google.com:443 2>/dev/null \ | openssl x509 -noout -text \ | grep "CA Issuers" \ | grep -oP 'http://[^ ]+') echo "AIA URL: $AIA_URL" # Step 2: download (usually DER format) curl -sL "$AIA_URL" -o issuer.der # Step 3: convert DER โ PEM openssl x509 -inform DER -in issuer.der -out issuer.pem # Step 4: inspect openssl x509 -in issuer.pem -noout -subject -issuer -dates
HOST=google.com; PORT=443 curl -sL $(echo | openssl s_client -connect $HOST:$PORT 2>/dev/null \ | openssl x509 -noout -text | grep "CA Issuers" | grep -oP 'http://[^ ]+') \ | openssl x509 -inform DER -out $HOST-root-ca.pem
TLS certificates exist in several formats. PEM is base64 text (the -----BEGIN CERTIFICATE----- format). DER is the raw binary equivalent. PKCS#7 bundles multiple certs. Most tools produce or consume PEM.
# DER โ PEM openssl x509 -inform DER -in cert.der -out cert.pem # PEM โ DER openssl x509 -outform DER -in cert.pem -out cert.der # PKCS#7 (.p7b) โ PEM (expand all certs in the bundle) openssl pkcs7 -print_certs -in bundle.p7b -out bundle.pem # Concatenate multiple PEM certs into one chain file cat intermediate.pem root.pem > ca-chain.pem # Count PEM blocks in a file grep -c "BEGIN CERTIFICATE" ca-chain.pem
After you've saved the CA cert, you can verify that a server's leaf cert was actually signed by it. This is the core of TLS trust.
# Save just the leaf cert echo | openssl s_client -connect google.com:443 2>/dev/null \ | openssl x509 > leaf.pem # Verify leaf against your saved CA chain openssl verify -CAfile google.com-ca.pem leaf.pem # โ leaf.pem: OK # Verify a live server's cert matches a saved CA (full handshake check) openssl s_client -connect google.com:443 -CAfile google.com-ca.pem -verify_return_error 2>&1 \ | grep "Verify return" # โ Verify return code: 0 (ok)
# Custom port (e.g. LDAPS, SMTPS, internal service) echo | openssl s_client -connect internal.corp:8443 -servername internal.corp 2>/dev/null # Skip cert verification (self-signed / private CA) echo | openssl s_client -connect internal.corp:443 -verify_quiet -noverify 2>/dev/null # Show full chain with -showcerts; -no_ign_eof keeps connection open briefly echo | openssl s_client -showcerts -connect example.com:443 # Force TLSv1.2 or TLSv1.3 echo | openssl s_client -tls1_2 -connect example.com:443 2>/dev/null echo | openssl s_client -tls1_3 -connect example.com:443 2>/dev/null # Check negotiated cipher suite echo | openssl s_client -connect example.com:443 2>/dev/null | grep "Cipher\|Protocol" # STARTTLS (SMTP, IMAP, etc.) echo | openssl s_client -connect mail.example.com:587 -starttls smtp 2>/dev/null
| Task | openssl | tls-ca-fetch |
|---|---|---|
| View chain | openssl s_client -showcerts โฆ | tls-ca-fetch hostname |
| Save CA cert(s) | s_client + awk pipeline | automatic |
| Identify leaf vs CA | manual grep for CA:TRUE | automatic |
| AIA root fetch | manual grep + curl + openssl convert | -fetch-root flag |
| Private / self-signed | -noverify flag | -insecure flag |
| Format conversion | full control | PEM only |
| Verify cert against CA | openssl verify | not supported |
| STARTTLS (SMTP/IMAP) | -starttls flag | not supported |
| Dependencies | openssl + awk + curl (usually pre-installed) | none โ single binary |
| Windows | needs WSL or Git Bash | .exe available |
| Scriptability | composable with shell tools | structured output |