Start with one question: does the error show up on one site or on every site?
- One site, on every device and network. The problem is on the site's side, usually the certificate. If it's your site, jump to the section for site owners.
- Every site, or only on one device or network. The problem is on your side: the clock, an antivirus, an extension or the network. The visitor section below covers it.
Every server-side case in this guide was reproduced in a local lab: nginx 1.30 with certificates from a test CA, curl 8.5 and OpenSSL 3.0 on Ubuntu 24.04, and Chrome for Testing 156. The screenshots and command output are real.
What "Your connection is not private" means
Before a page loads over HTTPS, the browser checks the site's certificate. The certificate has to be issued by a trusted certificate authority (CA), cover the exact domain you typed, be inside its validity dates, and come with the full chain up to a trusted root. If any of these checks fails, Chrome stops and shows a full-page warning instead of the site.
The line under the text is the important part. In the screenshot it's NET::ERR_CERT_DATE_INVALID, and the Advanced section says the certificate expired 98 days ago.
Other browsers show the same check with different words:
- Chrome: "Your connection is not private".
- Edge: "Your connection isn't private", with the same
NET::ERR_CERT_...codes, because Edge is built on Chromium. - Safari on Mac, iPhone and iPad: "This Connection Is Not Private".
- Firefox: "Warning: Potential Security Risk Ahead", with codes like
SEC_ERROR_EXPIRED_CERTIFICATE.
A different page, "This site can't provide a secure connection", means the secure connection didn't start at all. It has its own codes, like ERR_SSL_PROTOCOL_ERROR, and it's covered below too.
If you're a visitor: quick fixes
If the error shows up on many sites, or only on your device, try these in order:
- Check the date and time. A clock that's off by days or years makes valid certificates look expired or not yet valid. Chrome even says so in the Advanced text: "Your computer's clock is currently set to...". Turn on automatic date and time, then reload.
- Sign in to the Wi-Fi first. Hotels, airports and cafes often catch your first request to show a login page. Chrome usually offers a "Connect to Wi-Fi" button for this. Finish the login, then reload the site.
- Check your antivirus and VPN. Some antivirus tools and corporate proxies scan HTTPS traffic by putting their own certificate in the middle. If it's set up wrong or out of date, you get errors on many sites. If Gmail or another Google site shows
ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN, something between you and Google replaced its certificate, and this is the first thing to check. Turn off HTTPS scanning in the antivirus, or ask your IT team. - Try an Incognito window. Extensions are off there by default. If the site opens in Incognito, disable your extensions one by one.
- Update the browser and the operating system. Very old devices don't trust newer root certificates.
- Try another network, for example mobile data instead of Wi-Fi. If the error disappears, the original network is interfering.
If the error shows up on one site only and none of the above helps, the problem is on that site. Wait, or let the owner know.
When not to click "Proceed"
Under Advanced, Chrome offers "Proceed to ... (unsafe)". Skip it on any site where you log in, pay, or type personal data. You can't tell an expired certificate from an attacker on the same Wi-Fi by looking at the warning page.
It's reasonable to proceed only when you know exactly why the certificate fails: your own test server, a router admin page, or an internal tool your team runs with a self-signed certificate.
If there's no Proceed link at all, the site uses HSTS, and Chrome won't let anyone skip the warning. That's on purpose. Tricks you'll find online that skip the page don't remove the risk, so wait for the site owner to fix the certificate.
If you own the site: what each Chrome error code means
The code on the warning page tells you what to fix. Here are the ones you'll see most:
NET::ERR_CERT_DATE_INVALID. The certificate has expired, or it isn't valid yet. Usually renewal failed or nginx wasn't reloaded after renewal. Less often, the server or visitor clock is wrong.NET::ERR_CERT_AUTHORITY_INVALID. The browser doesn't trust whoever signed the certificate. Typical causes: a self-signed certificate, a certificate from a private CA, or a chain the browser can't complete.NET::ERR_CERT_COMMON_NAME_INVALID. The certificate doesn't cover the name in the address bar. Typical causes: a certificate forexample.comserved onwww.example.com, the wrong certificate picked by the server, or a certificate without a Subject Alternative Name (SAN).ERR_SSL_PROTOCOL_ERROR. The server answered with something that isn't TLS. Most often port 443 serves plain HTTP because thesslflag is missing in the nginxlistenline.ERR_SSL_VERSION_OR_CIPHER_MISMATCH. The browser and the server have no TLS version or cipher in common. Usually the server only allows TLS 1.0 or 1.1, which current browsers have dropped.NET::ERR_CERT_REVOKED. The CA has revoked the certificate, for example after the private key leaked. Issue a new certificate with a new key.NET::ERR_CERT_INVALID. The certificate is broken in a way Chrome won't accept. In our test, a certificate signed with SHA-1 produced this code.NET::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED. A publicly trusted certificate was not logged in Certificate Transparency logs. Certificates from public CAs are logged automatically, so this one usually points to an interception device or a misconfigured CA.
The sections below show how to confirm each case from the command line and how to fix it.
Check the certificate from the command line
The browser shows a code. The command line shows you why. Two tools cover almost everything: curl and openssl.
curl
Run curl against the site. -I asks only for headers, -v prints the TLS handshake:
shell
1curl -vI https://example.com/Here is the output against expired.badssl.com, a public test site with an expired certificate, from Ubuntu 24.04:
shell
1* Connected to expired.badssl.com (104.154.89.105) port 4432* ALPN: curl offers h2,http/1.13* TLSv1.3 (OUT), TLS handshake, Client hello (1):4* CAfile: /etc/ssl/certs/ca-certificates.crt5* CApath: /etc/ssl/certs6* TLSv1.3 (IN), TLS handshake, Server hello (2):7* TLSv1.2 (IN), TLS handshake, Certificate (11):8* TLSv1.2 (OUT), TLS alert, certificate expired (557):9* SSL certificate problem: certificate has expired10* Closing connection11curl: (60) SSL certificate problem: certificate has expiredExit code 60 means certificate verification failed. The message after it names the reason, and it maps to the Chrome code:
shell
1curl: (60) SSL certificate problem: certificate has expired2curl: (60) SSL certificate problem: certificate is not yet valid3curl: (60) SSL: no alternative certificate subject name matches target host name 'www.example.com'4curl: (60) SSL certificate problem: self-signed certificate5curl: (60) SSL certificate problem: self-signed certificate in certificate chain6curl: (60) SSL certificate problem: unable to get local issuer certificate7curl: (35) OpenSSL/3.0.13: error:0A00010B:SSL routines::wrong version number8curl: (35) OpenSSL/3.0.13: error:0A00042E:SSL routines::tlsv1 alert protocol versionIn order: expired, not yet valid, wrong name, self-signed, untrusted root, missing intermediate, plain HTTP on port 443, and an old TLS version on the server. You can try most of them on the BadSSL test subdomains.
curl -k skips the check and shows whether the site works behind the certificate problem. Use it for diagnosis only, never in scripts or health checks.
openssl s_client
openssl shows the certificate itself. This prints who it was issued to, who issued it, the validity dates and every name it covers:
shell
1openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \2 | openssl x509 -noout -subject -issuer -dates -ext subjectAltNameshell
1subject=CN = example.com2issuer=O = ITS Lab, CN = ITS Lab Intermediate R13notBefore=Nov 4 15:50:22 2026 GMT4notAfter=Feb 2 15:50:22 2027 GMT5X509v3 Subject Alternative Name:6 DNS:example.com, DNS:www.example.comThe issuer here is the test CA from our lab. On a real site you will see your CA, for example Let's Encrypt.
-servername matters. Without it, a server with several sites may send the default certificate instead of yours.
To see the full verdict and the chain the server sends, add -verify_hostname and look at the last line:
shell
1openssl s_client -connect example.com:443 -servername example.com -verify_hostname example.com </dev/nullshell
1Verify return code: 0 (ok)Anything other than 0 (ok) names the problem, for example 10 (certificate has expired), 62 (hostname mismatch), 18 (self-signed certificate) or 21 (unable to verify the first certificate).
Chrome DevTools
To see the certificate in Chrome, open DevTools with Ctrl+Shift+I on Windows and Linux or Cmd+Option+I on Mac. Open the Command menu with Ctrl+Shift+P or Cmd+Shift+P, type "privacy" and choose Show Privacy and security. In Security, then Overview, click View certificate. The panel used to be called Security, and older guides still use that name. Details are in the Chrome DevTools docs.
SSL Labs
SSL Labs Server Test checks your site from the outside and reports problems browsers sometimes hide, such as an incomplete chain or old TLS versions. It works only for public sites on port 443.
Fix the common causes
Each case below was broken on purpose in our lab, checked with curl, openssl and Chrome, and then fixed the way shown.
Expired certificate: NET::ERR_CERT_DATE_INVALID
This is the most common cause by far. curl says certificate has expired, and openssl shows a notAfter date in the past:
shell
1verify error:num=10:certificate has expired2Verify return code: 10 (certificate has expired)Renew the certificate and reload the web server. With Let's Encrypt and Certbot:
shell
1sudo certbot renew2sudo systemctl reload nginxIf renewal succeeded but the browser still shows the old date, nginx is still serving the old certificate from memory. A reload fixes it. With certbot --nginx, Certbot reloads nginx itself. With certonly, add a deploy hook so nginx is reloaded after every renewal. Our Let's Encrypt on Nginx guide shows the full setup, and the AWS EC2 SSL guide covers ACM and paid certificates.
Certificate isn't valid yet: also NET::ERR_CERT_DATE_INVALID
The same code shows up when the certificate's start date is in the future. Chrome's Advanced text says the certificate is "supposedly from 57 days in the future", and curl says certificate is not yet valid.
A brand-new certificate is valid from the moment it's issued, so this almost always means a wrong clock. If only some visitors see it, their device clock is wrong. If everyone sees it, check the clock on the server that issued or installed the certificate:
shell
1timedatectlshell
1System clock synchronized: yesIf it says no, turn on NTP with sudo timedatectl set-ntp true.
Wrong name: NET::ERR_CERT_COMMON_NAME_INVALID
The classic case: the certificate covers example.com, but visitors open www.example.com. Chrome says the certificate "is from" the other name:
openssl shows which names the certificate actually covers:
shell
1X509v3 Subject Alternative Name:2 DNS:example.comIssue a certificate that covers every name you serve:
shell
1sudo certbot --nginx -d example.com -d www.example.comThen pick one main address and redirect the other one to it. Our guide on redirecting HTTP to HTTPS with Nginx shows the www and non-www redirects.
Two less obvious causes give the same code:
- The name is only in the Common Name. Chrome ignores the CN and checks only the Subject Alternative Name list. In our test, a certificate with
CN=example.comand no SAN passed curl but failed in Chrome with "does not specify Subject Alternative Names". Public CAs always add SANs, so this happens with self-made certificates. - nginx picked another site's certificate. If no
server_namematches the requested name, nginx uses the default server for that port and its certificate. Check that the site has its ownserverblock withlisten 443 ssland the rightserver_name.
Self-signed or unknown CA: NET::ERR_CERT_AUTHORITY_INVALID
A self-signed certificate, the default certificate of a control panel, or one from your company's private CA isn't trusted by visitors' browsers:
openssl shows it right away: the issuer is the same as the subject.
shell
1 0 s:CN = example.com2 i:CN = example.com3Verify return code: 18 (self-signed certificate)For a public site, replace it with a certificate from a public CA. Let's Encrypt is free and Certbot sets it up in a few minutes. For internal tools, either do the same or install your private root CA on company devices through your device management. Don't train your team to click Proceed.
Missing intermediate certificate
CAs don't sign site certificates with their root directly. They use an intermediate certificate, and your server has to send it together with your certificate. If it doesn't, the chain is incomplete.
This one is tricky, because desktop Chrome often doesn't complain. It downloads the missing intermediate on its own using the address inside the certificate. In our test, Chrome opened the site without a warning, and the nginx log showed Chrome fetching the intermediate. curl, Python, Java, API clients and some mobile browsers don't do that and fail:
shell
1curl: (60) SSL certificate problem: unable to get local issuer certificateshell
1Verify return code: 21 (unable to verify the first certificate)So "it works in my browser" doesn't prove the chain is right. Count the certificates the server sends:
shell
1openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -c "BEGIN CERTIFICATE"1 means only your certificate is sent, and the chain is incomplete. 2 or more means the intermediate is there.
The fix is to point nginx to the full chain. With Let's Encrypt, that's fullchain.pem, not cert.pem:
nginx
1ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;2ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;With a paid certificate, put your certificate first and the intermediate after it in one file:
shell
1cat example.com.crt intermediate.crt > example.com.fullchain.crtYou can check the file before you deploy it. Pass the chain with -untrusted, otherwise openssl checks only the first certificate in the file and fails:
shell
1openssl verify -untrusted example.com.fullchain.crt example.com.fullchain.crtshell
1example.com.fullchain.crt: OKAlso check that the key belongs to the certificate. Both commands print the same public key if it does:
shell
1openssl x509 -in example.com.crt -noout -pubkey2openssl pkey -in example.com.key -puboutPort 443 serves plain HTTP: ERR_SSL_PROTOCOL_ERROR
Here the browser doesn't even get to the certificate. It shows "This site can't provide a secure connection" and says the site "sent an invalid response":
curl and openssl report wrong version number, because they got an HTTP answer where they expected TLS. In nginx, the usual reason is a listen line without ssl:
nginx
1listen 443;Add ssl and the certificate lines, then test the config and reload:
nginx
1listen 443 ssl;2ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;3ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;shell
1sudo nginx -t && sudo systemctl reload nginxThe same error can come from a firewall, load balancer or proxy that forwards port 443 to an HTTP port on the backend.
Old TLS version: ERR_SSL_VERSION_OR_CIPHER_MISMATCH
If the server allows only TLS 1.0 or 1.1, Chrome says the site "uses an unsupported protocol", and curl reports tlsv1 alert protocol version. Allow TLS 1.2 and 1.3:
nginx
1ssl_protocols TLSv1.2 TLSv1.3;Old configs copied from 2015-era guides are the usual source. Check the ssl_protocols line in every server block and in nginx.conf.
HSTS: why there's no "Proceed" link
HSTS (HTTP Strict Transport Security) is a response header that tells the browser to use only HTTPS for your domain for a set time. It also takes away the Proceed link. In our test, Chrome visited a site that sent HSTS. Then the site got a self-signed certificate, and the warning page had only a Reload button:
For visitors, this means nothing on their side will help. For site owners, it means a certificate mistake locks out everyone who visited the site before, until the certificate is fixed. That's why HSTS should go together with automatic renewal and monitoring.
You can check whether your site sends the header:
shell
1curl -sI https://example.com/ | grep -i strict-transport-securityshell
1Strict-Transport-Security: max-age=31536000How to add HSTS safely, starting with a short max-age, is covered in step 7 of our Nginx HTTP to HTTPS guide.
Mixed content is a different problem
Mixed content means an HTTPS page loads scripts, images or other files over plain HTTP. It doesn't cause the "Your connection is not private" page. The page opens, but parts of it break.
In current Chrome:
- images, audio and video requested over HTTP are upgraded to HTTPS automatically. If that host has no working HTTPS, the file doesn't load;
- scripts, iframes and other active content over HTTP are blocked.
Here is what the Console showed on our test page:
shell
1Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but requested an insecure element 'http://cdn.example.com/logo.png'. This request was automatically upgraded to HTTPS2Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but requested an insecure script 'http://cdn.example.com/app.js'. This request has been blocked; the content must be served over HTTPS.To fix it, change the URLs to https:// in your templates, database and settings. In WordPress, that usually means the site URL and old links in posts. To find what's left, open DevTools, then Privacy and security, and click View requests in Network panel under Non-secure origins. More on the rules is on MDN.
A site with no HTTPS at all gets yet another warning. Google announced that from Chrome 154, released in October 2026, Chrome asks before opening a public site over plain HTTP. If you still run a site without HTTPS, set it up now.
Keep certificates from expiring
Expired certificates will get more common, not less, because certificates are getting shorter:
- Public certificates. Under CA/Browser Forum ballot SC-081, the maximum validity is 200 days for certificates issued from March 15, 2026, 100 days from March 15, 2027, and 47 days from March 15, 2029.
- Let's Encrypt is moving from 90 to 45 days: 64 days by default from February 10, 2027, and 45 days from February 16, 2028.
- No reminder emails. Let's Encrypt stopped sending expiration emails on June 4, 2025. If renewal breaks, nobody tells you.
Manual renewal doesn't scale to this. What works:
- Automate renewal. Use an ACME client like Certbot with its timer, or a managed service like AWS Certificate Manager. Test renewal with
sudo certbot renew --dry-run. - Reload after renewal. A renewed file on disk doesn't help until nginx loads it. Use a deploy hook.
- Monitor the certificate the site actually serves, not the file on disk. A simple check that you can run from cron or a monitoring system:
shell
1#!/bin/sh2# Warn when the certificate a site serves expires in less than 14 days3host=example.com4echo | openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null \5 | openssl x509 -noout -checkend 1209600 >/dev/null \6 || echo "Certificate for $host expires in less than 14 days"1209600 is 14 days in seconds. Let's Encrypt also lists monitoring services that can watch certificates for you.
If you'd rather have someone else watch certificates, renewals and the rest of your servers, our team handles that every day. Take a look at our cloud engineering services and tell us what's breaking.
