What Is an SSL Error and How to Fix It
An SSL error is a TLS handshake or certificate failure, the browser refuses the connection before any HTTP request is sent. This guide decodes the ten messages you actually see in Chrome, Safari, Firefox, and Edge, shows the openssl commands I run first, and walks through the nginx, Caddy, Apache, and AWS ACM fixes that clear them for good.
I have been running TLS termination on Hetzner bare metal for years, which means I have seen every certbot edge case, every missing intermediate, every hostname mismatch, and plenty of genuinely exotic handshake failures. SSL errors are the error class with the most precise diagnostics available (openssl gives you the actual handshake bytes) and the class people are most scared to touch, because it involves crypto and browser trust stores and the word “certificate.” It does not need to be scary.
Every SSL error you are likely to encounter falls into one of ten specific failure modes. Once you can name the failure mode from the error code, the fix is usually a single config change or a single cert reissue. In this guide I will walk you through all ten, show you the openssl commands I reach for first, and cover the server-specific gotchas for nginx, Caddy, Apache, and AWS ACM.
1. What an SSL Error Actually Means
When you open https://example.com, two things happen before any HTTP request is sent. First, the browser and server perform a TLS handshake: exchange supported protocol versions, agree on a cipher, and exchange key material. Second, the browser validates the server’s certificate: is it signed by a trusted CA, is it within its validity window, does the hostname match, is it revoked, is the signature algorithm acceptable. Only if both of those succeed does the browser send a single byte of HTTP.
An “SSL error” means one of those two phases failed. The browser does not get to the HTTP layer, which is why refreshing the page does nothing and why there is no status code. What you see instead is a browser interstitial, typically titled “Your connection is not private” or “This Connection Is Not Private,” with a specific error code underneath that points to the exact failure.
Two notes on terminology. First, “SSL” and “TLS” are the same thing for practical purposes. SSL 2.0 and 3.0 are dead and disabled. Everything labeled SSL in a browser, in certbot, or in your vendor’s dashboard is really TLS 1.2 or TLS 1.3. Second, the error is almost always on the server side. A visitor gets very few levers to pull, which is one of the themes of this guide.
If you want a deeper dive into how the chain of trust itself is structured, the SSL certificate chain guide covers that ground. Here we focus on what goes wrong and how to repair it.
2. The Ten Most Common SSL Errors, Decoded
Every SSL error I have ever debugged in production fell into one of these ten buckets. The icon, the raw error code, and the one-line diagnosis are the pattern I use to triage in about ten seconds.
Protocol or cipher mismatch
ERR_SSL_PROTOCOL_ERROR
Client and server could not agree on a TLS version or cipher, or the server sent malformed handshake bytes. Usually a stale cipher list or TLS 1.0/1.1-only origin behind a modern browser.
Unknown or self-signed issuer
NET::ERR_CERT_AUTHORITY_INVALID
The chain terminates at a CA the browser does not trust. Self-signed certs, internal CAs, and missing intermediate certificates all land here. The fix is almost always: send the intermediate.
Expired or not yet valid
NET::ERR_CERT_DATE_INVALID
The leaf, an intermediate, or the client clock is outside the cert’s validity window. Check both the cert expiry and the system clock on the client, wrong clocks are the most under-diagnosed cause.
Hostname mismatch
NET::ERR_CERT_COMMON_NAME_INVALID
The hostname in the URL is not in the certificate’s Subject Alternative Name list. Common after a domain rename, a vhost misconfig, or hitting a server by IP when the cert is bound to a name.
Certificate revoked
NET::ERR_CERT_REVOKED
The CA has marked this specific cert as revoked via OCSP or CRL, usually because the private key was compromised or mis-issued. You cannot ignore this one. Reissue the cert.
Weak signature (SHA-1, RSA-1024)
NET::ERR_CERT_WEAK_SIGNATURE_ALGORITHM
The cert was signed with an algorithm the browser no longer accepts (SHA-1 has been dead since 2017, RSA under 2048 bits is also rejected). Reissue with SHA-256 and at least RSA-2048 or ECDSA P-256.
Version or cipher suite mismatch
ERR_SSL_VERSION_OR_CIPHER_MISMATCH
The server supports no TLS version the client will use, or no common cipher. Modern Chrome disables TLS 1.0 and 1.1 entirely. Enable TLS 1.2 and 1.3 and drop RC4, 3DES, and EXPORT ciphers.
SNI mismatch
ERR_SSL_UNRECOGNIZED_NAME_ALERT
The client sent an SNI hostname the server did not recognise. Common on multi-tenant origins or CDNs where the default vhost has no matching cert. Confirm the SNI value with openssl -servername.
Chrome generic TLS failure
Your connection to this site is not secure
Chrome’s catch-all phrasing when the connection completed but the cert is not fully trusted for the origin. Click Advanced to see the specific NET::ERR_CERT_* code underneath, that is the real diagnosis.
Mixed content on an HTTPS page
MIXED_CONTENT
Page loaded over HTTPS but references an http:// subresource (image, script, CSS). Modern browsers block active mixed content (JS, CSS) outright and warn on passive. Fix with protocol-relative or https:// URLs and a Content-Security-Policy upgrade-insecure-requests directive.
3. For Visitors: What You Can Actually Do
Honest answer: not much. An SSL error is a server-side problem in 95% of cases. Before you give up or report the site, try these in order:
- Check your system clock. A wrong date is the single most common cause of certificate errors on personal devices, especially laptops whose CMOS battery has drained. Certificates are valid only within a specific time window. If your clock is off by even an hour, valid certs look expired (or not-yet-valid) and the browser correctly refuses them. Set your clock to automatic.
- Try a different network. Switch from WiFi to cellular. Hotel, airport, school, and corporate networks often perform TLS interception, substituting their own certificate so they can inspect traffic. If the site loads on cellular but not on WiFi, the WiFi is the problem, not the site.
- Update your browser. A Chrome or Safari from 2019 will reject certificates chained to roots that were added to the trust store after that version shipped. Update to the current release. This is also the fix when a browser starts rejecting a site that used to work yesterday because of a root expiry event (DST Root CA X3 in 2021 took out millions of old devices for exactly this reason).
- Confirm it is not just you. Open our website checker or any independent “is-it-down” service and enter the URL. If the checker also reports a TLS failure, the cert is genuinely broken on the server. If the checker shows the site as up, the problem is local to your device or network.
- Report it to the site owner. Most small sites do not have external TLS monitoring. A quick email or social post reaches the owner faster than their own alerts would. Include the exact error code (NET::ERR_CERT_DATE_INVALID etc.), it narrows the fix to seconds.
What will not help: reinstalling your browser, running antivirus, or switching DNS servers. None of those touch the TLS handshake with the origin server, and all of them waste time.
4. For Site Owners: Diagnose From the Cert Outward
The rule that saves the most time: diagnose SSL errors starting at the certificate, then the chain, then the handshake, then the server config. Work outward from the smallest, most concrete artifact first. Ninety percent of the time you never need to look past the cert itself.
Step 1: Read the cert directly
The fastest command I know for “is the cert valid, for the right name, within its window”:
# Pull the cert the server is actually serving
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com \
</dev/null 2>/dev/null | openssl x509 -noout -dates -subject -issuer
# Output:
# notBefore=Feb 14 00:00:00 2026 GMT
# notAfter=May 15 23:59:59 2026 GMT
# subject=CN=yourdomain.com
# issuer=CN=R11, O=Let's Encrypt, C=USCheck the notAfter date (expired is the most common single failure), the subject CN and SAN list (must include the hostname you are serving), and the issuer (should be a public CA, not your own name).
Step 2: Verify the chain, not just the leaf
Browsers need the leaf and the intermediate. The root is already in their trust store. If your server only sends the leaf, Chrome and some mobile clients will fail. Check the depth count:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com \
-showcerts </dev/null 2>/dev/null | grep -E "^(depth|verify)"
# depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
# depth=1 C = US, O = Let's Encrypt, CN = R11
# depth=0 CN = yourdomain.com
# verify return:1If you only see depth=0, your server is not sending the intermediate and you need to concatenate it into your fullchain file. The certificate chain guide covers the exact server configs for nginx, Apache, and Caddy.
Step 3: Confirm SNI resolves to the right vhost
If your origin hosts multiple domains behind the same IP, the client sends the hostname in the TLS SNI extension and the server uses it to pick the right certificate. When SNI routing breaks, you get the wrong cert and a name-mismatch error:
# With SNI (normal browser behavior)
openssl s_client -servername yourdomain.com -connect yourdomain.com:443 \
</dev/null 2>/dev/null | openssl x509 -noout -subject
# Without SNI (tests the default vhost)
openssl s_client -connect yourdomain.com:443 \
</dev/null 2>/dev/null | openssl x509 -noout -subjectIf those two commands return different subjects, your SNI-less default vhost is something you probably forgot about. Older clients and some internal tools do not send SNI. Either add a default cert that covers everything, or return 421 Misdirected Request.
Step 4: Check the renewal pipeline
A cert that was valid yesterday and broken today almost always means an automated renewal failed silently. The three places I look first:
# Let's Encrypt via certbot
sudo tail -50 /var/log/letsencrypt/letsencrypt.log
# Caddy auto-HTTPS
sudo journalctl -u caddy --since "24 hours ago" | grep -i "acme\|certificate"
# systemd timer status (for cron-based renewals)
sudo systemctl list-timers --all | grep -i "certbot\|renew"The first ERROR line in the logs tells you what broke: ACME challenge unreachable, rate limit tripped, DNS provider API failing, or a stale web root path left over from a deploy. Let’s Encrypt’s duplicate-cert rate limit (5 identical cert requests per FQDN set per week) has burned me more than once on a deploy loop.
Step 5: Reload the web server, do not just replace the file
A renewed cert on disk does nothing until your web server reloads. The quietest production outages I have had were exactly this: new cert in the right path, process still serving the old one because nobody sent a SIGHUP:
# nginx: graceful reload, does NOT drop connections
sudo nginx -t && sudo nginx -s reload
# Apache
sudo apachectl configtest && sudo systemctl reload apache2
# Caddy reloads automatically on cert rotation, but if you edited Caddyfile:
sudo caddy reload --config /etc/caddy/CaddyfileReload, not restart. A restart drops in-flight connections and disrupts healthy endpoints. A reload is what you want in 99% of cases.
Step 6: Check cipher and protocol compatibility
If the error is ERR_SSL_VERSION_OR_CIPHER_MISMATCH the cert is fine, the negotiation failed. Enumerate what the server offers:
# Fast: just the supported versions
nmap --script ssl-enum-ciphers -p 443 yourdomain.com
# Deep: testssl.sh gives grades and a long report
testssl.sh --severity HIGH yourdomain.comIn 2026, your server should offer TLS 1.2 and TLS 1.3 and nothing else. Disable TLS 1.0 and 1.1. Drop RC4, 3DES, and EXPORT ciphers. If your upstream terminates TLS in front of nginx (Cloudflare, an ALB, a WAF), any of these checks points at the frontmost TLS endpoint, which is usually what you want to fix first.
5. Browser and Server Error Phrasing
The same underlying cert failure shows up with different wording in each browser and each server. Matching the phrasing to the real cause saves a surprising amount of time.
| Platform | Error Message | Real Cause & Fix |
|---|---|---|
| Chrome | NET::ERR_CERT_* | Click Advanced to see the exact error subtype. Chrome is the most verbose of the major browsers about the underlying cause, which makes it the fastest path to the real failure mode. |
| Safari | This Connection Is Not Private | Safari hides the specific reason unless you open Console.app during page load. Look for CFNetwork "kCFStreamErrorDomainSSL" entries, they map one-to-one with Chrome’s NET::ERR_CERT_* codes. |
| Firefox | SEC_ERROR_UNKNOWN_ISSUER | Firefox uses its own bundled Mozilla trust store (not the OS one). A cert trusted on Chrome/Windows can still fail on Firefox if the CA was removed from Mozilla’s root program. Check via about:certificate. |
| Edge | DLG_FLAGS_INVALID_CA | Edge uses the Windows Certificate Store, so a cert that works here but fails on macOS/iOS usually means a Microsoft-only root is signing the chain. Reissue with a public CA if you need cross-platform trust. |
| nginx | SSL_do_handshake() failed | Check ssl_certificate points to a fullchain file (leaf + intermediate concatenated) and ssl_certificate_key points to the matching private key. After replacing files on disk, run "nginx -s reload", not "restart". |
| Caddy | tls: no certificates configured | Caddy handles ACME automatically in almost every case. When it fails, check the Caddy log for the actual ACME error (rate limit, DNS challenge, port 80 unreachable). Caddyfile global "auto_https disable_certs" can silently break things, do not enable it. |
| Apache | AH02032: Hostname provided via SNI and hostname provided via HTTP have no compatible SSL setup | The SNI value and the Host header landed on different vhosts with different certificates. Consolidate the vhost blocks or add SSLStrictSNIVHostCheck off as a debugging step only. |
| AWS ACM / ALB | Target group SSL handshake failed | ACM cert bound to the listener is fine, but the target group’s health check is using HTTPS against a backend with a self-signed or expired cert. Switch the target group to HTTP between ALB and origin if the hop is inside the VPC, or put a valid cert on the origin. |
Firefox-specific gotcha: Firefox ships its own Mozilla trust store, independent of macOS and Windows. A cert that validates cleanly in Chrome on a Mac can still fail in Firefox on the same Mac if the root was removed from the Mozilla program. Open about:certificate in Firefox to see exactly which cert the browser received and where the chain breaks.
6. Diagnosing With openssl, curl, and testssl.sh
Three commands cover 90% of SSL debugging. The first is the handshake dump:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com
# Look at:
# - "Verify return code: 0 (ok)" good
# - "Verify return code: 10 (cert has expired)"
# - "Verify return code: 18 (self-signed)"
# - "Verify return code: 20 (unable to get local issuer)" missing intermediate
# - "Verify return code: 21 (unable to verify the first certificate)"The second is curl, which fails fast and gives you a readable error:
curl -vI https://yourdomain.com/
# Useful flags:
# --resolve yourdomain.com:443:<origin-ip> bypass CDN, test origin directly
# --cacert /path/to/ca.pem test against a custom CA
# -k ignore errors (debugging only, never in prod)The third is testssl.sh, which enumerates versions and ciphers and grades the server:
# Full report (takes 2-3 minutes)
testssl.sh yourdomain.com
# Fast subset
testssl.sh --fast yourdomain.com
# Only severity HIGH+
testssl.sh --severity HIGH yourdomain.comBetween those three you will nail down the root cause in almost every case. For a faster single-purpose check, our free SSL checker runs the same validation as openssl from outside your network and returns a pass/fail plus the chain depth, which is useful when you do not have a shell on the server.
7. How to Prevent SSL Errors From Reaching Users
Four structural changes cut SSL incidents to near zero on my own infrastructure. None of them is clever, all of them are boring, that is why they work:
- Automate renewal and reload in one unit. certbot
--deploy-hook "systemctl reload nginx", or let Caddy handle it end-to-end. The renew step and the reload step must live in the same systemd service or the same deploy hook, not two independent crons. A successful renew with no reload is an outage waiting 90 days. - Enable HSTS with preload. Once your site is solid, add
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadand submit to hstspreload.org. Preloading means browsers refuse plain HTTP to your domain even on the first visit. The cost is that you can never go back to HTTP, so do this only when your HTTPS setup is stable. - Enforce modern TLS. Mozilla publishes a canonical “intermediate” config for nginx, Apache, HAProxy, and AWS ALB. Copy it verbatim (TLS 1.2 + TLS 1.3, modern cipher suites, ECDHE key exchange). Delete TLS 1.0 and 1.1. You should never be hand-writing a cipher list in 2026.
- Use client-cert mTLS where it fits. For admin endpoints, internal dashboards, or API backends that only talk to a known set of clients, pin mTLS. In my own stack I run a Caddyfile snippet that accepts connections only if the client cert chains to the Cloudflare Origin CA, which turns “anyone on the Internet” into “anyone routed through our edge.” That closes an entire class of direct-to-origin abuse.
The structural theme is: make valid TLS the only thing your server can serve, and monitor for drift continuously. Which brings us to the next section.
8. Monitor SSL Expiry Before the Cert Breaks
The hardest category of SSL error to catch locally is the one where the cert on disk is fine but the handshake the public gets is broken. Local cron reading /etc/ssl/fullchain.pemmisses hostname mismatches. It misses SNI misconfig. It misses “we renewed but never reloaded nginx.” It misses the intermediate being removed from the bundle.
The only reliable check is a real TLS handshake from outside your network. That is the category Visual Sentinel sits in. At the moment we track 356 SSL certificates across 118 customer monitors, and 95 of them expire in the next 30 days. 79% are Let’s Encrypt, which means auto-renewal is the only thing standing between those sites and a browser warning, and external monitoring is the only thing that catches a renewal that quietly failed.
Catching SSL errors externally
Our SSL monitoring performs a real TLS handshake against your public URL from EU and US regions every few minutes. It validates the full chain, not just the leaf expiry, catches hostname mismatches, flags weak signature algorithms, and alerts you at 30, 14, 7, 3, and 1 day before expiry via email, Slack, Discord, Telegram, WhatsApp, or webhook.
Start free SSL monitoringBeyond monitoring, the other big win is a staging probe that runs the same handshake against a non-public staging domain. If the probe fails in staging, the renewal pipeline has a problem you can fix before it hits prod. Belt and braces.
9. Frequently Asked Questions
Related Guides
What Is an SSL Certificate Chain?
The companion guide to this one. Explains how the chain of trust is structured, how browsers walk it, and why a missing intermediate is the most common cert error in production.
What Is 502 Bad Gateway and How to Fix It
The other half of the reverse-proxy failure surface. TLS handshake errors and 502s often share the same proxy-to-origin root cause.
SSL Monitoring by Visual Sentinel
External SSL handshake monitoring from EU and US regions, with expiry alerts at 30, 14, 7, 3, and 1 day before your cert breaks.
Rai Ansar
DevOps Engineer, Founder of Visual Sentinel
I run production TLS on Hetzner bare metal and spend a lot of time watching other people’s certs expire on the dashboard before they do. Visual Sentinel exists because a cert that looks fine on disk and broken in a browser is exactly the kind of failure a localhost healthcheck cannot catch. More from Rai.
Catch SSL errors before your users do
External TLS handshake monitoring, full chain validation, and expiry alerts at 30, 14, 7, 3, and 1 day out. Set it up in under two minutes.
Start SSL MonitoringFree plan available · See all plans