SSL Handshake Failed: Diagnose and Fix TLS Errors for Every Server
You just deployed a new certificate, everything looks fine in the dashboard, and then your monitoring lights up: "SSL handshake failed." Or maybe a customer reports they cannot access your site. Or your API integration suddenly stops working with a cryptic TLS error. Whatever brought you here, this guide will walk you through every common cause and give you the exact commands to fix it.
TL;DR: Run openssl s_client -connect domain.com:443 -servername domain.com to see the exact handshake error. Most common causes: expired certificate, wrong domain on the certificate, or TLS version mismatch between client and server.
What Happens During an SSL/TLS Handshake
Before we start troubleshooting, it helps to understand what actually happens when a client connects to your server over HTTPS. The handshake is a four-step negotiation that happens in milliseconds, and any of these steps can fail.
- ClientHello: The client (browser, curl, API client) sends a message listing the TLS versions it supports, the cipher suites it can use, and the hostname it wants to reach (via SNI). Think of it as the client saying "here is what I can work with."
- ServerHello: The server picks the highest TLS version and strongest cipher suite that both sides support, and sends that choice back. If the server cannot find any overlap with what the client offered, the handshake fails right here.
- Certificate Exchange: The server sends its SSL certificate (and the intermediate certificates forming the chain up to a trusted root CA). The client verifies the certificate is valid, not expired, covers the requested domain, and chains to a trusted root.
- Key Exchange: Both sides agree on a shared secret using asymmetric cryptography (typically ECDHE in modern TLS). This shared secret is used to derive symmetric encryption keys for the actual data transfer.
If anything goes wrong at any step, you get an "SSL handshake failed" error. The good news is that openssl s_client will tell you exactly which step broke and why.
Step 1: Diagnose with openssl s_client
This is your single most important debugging tool. Every other step in this guide starts here. Run this command and read the output carefully:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com
The -servername flag sends the SNI extension, which is critical if your server hosts multiple domains on one IP. Without it, you might get the wrong certificate back and waste time debugging the wrong problem.
Here is what to look for in the output:
- Certificate chain: Shows every certificate from leaf to root. If you see only one certificate and the issuer is not a root CA, you are missing intermediates.
- Verify return code: This is the money line.
Verify return code: 0 (ok)means the chain is valid. Anything else tells you exactly what is wrong. - Protocol: Shows which TLS version was negotiated (e.g.,
TLSv1.3orTLSv1.2). - Cipher: Shows the agreed cipher suite (e.g.,
TLS_AES_256_GCM_SHA384).
Common verify return codes you will encounter:
10- Certificate has expired18- Self-signed certificate19- Self-signed certificate in chain20- Unable to get local issuer certificate (missing intermediate)21- Unable to verify the first certificate (incomplete chain)62- Hostname mismatch
Write down the verify return code. It points you directly to the right section below.
Step 2: Certificate Expired
This is the number one cause of SSL handshake failures, and it is almost always preventable. Certificates expire, and if you do not have automated renewal set up, it is only a matter of time before one catches you off guard.
Check when your certificate expires:
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -dates
You will see output like:
notBefore=Jan 15 00:00:00 2026 GMT
notAfter=Apr 15 23:59:59 2026 GMT
If notAfter is in the past, your certificate has expired. Here is how to fix it depending on your setup:
Let's Encrypt / Certbot
# Test renewal first (dry run)
sudo certbot renew --dry-run
# If dry run passes, renew for real
sudo certbot renew
# Restart your web server to pick up the new cert
sudo systemctl reload nginx # or apache2, or httpd
Set up a cron job so this never happens again:
# Add to crontab - runs twice daily, only renews if needed
0 0,12 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
AWS Certificate Manager (ACM)
ACM certificates renew automatically as long as the domain validation is still working. If auto-renewal failed, check your DNS CNAME validation records or email validation. Go to ACM in the AWS console, find the certificate, and check the renewal status. If the CNAME record was removed, re-add it and ACM will pick it up automatically.
For more details on renewal failures, see our guide on fixing Let's Encrypt renewal issues.
Step 3: Certificate Domain Mismatch
Your certificate is valid and not expired, but the browser still rejects it. The most likely cause: the domain on the certificate does not match the domain the client is requesting.
Check what domains your certificate covers:
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
You will see something like:
X509v3 Subject Alternative Name:
DNS:example.com, DNS:www.example.com
The certificate must list the exact domain the client is connecting to. Here are the common gotchas:
- www vs non-www: A certificate for
example.comdoes not coverwww.example.comand vice versa. You need both listed in the SAN (Subject Alternative Name) field, or use a wildcard. - Wildcard limitations: A wildcard certificate for
*.example.comcoverswww.example.comandapi.example.com, but it does not cover the bare domainexample.comor nested subdomains likeapp.staging.example.com. You typically need the wildcard plus the bare domain in the SAN. - CN vs SAN: Modern browsers ignore the Common Name (CN) field entirely and only check the Subject Alternative Name (SAN) extension. If your certificate has the domain in CN but not in SAN, it will fail. This mostly affects very old or manually generated certificates.
The fix is to reissue your certificate with all the domains you need listed in the SAN field. With certbot:
sudo certbot certonly --nginx -d example.com -d www.example.com
Step 4: Incomplete Certificate Chain
Your leaf certificate is valid and covers the right domain, but the client still cannot verify it. The problem is almost always a missing intermediate certificate. The client needs the full chain from your certificate to a trusted root CA, and if any link in that chain is missing, verification fails.
Check your chain with openssl:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -showcerts
Count the certificates in the output (each one starts with -----BEGIN CERTIFICATE-----). You should see at least two: your leaf certificate and the intermediate. If you only see one, your chain is incomplete.
To fix this, you need to build a fullchain file that includes your certificate plus the intermediate(s):
# Concatenate your cert + intermediate(s) into one file
cat your-domain.crt intermediate.crt > fullchain.crt
# Nginx
ssl_certificate /etc/ssl/certs/fullchain.crt;
ssl_certificate_key /etc/ssl/private/your-domain.key;
# Apache
SSLCertificateFile /etc/ssl/certs/fullchain.crt
SSLCertificateKeyFile /etc/ssl/private/your-domain.key
Where do you get the intermediate certificate? Your CA provides it. You can also download it from the Authority Information Access (AIA) URL embedded in your certificate. Use our Certificate Decoder to extract the AIA URL from your certificate.
For a deeper walkthrough on chain issues, check out our article on fixing incomplete certificate chains.
A subtle trap: some browsers cache intermediate certificates from previous visits, so the site works fine for you but fails for new visitors. Always test with openssl, not just your browser, because openssl does not cache anything.
Step 5: TLS Version Mismatch
The client and server cannot agree on a TLS protocol version. This happens in two scenarios: the server only supports older protocols that the client has dropped, or the server requires a newer protocol that the client does not support.
Test which TLS versions your server supports:
# Test TLS 1.2
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -tls1_2
# Test TLS 1.3
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -tls1_3
# Test TLS 1.1 (should fail on modern servers)
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -tls1_1
# Test TLS 1.0 (should also fail)
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -tls1
If TLS 1.2 and 1.3 both fail, your server configuration has a serious problem. If only 1.0 and 1.1 work, your server is using deprecated protocols and modern clients will refuse to connect.
Here is how to configure your server for TLS 1.2 and 1.3 only:
# Nginx
ssl_protocols TLSv1.2 TLSv1.3;
# Apache
SSLProtocol -all +TLSv1.2 +TLSv1.3
Restart your web server after making changes. If you have old clients (embedded devices, legacy systems, old Java applications) that only support TLS 1.0 or 1.1, you have a tough choice. Keeping those protocols enabled puts all your users at risk. The better approach is to set up a separate endpoint for legacy clients with stricter access controls, and keep your main site on modern TLS only.
Step 6: Cipher Suite Incompatibility
Even if both sides agree on the TLS version, they also need to agree on a cipher suite. If the server only offers ciphers that the client does not support (or vice versa), the handshake fails with a "no shared cipher" error.
Check which ciphers your server supports:
nmap --script ssl-enum-ciphers -p 443 yourdomain.com
Or test a specific cipher with openssl:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -cipher ECDHE-RSA-AES256-GCM-SHA384
The easiest way to get your cipher configuration right is to use the Mozilla SSL Configuration Generator at ssl-config.mozilla.org. It gives you three profiles:
- Modern: TLS 1.3 only. Maximum security, but excludes older clients. Good for APIs and internal services.
- Intermediate: TLS 1.2 and 1.3. The recommended default for most websites. Balances security with broad compatibility.
- Old: TLS 1.0 through 1.3. Only use this if you absolutely must support ancient clients. Strongly discouraged.
Select your web server (Nginx, Apache, HAProxy, etc.), pick "Intermediate," and copy the generated configuration directly into your server config. Zero guesswork.
Pro Tip: Use the Mozilla SSL Configuration Generator at ssl-config.mozilla.org to get the exact cipher config for your server and security level. Select your server software, pick your compatibility profile, and copy-paste the config. It is maintained by Mozilla's security team and updated regularly.
Step 7: SNI Not Supported
Server Name Indication (SNI) is a TLS extension that lets the client tell the server which hostname it wants during the handshake, before the certificate is sent. This allows multiple HTTPS sites to share a single IP address.
If your client does not send the SNI extension, the server does not know which certificate to present. It will either send a default certificate (which probably does not match your domain) or refuse the connection entirely.
Clients that do not support SNI are rare today, but you might hit this with:
- Very old versions of curl (pre-7.18.1) or Python (pre-2.7.9 / pre-3.2)
- Old Java versions (pre-Java 7) and some embedded HTTP clients
- Custom scripts that use low-level socket connections without setting the SNI hostname
- Load balancers or proxies that strip or do not forward the SNI extension
Test whether SNI is the problem by comparing with and without the -servername flag:
# With SNI (correct)
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com
# Without SNI (simulates old client)
openssl s_client -connect yourdomain.com:443
If you get different certificates, SNI is required and your client is not sending it. Fix the client to send SNI, or if that is not possible, give the site a dedicated IP address so the server does not need SNI to select the right certificate.
Step 8: Self-Signed Certificate Errors
Self-signed certificates are not signed by a trusted Certificate Authority, so clients reject them by default. This is expected and correct behavior. If you see verify return code 18 (self-signed certificate) or 19 (self-signed certificate in chain), you have two options depending on your use case.
For Development and Testing
You can bypass certificate verification for testing, but never in production:
# curl - skip verification
curl -k https://localhost:8443/api/health
# Python requests
import requests
requests.get('https://localhost:8443', verify=False)
# Node.js (set env var)
NODE_TLS_REJECT_UNAUTHORIZED=0 node app.js
A better approach for local development is to use a tool like mkcert that creates locally-trusted certificates. It installs a local CA into your system trust store, so your browser and tools trust the certificates without any flags:
# Install mkcert
brew install mkcert # macOS
mkcert -install # Install local CA
# Create cert for local dev
mkcert localhost 127.0.0.1 ::1
For Internal Services in Production
If you use self-signed or internal CA certificates for service-to-service communication, add the CA certificate to the client's trust store rather than disabling verification:
# Linux (Debian/Ubuntu)
sudo cp internal-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
# Linux (RHEL/CentOS)
sudo cp internal-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust
# curl with custom CA
curl --cacert /path/to/internal-ca.crt https://internal-service.local
Never use --insecure, verify=False, or NODE_TLS_REJECT_UNAUTHORIZED=0 in production. It completely disables certificate verification and makes you vulnerable to man-in-the-middle attacks.
TLS Version Comparison
Here is a quick reference for understanding TLS versions, their security posture, and what you should be running:
| Version | Security Level | Browser Support | Performance | Recommendation |
|---|---|---|---|---|
| TLS 1.0 | Broken (BEAST, POODLE) | Dropped by all major browsers | Slow (full handshake) | Disable immediately |
| TLS 1.1 | Weak (deprecated RFC 8996) | Dropped by all major browsers | Slow (full handshake) | Disable immediately |
| TLS 1.2 | Strong (with modern ciphers) | Universal (99%+ coverage) | Good (session resumption) | Minimum required version |
| TLS 1.3 | Strongest (no legacy ciphers) | All modern browsers (95%+) | Fastest (0-RTT, 1-RTT handshake) | Preferred, enable alongside 1.2 |
The performance difference between TLS 1.2 and 1.3 is significant. TLS 1.3 reduces the handshake from two round trips to one (and zero for resumed connections with 0-RTT). If you serve a high-traffic site, enabling TLS 1.3 reduces latency on every single connection.
Cloudflare SSL Handshake Errors (525 and 526)
If you use Cloudflare as a reverse proxy, you might encounter two specific error codes that look like handshake failures but have a slightly different root cause. These errors happen between Cloudflare and your origin server, not between the visitor and Cloudflare.
Error 525: SSL Handshake Failed
Cloudflare could not complete the TLS handshake with your origin. Common causes:
- Your origin server does not have a valid SSL certificate installed
- Your origin does not support TLS 1.2 or higher (Cloudflare requires at least TLS 1.2 for origin connections)
- Port 443 is not open or is firewalled on your origin
- Your origin is using a cipher suite that Cloudflare does not support
Debug it by connecting directly to your origin IP, bypassing Cloudflare:
openssl s_client -connect YOUR_ORIGIN_IP:443 -servername yourdomain.com
Error 526: Invalid SSL Certificate
The handshake completed, but Cloudflare rejected the certificate because it is not trusted. This happens when you use Full (Strict) SSL mode and your origin has a self-signed or expired certificate. Fix options:
- Install a free Cloudflare Origin CA certificate on your origin (trusted by Cloudflare but not browsers, which is fine since Cloudflare terminates the public-facing TLS)
- Install a certificate from a public CA (Let's Encrypt, DigiCert, etc.)
- Switch to Full (not Strict) SSL mode in Cloudflare, though this is less secure since it does not validate the origin certificate
The key thing to understand: 525 means the handshake itself failed (protocol or cipher issue), while 526 means the handshake worked but the certificate was rejected (trust issue). Debug them differently.
Check Your SSL Configuration
Run a free SSL check on your domain to verify your certificate, chain, and TLS configuration. Instant results with detailed remediation steps.
Check Your SSL FreeCommon Mistakes That Cause Handshake Failures
After debugging hundreds of SSL issues, these are the mistakes I see over and over:
- Using a self-signed certificate in production. It works in your browser because you added an exception, but every other client rejects it. Use Let's Encrypt. It is free and takes five minutes.
- Forgetting to restart the web server after a certificate change. You renewed the certificate, the file on disk is correct, but the server is still using the old certificate loaded in memory. Always reload or restart after any certificate change:
sudo systemctl reload nginx. - Mixed cipher configurations across load-balanced servers. Server A supports modern ciphers, server B has a different config with weaker or incompatible ciphers. Depending on which server handles the request, the handshake may or may not succeed. Use configuration management (Ansible, Puppet, Chef) to ensure consistent TLS config across all servers.
- Copying the certificate without the intermediate. Your CA gave you three files: the cert, the intermediate, and the root. You only installed the cert. The chain is broken for any client that does not have the intermediate cached.
- Redirecting HTTP to HTTPS without a valid certificate. You set up the redirect, but the certificate is expired or for the wrong domain. The user gets the SSL error before the redirect can even happen. Fix the certificate first, then set up the redirect.
- Testing with the wrong hostname. You test with
https://192.168.1.50but the certificate is issued forapp.internal.com. Of course it fails. The certificate must match the hostname in the URL, not the IP address (unless the IP is listed in the SAN, which is unusual).
Frequently Asked Questions
Why does my SSL handshake fail even though my certificate is valid?
A valid certificate alone does not guarantee a successful handshake. The server also needs a complete certificate chain (including intermediates), compatible TLS protocol versions, and matching cipher suites. Use openssl s_client to check each of these. The most common cause is a missing intermediate certificate, which makes the chain incomplete even though the leaf certificate itself is perfectly valid.
How do I fix Cloudflare error 525 SSL handshake failed?
Error 525 means Cloudflare could not complete the SSL handshake with your origin server. Check that your origin has a valid SSL certificate installed, that it supports TLS 1.2 or higher, and that port 443 is open. If you use Full (Strict) SSL mode, your origin certificate must be trusted by Cloudflare. Install a Cloudflare Origin CA certificate or a certificate from a public CA. Test directly against your origin IP with openssl s_client -connect YOUR_ORIGIN_IP:443 -servername yourdomain.com to isolate the issue from Cloudflare.
Is TLS 1.0 or 1.1 still safe to use?
No. TLS 1.0 and 1.1 are officially deprecated as of RFC 8996 (March 2021). They contain known vulnerabilities including BEAST, POODLE, and padding oracle attacks. All major browsers have dropped support as of 2020. You should disable TLS 1.0 and 1.1 on your server and require TLS 1.2 at minimum, with TLS 1.3 preferred for best security and performance. If you have legacy clients that only support older protocols, isolate them behind a dedicated endpoint with strict access controls.
Decode and Inspect Any Certificate
Paste your PEM-encoded certificate into our Certificate Decoder to see every field: SAN, issuer, expiry, key algorithm, and chain validation. Free, instant, no signup.
Decode a CertificateThe Bottom Line
SSL handshake failures look scary, but they almost always come down to one of eight causes: expired cert, domain mismatch, incomplete chain, TLS version conflict, cipher mismatch, missing SNI, self-signed cert, or a Cloudflare origin issue. Start every investigation with openssl s_client. Read the verify return code. It will point you to the exact problem in seconds.
Once you fix the immediate issue, set up automated certificate renewal, use the Mozilla SSL Configuration Generator for your cipher config, and test your setup regularly with our SSL Checker. SSL problems are one of the few categories where the right tooling can eliminate the problem entirely.
Related tools: SSL Checker, Certificate Decoder, Exposure Checker. Related guides: Fix Incomplete Certificate Chains, Fix Let's Encrypt Renewal Failures.
Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.