SSL / TLS troubleshooting
Certificate expired
Check dates:
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates.
Renew with certbot renew on Linux or check cert-manager
Certificate status in Kubernetes. Verify the auto-renew timer:
systemctl list-timers | grep certbot.
Hostname mismatch (ERR_CERT_COMMON_NAME_INVALID)
The cert's SAN list does not include the hostname the client requested.
Inspect:
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -ext subjectAltName.
Re-issue the cert with the correct -d domains in certbot.
Connecting by IP instead of hostname is a separate but related trap — see
SNI vs IP access confusion below. Wrong cert despite a valid
cert on disk often means multi-vhost or load balancer misconfiguration — see
Wrong cert served below.
SNI vs IP access confusion
A major real-world TLS failure mode — the service works by hostname but fails when clients connect to the raw IP. Certificate selection depends on SNI (the hostname the client sends during the handshake) and the server's virtual host mapping, not the TCP destination alone.
# Often fails — no hostname, wrong default cert
curl -vk https://203.0.113.10/
# Works — SNI matches the vhost cert
curl -vk https://example.com/
openssl s_client -connect 203.0.113.10:443 -servername example.com
Hitting an IP directly usually sends no SNI (or an IP in SNI,
which no public cert covers). The server falls back to its default
TLS vhost — often the wrong cert, a self-signed catch-all, or the first
listen 443 block in config. Browsers show hostname mismatch;
curl and health checks fail even though
curl https://example.com works fine.
Fix monitoring and internal clients to use the hostname (with DNS or
/etc/hosts), or pass SNI explicitly:
curl --resolve example.com:443:203.0.113.10 https://example.com/.
Do not expect a valid public cert for bare IP access unless you deliberately
issued one for that IP (rare). See nginx/Apache
server_name / vhost docs when multiple certs share one IP.
When the right cert is on disk but the wrong one is served with SNI, see
Wrong cert served (multi-vhost / load balancer) below.
Wrong cert served (multi-vhost / load balancer)
Common when several hostnames or services share one IP — the correct certificate exists on the server or in the cloud console, but clients receive a different one. Typical in Nginx, HAProxy, and AWS ALB (listener cert list, default action, or SNI routing).
Symptoms: hostname mismatch in the browser; cert on disk looks
right; certbot certificates or ACM shows the expected domain, but
the handshake presents another site's cert or a default catch-all.
# What clients get via DNS
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
# Same SNI, direct to LB IP — isolates DNS vs LB config
openssl s_client -connect 203.0.113.10:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -ext subjectAltName
Compare the two outputs — subject and SAN must match
example.com. If DNS path and LB-IP path differ, DNS may point at
the wrong tier. If both show the wrong cert, SNI routing on the LB is wrong.
- Nginx — wrong
default_server, broadserver_name _, or cert on a block that does not match SNI; dump withnginx -T | grep -E 'server_name|ssl_certificate|listen' - HAProxy — cert bound to wrong frontend, SNI map missing hostname, or TLS termination on a catch-all bind
- AWS ALB — cert not attached to the listener, older cert still on the listener, or wrong listener (443 vs 8443) / wrong load balancer
Fix the vhost or listener that handles the SNI hostname — not just replace files
on disk. Re-test with -servername after reload. Related:
SNI vs IP access confusion above when no SNI is sent at all.
Incomplete certificate chain
Server sends only the leaf cert without intermediates. Browsers may still
work (they cache intermediates); other clients fail. Compare with
openssl s_client -showcerts. Configure the server to serve
fullchain.pem (Let's Encrypt) not just cert.pem.
Private key does not match certificate
After replacing cert or key files, moduli may diverge. Compare hashes:
openssl x509 -noout -modulus -in cert.pem | openssl md5 vs
openssl rsa -noout -modulus -in privkey.pem | openssl md5.
They must match or the server will fail to start or handshakes will fail.
TLS handshake fails / connection reset
Use verbose client:
openssl s_client -connect example.com:443 -servername example.com -tls1_2.
Try different TLS versions. Check cipher mismatch, wrong port, or firewall
blocking 443. For local vhosts, SNI is required:
-servername must match the configured cert — see
SNI vs IP access confusion when testing by IP. Slow handshakes may
also trace to missing OCSP stapling — see below. When only some clients fail,
see Cipher / protocol mismatch (legacy clients) below.
Cipher / protocol mismatch (legacy clients)
Handshake succeeds for modern browsers but fails for a subset of clients — often after hardening TLS (TLS 1.3-only, strict cipher lists, disabling SHA-1 or RSA key exchange). Common affected clients: old Java runtimes, old OpenSSL versions, embedded devices, and legacy integration jobs that cannot negotiate current protocols or ciphers.
Symptoms: handshake failure only for some clients; curl from a current machine works; monitoring or partner APIs fail with SSL errors while the public site looks fine.
# Test each protocol the client might use
openssl s_client -connect example.com:443 -servername example.com -tls1_2
openssl s_client -connect example.com:443 -servername example.com -tls1_1
openssl s_client -connect example.com:443 -servername example.com -tls1
# Verbose — see negotiated cipher and failure reason
openssl s_client -connect example.com:443 -servername example.com -tls1_2 -state -msg
If -tls1_2 fails but default (TLS 1.3) succeeds, the server or load
balancer may not offer TLS 1.2 or shares no cipher with the legacy client. Check
ssl_protocols / ssl_ciphers (nginx), HAProxy
ssl-min-ver / cipher suites, or ALB security policy (e.g.
ELBSecurityPolicy-TLS-1-2-Ext-2018-06 vs stricter policies).
Fix is a tradeoff: enable TLS 1.2 and compatible ciphers for legacy peers, or upgrade the client. Avoid re-enabling TLS 1.0/1.1 or weak ciphers on the public internet unless you accept the security risk — prefer a dedicated legacy endpoint or VPN for old integrations.
OCSP stapling
OCSP stapling lets the server attach a fresh revocation check (OCSP response) during the TLS handshake. Without it, clients fetch revocation status from the CA themselves — extra latency on every connection. Missing or broken stapling causes slow handshakes; strict clients may fail the handshake when stapling is misconfigured or the stapled response is stale.
openssl s_client -status -connect example.com:443 -servername example.com
Look for OCSP Response Status: successful and a stapled
response block in the output. If you see no OCSP response sent,
stapling is off or the server could not fetch a valid response from the CA.
# nginx — stapling needs full chain + trusted issuer certs
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
Common fixes: serve fullchain.pem, point
ssl_trusted_certificate at the issuer chain, ensure outbound HTTPS
to the CA OCSP responder is allowed, and reload after renewal (stapled responses
expire). See openssl debugging workflow at the end of this page —
step 4 checks stapling from the client side.
Certbot renewal or issuance fails
HTTP-01 needs port 80 reachable and a valid webroot or nginx plugin config.
DNS-01 needs correct TXT records (useful behind firewalls). Test with
certbot certonly --dry-run. Read logs:
/var/log/letsencrypt/letsencrypt.log. Rate limits apply —
use staging for testing: --staging.
Certificate transparency and crt.sh
Public CAs log every issued certificate to Certificate Transparency
(CT) logs. That is separate from what
openssl s_client shows on the wire today — CT reveals
what was issued, including history, wildcards, and certs you
did not expect.
crt.sh is the standard search interface for CT logs. Useful when diagnosing:
- Issuance history — when certs were created and by which CA
- Unexpected certificates — shadow or forgotten subdomains, old staging certs, unauthorized issuances
- Verify what was actually issued — compare SAN list in CT to what you intended in certbot or cert-manager
# Web UI
https://crt.sh/?q=example.com
# JSON from the shell
curl -s "https://crt.sh/?q=%25.example.com&output=json" | jq '.[].name_value' -r | sort -u
CT complements the openssl workflow below: s_client shows the cert
the server presents now; crt.sh shows what has been publicly logged for the
domain. Not every internal or self-signed cert appears in CT — only those
issued by participating public CAs.
cert-manager on Kubernetes
When ingress TLS fails, trace the cert-manager resources:
kubectl describe ingress -n cert-manager
kubectl describe certificate -n myns my-tls
kubectl describe challenge -n myns -l "cert-manager.io/certificate-name=my-tls"
kubectl logs -n cert-manager deploy/cert-managerCommon issues: ClusterIssuer not ready, HTTP-01 solver ingress not reachable, DNS-01 credentials wrong, or certificate stuck in "Issuing" due to rate limits.
Self-signed or internal CA trust
Test without browser trust store:
# Skip verification (debug only)
openssl s_client -connect internal.local:443 -servername internal.local
# Verify against your CA
openssl s_client -connect internal.local:443 -CAfile /etc/ssl/internal-ca.pemopenssl debugging workflow
1. Handshake and chain from client perspective
openssl s_client -connect example.com:443 -servername example.com -showcerts2. Expiry, subject, SAN
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName3. Verify local cert files
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -text -noout
openssl verify -CAfile chain.pem cert.pem4. OCSP stapling
openssl s_client -status -connect example.com:443 -servername example.com
# Expect: OCSP Response Status: successfulFor issuance history and unexpected certs on a domain, see Certificate transparency and crt.sh above.
Practice scenarios
Hands-on SSL scenarios on live Linux VMs: ssl