SadServers
  • Scenarios
  • Labs
    All Labs Linux & Bash Web Servers Databases Data Processing Docker Kubernetes CI/CD Infrastructure as Code Tooling / Applications
  • Dashboard
  • Solutions
    For Individuals For Businesses
  • Ranking
  • Newsletter
  • Documentation
    FAQ Support Pro Accounts Pro+ Accounts Business Accounts Gift API CLI/TUI Privacy Troubleshooting Interviews
  • Blog
  • Pricing
  • Gift
    Gift Purchase Gift Redeem
  • About
Log In - Sign Up
  1. Labs
  2. SSL
  3. Troubleshooting

Guide

Concepts and learning path

Troubleshooting

Failure modes and fixes

Cheatsheet

Commands to keep handy

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, broad server_name _, or cert on a block that does not match SNI; dump with nginx -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-manager

Common 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.pem

openssl debugging workflow

1. Handshake and chain from client perspective

openssl s_client -connect example.com:443 -servername example.com -showcerts

2. Expiry, subject, SAN

openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

3. Verify local cert files

openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -text -noout openssl verify -CAfile chain.pem cert.pem

4. OCSP stapling

openssl s_client -status -connect example.com:443 -servername example.com # Expect: OCSP Response Status: successful

For 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

Cheatsheet →
SadServersSadServers

Real-world Linux and DevOps scenarios for hands-on learning and technical assessment.

Uptime Robot ratio (30 days)
Product
  • Scenarios
  • For Individuals
  • For Businesses
  • Pricing
Resources
  • FAQ
  • Blog
  • Newsletter
Company
  • About Us
  • Support
  • Privacy Policy
  • Terms of Service
  • Contact
Connect With Us
info@sadservers.com

Made in Canada 🇨🇦
Updated: 2026-06-13 16:06 UTC – 2d2950a