Security and identity
TLS and certificates
Chain of trust, OpenSSL commands for inspecting and issuing certificates, trust stores and the diagnosis of a failed handshake.
Cheatsheet #
| Task | Command |
|---|---|
| Inspect a live certificate | openssl s_client -connect host:443 -servername host </dev/null | openssl x509 -noout -text |
| Expiry dates | openssl x509 -in cert.pem -noout -dates |
| Subject and SANs | openssl x509 -in cert.pem -noout -subject -ext subjectAltName |
| Verify a chain | openssl verify -CAfile ca.pem -untrusted intermediate.pem cert.pem |
| Does key match cert | diff <(openssl pkey -in key.pem -pubout) <(openssl x509 -in cert.pem -noout -pubkey) |
| Read a CSR | openssl req -in req.csr -noout -text -verify |
| New key + CSR | openssl req -new -newkey rsa:2048 -nodes -keyout k.pem -out r.csr -config san.cnf |
| Self-signed for testing | openssl req -x509 -newkey ed25519 -nodes -days 30 -keyout k.pem -out c.pem -subj '/CN=test' |
| PEM to PKCS#12 | openssl pkcs12 -export -in cert.pem -inkey key.pem -certfile ca.pem -out bundle.p12 |
| PKCS#12 to PEM | openssl pkcs12 -in bundle.p12 -nodes -out all.pem |
| Show the served chain | openssl s_client -connect host:443 -showcerts </dev/null |
| Test a protocol version | openssl s_client -connect host:443 -tls1_2 |
| Fingerprint | openssl x509 -in cert.pem -noout -fingerprint -sha256 |
The chain of trust #
A certificate binds a public key to a name and is signed by an issuer. Validation walks from the server’s leaf up through intermediates to a root the client already trusts, checking signature, validity dates, name match, and allowed usage at every step.
Three things fail independently: trust (is the root in this store), chain completeness (did the server send the intermediates), and name match (does a SAN cover the hostname requested). A browser may succeed where curl fails because browsers cache intermediates from earlier sites; servers must send the full chain.
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName| Symptom | Cause |
|---|---|
unable to get local issuer certificate | Server sent an incomplete chain, or the root is not in this trust store |
certificate has expired | Leaf or an intermediate is past notAfter — check every level |
Hostname mismatch | No SAN covers the name; CN is ignored by modern clients |
self signed certificate in certificate chain | A private CA is in use and is not trusted here |
tlsv1 alert unknown ca | The server rejected the client certificate’s issuer (mTLS) |
no shared cipher | Protocol or cipher policy mismatch, or the wrong key type for the ciphersuites offered |
| Works in a browser, fails in curl | Missing intermediate that the browser had cached |
Reading certificates #
openssl x509 -in cert.pem -noout -text # everything
openssl x509 -in cert.pem -noout -subject -issuer -dates
openssl x509 -in cert.pem -noout -ext subjectAltName,keyUsage,extendedKeyUsage
openssl x509 -in cert.pem -noout -fingerprint -sha256
openssl x509 -in cert.pem -noout -checkend 604800 # expires within 7 days?
openssl crl2pkcs7 -nocrl -certfile chain.pem | openssl pkcs7 -print_certs -noout # list a bundleThe fields that matter operationally: Not After, Subject Alternative Name, Key Usage / Extended Key Usage, and Basic Constraints (a CA certificate has CA:TRUE).
Keys and CSRs #
openssl genpkey -algorithm ed25519 -out key.pem # modern, small, fast
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out key.pem
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out key.pem
chmod 600 key.pem
openssl req -new -key key.pem -out req.csr -config san.cnf
openssl req -in req.csr -noout -text -verify # confirm before sending# san.cnf — SANs are mandatory; CN alone is ignored
[req]
distinguished_name = dn
req_extensions = v3_req
prompt = no
[dn]
CN = api.example.com
O = Example Pty Ltd
C = AU
[v3_req]
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt
[alt]
DNS.1 = api.example.com
DNS.2 = api.internal.example.com
IP.1 = 10.0.0.5Confirm a key and certificate belong together before deploying:
diff <(openssl pkey -in key.pem -pubout) <(openssl x509 -in cert.pem -noout -pubkey) && echo matchA private CA #
For internal services, issue from your own CA and distribute the root — not self-signed certificates per host, which cannot be validated or revoked as a set.
# Root (offline, long-lived)
openssl req -x509 -new -newkey rsa:4096 -nodes -days 3650 -keyout ca.key -out ca.pem \
-subj '/CN=Example Internal Root CA/O=Example Pty Ltd/C=AU' \
-addext 'basicConstraints=critical,CA:TRUE,pathlen:1' -addext 'keyUsage=critical,keyCertSign,cRLSign'
# Issue a leaf from a CSR
openssl x509 -req -in req.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 90 \
-extfile san.cnf -extensions v3_req -out cert.pem
openssl verify -CAfile ca.pem cert.pem
cat cert.pem intermediate.pem > fullchain.pem # leaf first, root omittedServe fullchain.pem: leaf, then intermediates, in order, root excluded. Order matters to strict clients, and including the root only wastes handshake bytes.
Trust stores #
Each runtime has its own store; installing a CA for the system does not necessarily reach the application.
| Platform | Location | Command |
|---|---|---|
| Debian/Ubuntu | /usr/local/share/ca-certificates/*.crt | update-ca-certificates |
| RHEL/Fedora | /etc/pki/ca-trust/source/anchors/ | update-ca-trust extract |
| Alpine | /usr/local/share/ca-certificates/ | update-ca-certificates |
| curl / OpenSSL | System bundle | SSL_CERT_FILE, --cacert |
Python requests | certifi bundle | REQUESTS_CA_BUNDLE |
| Node.js | Built-in list | NODE_EXTRA_CA_CERTS |
| Java | cacerts keystore | keytool -importcert -alias x -file ca.pem -cacerts |
| Go | System store | SSL_CERT_FILE, or x509.SystemCertPool |
In a container the store must be inside the image: apk add ca-certificates or COPY ca.pem /usr/local/share/ca-certificates/ plus the update command. x509: certificate signed by unknown authority from a scratch image usually means no CA bundle at all.
Formats #
| Format | Contents | Typical extension |
|---|---|---|
| PEM | Base64 with -----BEGIN----- headers | .pem, .crt, .key |
| DER | Binary of the same structure | .der, .cer |
| PKCS#12 | Encrypted bundle of key + certificates | .p12, .pfx |
| PKCS#8 | Standard private key container | .key, PEM-wrapped |
| JKS | Java keystore (legacy) | .jks |
openssl x509 -in cert.der -inform der -out cert.pem # DER -> PEM
openssl x509 -in cert.pem -outform der -out cert.der # PEM -> DER
openssl pkcs12 -export -in cert.pem -inkey key.pem -certfile ca.pem -out bundle.p12
openssl pkcs12 -in bundle.p12 -nodes -out all.pem # extract everything
openssl rsa -in key.pem -out key.pkcs1.pem # PKCS#8 -> PKCS#1 for old software
keytool -importkeystore -srckeystore bundle.p12 -srcstoretype PKCS12 -destkeystore store.jksTesting a server #
openssl s_client -connect host:443 -servername host </dev/null # handshake detail
openssl s_client -connect host:443 -tls1_2 </dev/null # force a version
openssl s_client -connect host:443 -cipher 'ECDHE-RSA-AES128-GCM-SHA256' </dev/null
openssl s_client -connect host:443 -cert client.pem -key client.key </dev/null # mTLS
openssl s_client -connect host:443 -status </dev/null | grep -A5 'OCSP' # stapling
openssl s_client -connect host:25 -starttls smtp </dev/null # opportunistic TLS
openssl s_time -connect host:443 -new -time 10 # handshakes per second
nmap --script ssl-enum-ciphers -p 443 host # full policy view-servername sets SNI. Without it a virtual-hosted server returns its default certificate, which produces a name mismatch that does not exist in practice.
Configuration #
Support TLS 1.2 and 1.3, disable everything older, and prefer forward-secret suites. TLS 1.3 removes the cipher negotiation problem entirely — its five suites are all acceptable.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305;
ssl_session_timeout 1d;
ssl_stapling on;
add_header Strict-Transport-Security "max-age=63072000" always;HSTS is hard to undo: browsers honour max-age even after you remove the header. Start with a short value and raise it once certificates renew reliably.
Lifecycle #
Automate renewal and alert on expiry independently of the automation, because the failure mode is always “renewal silently stopped working three months ago”.
# Days remaining for a live endpoint
echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2 \
| xargs -I{} sh -c 'echo $(( ( $(date -d "{}" +%s) - $(date +%s) ) / 86400 )) days'Rotate the key at renewal rather than reusing it, keep the private key off shared storage and out of version control, and treat any key that has been emailed or pasted as compromised.
Oneliners #
# Expiry for a list of hosts
while read -r h; do d=$(echo | openssl s_client -connect "$h:443" -servername "$h" 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2); printf '%-35s %s\n' "$h" "$d"; done < hosts.txt
# Fail if a certificate expires within 30 days
openssl x509 -in cert.pem -noout -checkend 2592000 || echo 'renew now'
# Which SANs does a live certificate carry
echo | openssl s_client -connect host:443 -servername host 2>/dev/null | openssl x509 -noout -ext subjectAltName
# Chain as served, subject and issuer only
echo | openssl s_client -connect host:443 -showcerts 2>/dev/null | awk '/BEGIN CERT/,/END CERT/' | openssl crl2pkcs7 -nocrl -certfile /dev/stdin | openssl pkcs7 -print_certs -noout
# Verify a chain file offline
openssl verify -CAfile root.pem -untrusted intermediate.pem cert.pem
# Certificate from a Kubernetes secret
kubectl get secret tls-api -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -dates
# Compare deployed certificate with the file on disk
diff <(openssl x509 -in cert.pem -noout -fingerprint -sha256) <(echo | openssl s_client -connect host:443 -servername host 2>/dev/null | openssl x509 -noout -fingerprint -sha256)
# Protocol versions a server accepts
for p in tls1 tls1_1 tls1_2 tls1_3; do printf '%-8s ' "$p"; echo | openssl s_client -connect host:443 -"$p" >/dev/null 2>&1 && echo yes || echo no; done
# Generate a throwaway certificate for local testing
openssl req -x509 -newkey ed25519 -nodes -days 30 -keyout /tmp/k.pem -out /tmp/c.pem -subj '/CN=localhost' -addext 'subjectAltName=DNS:localhost,IP:127.0.0.1'
# Decode a certificate pasted as base64
base64 -d <<< "$B64" | openssl x509 -noout -text
# Serial numbers of everything in a bundle
openssl crl2pkcs7 -nocrl -certfile bundle.pem | openssl pkcs7 -print_certs | grep -E 'subject|serial'Further reading #
- Mozilla TLS configuration generator for server snippets
man openssl-s_client,man openssl-x509,man openssl-req