TLS and certificates: every browser warning and what causes it
A visitor sends you a screenshot. Red triangle, “Your connection is not private”, a code in small grey capitals such as NET::ERR_CERT_DATE_INVALID, and the “Back to safety” button they pressed instead of “Proceed anyway”. Other visitors will have seen the same warning and left without telling you.
Every one of those warnings has an exact cause, and the code tells you which one. Find your code in the table and jump to its section, where one command confirms it. Most fixes take under fifteen minutes once you know where to look.
Find your code first
| The visitor sees (Chrome, Edge, Brave) | Firefox says | curl says | Cause |
|---|---|---|---|
| NET::ERR_CERT_DATE_INVALID | SEC_ERROR_EXPIRED_CERTIFICATE | curl: (60) SSL certificate problem: certificate has expired | The certificate ran out, or the visitor’s clock is wrong |
| NET::ERR_CERT_COMMON_NAME_INVALID | SSL_ERROR_BAD_CERT_DOMAIN | curl: (60) SSL: no alternative certificate subject name matches target host name | The certificate is for a different name than the one in the address bar |
| NET::ERR_CERT_AUTHORITY_INVALID | SEC_ERROR_UNKNOWN_ISSUER or MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT | curl: (60) SSL certificate problem: unable to get local issuer certificate | Self-signed, or the intermediate certificate is missing from the chain |
| ERR_SSL_VERSION_OR_CIPHER_MISMATCH | SSL_ERROR_NO_CYPHER_OVERLAP | curl: (35) error:0A000410:SSL routines::sslv3 alert handshake failure | The server only offers protocols or ciphers the browser refuses |
| ERR_SSL_PROTOCOL_ERROR | SSL_ERROR_RX_RECORD_TOO_LONG | curl: (35) error:0A00010B:SSL routines::wrong version number | Port 443 is answering with something that is not TLS (Transport Layer Security) |
| NET::ERR_CERT_REVOKED | SEC_ERROR_REVOKED_CERTIFICATE | (curl does not check revocation by default) | The issuing authority revoked the certificate |
| “Not secure” in the address bar, page loads anyway | Padlock with a warning triangle | nothing | Plain HTTP (Hypertext Transfer Protocol), or an HTTPS (Hypertext Transfer Protocol Secure) page pulling in HTTP resources |
The command behind the curl column is the same every time, so run it once against your own site and keep the output:
curl -sSI https://www.example.com/ -o /dev/null
If it prints nothing, curl was happy, and the problem is either specific to one browser or has already fixed itself. If it prints a line starting curl: (60) or curl: (35), you have your row.
NET::ERR_CERT_DATE_INVALID: it expired
The certificate had a notAfter date, the date passed, and every browser started refusing it within the same minute. Certificates from the big automated issuers now run 90 days, so anything renewed by hand, or by a cron job that stopped, hits this on a predictable schedule.
Confirm it in one line:
echo | openssl s_client -servername www.example.com -connect www.example.com:443 2>/dev/null | openssl x509 -noout -dates
You get back two lines, notBefore= and notAfter=. If notAfter is in the past, that is the whole diagnosis. If notAfter is in the future and the visitor still sees NET::ERR_CERT_DATE_INVALID, their device clock is wrong, which happens on laptops with a dead CMOS battery and on phones that never synced after a factory reset. You cannot fix that from your side; tell them to check the date.
The fix for a real expiry depends on how the certificate got there. If it was Let’s Encrypt via certbot, run sudo certbot renew and then sudo systemctl reload nginx (or apache2). Certbot renews the file on disk; it does not always tell the web server to load the new one, and a stale process serving an old certificate from memory looks identical to a failed renewal.
If a hosting panel issued it, look for the auto-renew toggle and check it is still on.
Then find out why it lapsed. sudo systemctl list-timers | grep certbot should show a timer that ran within the last day. If it is absent, nobody automated the renewal, and it will expire again in 90 days.
NET::ERR_CERT_COMMON_NAME_INVALID: right certificate, wrong name
The server presented a valid certificate for a name that is not the one the visitor typed. The classic pair is example.com and www.example.com: the certificate covers one, the visitor used the other. The second classic is a shared host that presents its own default certificate, so your visitor sees a certificate for host042.example.net and a warning.
List every name the certificate covers:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -ext subjectAltName
The output looks like DNS:example.com, DNS:www.example.com. If the name the visitor typed is absent from that list, reissue with both names. For certbot, run certbot -d example.com -d www.example.com again and accept the expand prompt. For a hosting panel it is usually a checkbox called “include www” that was not ticked.
If the list shows a name you do not recognise, the server is not reading your certificate for that hostname. On nginx that means the server_name in the block holding ssl_certificate does not match, and nginx fell through to its default server. On Apache it is a <VirtualHost *:443> with no matching ServerName. Once the block is fixed and reloaded, the openssl line above should list your names.
Rule out one more cause: a DNS (Domain Name System) record pointing at the wrong box entirely. If example.com resolves to an old IP (Internet Protocol) that still answers on 443 with a certificate from two years ago, reissuing on the new box will not help. dig +short example.com settles it in two seconds. The DNS and DNSSEC guide goes through how records go stale and what DNSSEC (Domain Name System Security Extensions) changes.
NET::ERR_CERT_AUTHORITY_INVALID: nobody the browser trusts signed it
Two different problems produce the same screen. The first is a self-signed certificate: the server signed it for itself and the browser has never heard of the signer. The second is a missing intermediate: a real authority signed it, and the server sends only the leaf, without the intermediate that links it back to a root the browser holds. Some browsers go and fetch the missing intermediate themselves and the page works. Others do not, so that visitor sees the warning while you see a working site.
The census counts 4,390,979 self-signed certificates among the 220,264,382 certificates it collected in the October 2026 edition. Some of those are deliberate, on internal hosts and appliances that were never meant for the public. The rest are servers still serving the placeholder certificate they shipped with.
Tell the two apart with the verify return code:
echo | openssl s_client -servername www.example.com -connect www.example.com:443 2>/dev/null | grep "Verify return code"
Verify return code: 18 (self-signed certificate) means exactly that: get a real certificate. Verify return code: 21 (unable to verify the first certificate) means the leaf is real and the chain is short.
On nginx, ssl_certificate must point at the full chain file (certbot names it fullchain.pem, and the mistake is pointing at cert.pem). On Apache 2.4.8 and later, SSLCertificateFile accepts the concatenated chain; older builds need a separate SSLCertificateChainFile. On a load balancer or CDN (Content Delivery Network), there is a field for the chain or “CA bundle”, and it is easy to paste the certificate in and leave that field empty.
The two handshake errors: nothing was ever exchanged
ERR_SSL_VERSION_OR_CIPHER_MISMATCH
The browser and server could not agree on a protocol version or a cipher suite, and the connection ended before the server got as far as sending a certificate, so a new certificate will not help. The cause is almost always a server that only speaks TLS 1.0 or 1.1, which every mainstream browser dropped, or a configuration that pins one obscure cipher.
Do not confuse that with a server negotiating 1.2 rather than 1.3. Of the 220,264,382 domains where the census completed a TLS handshake, 208,230,719 (94.5%) negotiated TLS 1.3 and 12,033,663 (5.5%) negotiated TLS 1.2. TLS 1.1: 0 (0.00%). TLS 1.0: 0 (0.00%). A site on 1.2 works in every current browser and produces no warning of any kind; the mismatch error comes from servers that offer nothing the browser will accept.
Prove it by asking for each version in turn:
openssl s_client -connect www.example.com:443 -tls1_2 </dev/null 2>&1 | grep -E "Protocol|alert"
openssl s_client -connect www.example.com:443 -tls1_3 </dev/null 2>&1 | grep -E "Protocol|alert"
If both return alert handshake failure, the server config is the problem. On nginx set ssl_protocols TLSv1.2 TLSv1.3; and remove any ssl_ciphers line you do not understand. On Apache it is SSLProtocol -all +TLSv1.2 +TLSv1.3. Old appliances sometimes cannot be raised past 1.0; put those behind a reverse proxy that terminates TLS properly.
ERR_SSL_PROTOCOL_ERROR
The browser connected to port 443, sent a TLS ClientHello, and got back bytes that were not a TLS ServerHello. The Firefox name is more literal: SSL_ERROR_RX_RECORD_TOO_LONG, because what came back was usually an HTTP response, and its first line read as an impossibly long TLS record.
The usual cause is a web server block listening on 443 without ssl in the listen directive, so it answers plain HTTP on the secure port. The other common one is a firewall or proxy forwarding 443 to a backend’s port 80.
curl -sv https://www.example.com/ 2>&1 | grep -E "wrong version|SSL routines"
error:0A00010B:SSL routines::wrong version number confirms it. Check listen 443 ssl; on nginx, SSLEngine on inside the 443 VirtualHost on Apache, and whatever port mapping sits in front of the container.
NET::ERR_CERT_REVOKED and the warnings without a code
Revocation is rare and almost never your fault. An authority revokes a certificate when someone reports the key compromised, or when the authority finds it issued a whole batch against its own rules and pulls the lot. The fix is the same as expiry: reissue, which with certbot is certbot renew --force-renewal. If nobody told you why, check the authority’s status page for a mass revocation before assuming your key leaked.
The warning with no code at all is the plain “Not secure” in the address bar on a page that otherwise loads. Two causes. Either the page is served over HTTP with no TLS in the picture, or it is served over HTTPS and then loads an image or a script over http://. Browsers block the script outright and mark the page for the image.
Open the browser console: Mixed Content: The page at 'https://www.example.com/' was loaded over HTTPS, but requested an insecure image 'http://...' names the exact resource. Search the codebase for http:// and fix each one, or add <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> as a stopgap while you do. For the rest of the header set that stops a page being downgraded or framed, see the HTTP security headers guide.
Check it before a visitor does
Of the 220,264,382 domains where the census completed a TLS handshake, 196,381,007 (89.2%) presented a certificate that validated and 23,883,375 (10.8%) presented one that did not. Anyone who visits a domain in that second group sees a warning like the one in the screenshot. Per country and per TLD (Top-Level Domain) breakdowns of that figure are on the census data page.
The scan at the top of this page runs the same checks against your domain: whether 443 answers, what version it negotiates, whether the certificate validates from a clean trust store, what names it covers, when it expires, and whether the chain is complete. It reports the error class by name, so you can match it against the table above.
Run it against the bare domain and the www name separately. A site that passes on one and fails on the other is the wrong name case, invisible from your own desk because you always type the form that works.
Getting a free certificate with Let’s Encrypt and Certbot
If you run your own VPS or dedicated server with Nginx or Apache, a certificate costs nothing. Let’s Encrypt is a non-profit certificate authority that issues certificates through the automated ACME (Automatic Certificate Management Environment) protocol, and Certbot is the client that runs on your server and handles both issuance and renewal. Check first whether there is anything to do:
- Behind Cloudflare with the orange cloud switched on, Cloudflare terminates the connection at its own edge with its own certificates, and visitors already see a valid padlock. The leg from Cloudflare to your server still wants a certificate, either from Let’s Encrypt or from Cloudflare’s free Origin CA.
- On a managed host or site builder such as Squarespace, Wix, Shopify or most cPanel hosts, certificates are issued and renewed for you, many of them by Let’s Encrypt underneath (cPanel calls it AutoSSL). Confirm the padlock is there and switch on force HTTPS.
On your own server, the domain’s A or AAAA (IPv6 address) record has to point at it and port 80 has to be reachable, because the default HTTP challenge has the authority fetch a file over port 80. Then, on most modern Linux distributions:
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo certbot --nginx
sudo certbot renew --dry-run
On Debian or Ubuntu, sudo apt install certbot python3-certbot-nginx installs it from apt instead. On Apache, run sudo certbot --apache in place of sudo certbot --nginx. Certbot reads the server config, lists the hostnames it finds and asks which to secure, so pick both the bare domain and the www form if you serve both. When it asks whether to redirect HTTP to HTTPS, say yes: that’s the setting that moves visitors who type the bare domain onto the encrypted connection. Certbot then writes the certificate into the config and reloads the server.
The certificate lasts 90 days, and the timer Certbot installs renews it around day 60. sudo certbot renew --dry-run simulates that renewal and says in its output whether it would succeed, which is the proof the timer is armed. Keep the terminal open until it passes.
Five things break this setup:
- A firewall blocking port 80 makes issuance fail with a validation error, even though once the redirect is on no visitor browses over that port.
- A wildcard certificate for
*.yourdomain.comneeds the DNS-01 challenge, which places a TXT (Text) record at_acme-challenge.yourdomain.comand only automates cleanly where your DNS provider has a supported Certbot plugin. Listing the real hostnames is simpler and works with the default method. - Let’s Encrypt limits how many certificates can be requested per domain per week. Add
--test-certwhile you are testing, so failed attempts use the staging environment and stay off the production quota, then drop the flag for the real certificate. - A CAA (Certification Authority Authorization) record that lists only other authorities makes every Let’s Encrypt issuance and renewal fail, and it fails silently, weeks later, when the renewal comes due. If you publish CAA, it has to allow
letsencrypt.org. - Certbot applies a modern TLS configuration on a fresh setup. A server config that predates it still needs TLS 1.0 and 1.1 and weak ciphers turned off by hand, as in the version mismatch section above.
Make it stay fixed
Every certificate has a notAfter date, so set the renewal up to run without you, then verify the timer: sudo certbot renew --dry-run exercises the whole path without issuing anything and tells you in plain text if the challenge would fail. Put a calendar reminder 14 days before notAfter for anything that renews by hand, because the issuer’s expiry email goes to whichever address signed up, and that is rarely the inbox anyone watches.
Monitor from outside your network with a clean trust store, because your own browser has cached the intermediate and trusts things a visitor’s does not. A free uptime checker with an expiry alert covers it.
Once the certificate is right and stays right, tell browsers to stop trying HTTP. A Strict-Transport-Security: max-age=31536000; includeSubDomains header means a returning visitor never sees a downgrade and never sees the “Not secure” page from a stray HTTP link. Get the certificate solid first, because HSTS (HTTP Strict Transport Security) with a broken certificate turns a warning the visitor can click through into one they cannot. The HSTS guide walks through turning it on in the right order.
Then run the scan again and keep the output as the baseline.
Figures from the defaults.exposed census, data as of 12 October 2026. Census numbers move every month; the current values are on the census data page.