Learning objectives
After reading this article you will be able to:
- Identify the cause from the browser, curl, Node.js or Python error message
- Diagnose expired certificates, wrong domains and incomplete chains with openssl
- Fix the problem without turning off certificate verification
You open your site and the browser shows a red warning page or a "Not secure" label. Or your bot, your API or a curl call fails with certificate has expired. All of these share one root: while setting up the TLS connection, the client received a certificate and could not trust it.
The good news is that there are only a few causes, and openssl finds the right one in under a minute. This guide goes from the error message to the fix without asking you to turn verification off. If you want background on what the certificate does, see what is SSL/TLS.
How do you read the error message?#
The message already points at the cause. Chrome and Chromium-based browsers show a NET::ERR_... code:
NET::ERR_CERT_DATE_INVALID # expired, or the device clock is wrong
NET::ERR_CERT_COMMON_NAME_INVALID # the certificate does not cover this domain
NET::ERR_CERT_AUTHORITY_INVALID # unknown issuer, self-signed or incomplete chain
ERR_SSL_VERSION_OR_CIPHER_MISMATCH # handshake: no shared TLS version or cipher
ERR_SSL_PROTOCOL_ERROR # handshake: the server did not speak TLS properlyOutside the browser, the names come from OpenSSL. In Node.js the error carries err.code:
CERT_HAS_EXPIRED # expired
CERT_NOT_YET_VALID # start date is in the future
ERR_TLS_CERT_ALTNAME_INVALID # domain name does not match
UNABLE_TO_VERIFY_LEAF_SIGNATURE # incomplete chain
DEPTH_ZERO_SELF_SIGNED_CERT # self-signed
SELF_SIGNED_CERT_IN_CHAIN # self-signed inside the chaincurl answers with curl: (60) SSL certificate problem: unable to get local issuer certificate, and Python with ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]. Those two are generic: they say verification failed, and the cause is in the text that follows or in the diagnosis below.
How do you diagnose it with openssl?#
One command shows the dates, the issuer and the names covered:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates -issuer -subject -ext subjectAltNameRead the output like this:
notAfterin the past: the certificate expired.subjectAltNamewithout your domain: the certificate is for a different name.issuerequal tosubject: self-signed.
To see the chain and the verification verdict, run s_client without the filters and look for Verify return code. A value of 0 (ok) means success. Anything else gives the reason, such as 20 (unable to get local issuer certificate).
The -servername argument matters: many servers host several domains on one IP and pick the certificate by the name sent in the SNI extension. Without it you may get the wrong certificate and chase the wrong problem.
Why did the certificate expire?#
This is the number one cause. Let's Encrypt certificates are short-lived, and automatic renewal fails silently when the timer is disabled, port 80 is taken by another service or the DNS API credential expires. Let's Encrypt keeps shortening that lifetime in stages, so manual renewal stops being viable.
The fix:
- Run the renewal and read the error:
sudo certbot renew --dry-run. - Fix the cause (port 80 open, DNS pointing at the right server, credential renewed).
- Reload the web server, because many only read the certificate at startup:
sudo systemctl reload nginx. - Confirm with the
opensslcommand above.
The process is covered in how Let's Encrypt works. Before blaming the server, check the date on your computer or phone: a wrong clock causes ERR_CERT_DATE_INVALID even with a valid certificate.
What if the domain does not match?#
A certificate is valid for a list of names. example.com does not cover www.example.com unless both are on the list, and a *.example.com certificate covers api.example.com but not example.com or a.b.example.com. Common causes:
- The domain points to a server that has another site's certificate.
- DNS still points to the old IP (see DNS propagation).
- You issued the certificate for the root domain only and forgot
www.
Fix it by issuing the certificate with every name you use and checking where the domain resolves (dig +short example.com). The step-by-step is in how to point a domain to your app.
What if the chain is incomplete?#
The server must send the site certificate plus the intermediates. Browsers sometimes complete the chain on their own, but curl, Node.js and Python do not, which is why the site opens in Chrome while your API fails with UNABLE_TO_VERIFY_LEAF_SIGNATURE or unable to get local issuer certificate.
The fix is to serve the full-chain file. With Certbot, that is fullchain.pem, not cert.pem:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;If the error appears only on your machine, with a certificate from a private CA (corporate network, inspection proxy, internal environment), trust that CA instead of turning verification off:
curl --cacert ./my-ca.pem https://internal.example.com
NODE_EXTRA_CA_CERTS=./my-ca.pem node app.jsIn Node.js, rejectUnauthorized defaults to true, and false removes all protection against interception. For your own CA, pass it in the ca option.
Why does it say "Not secure" with HTTPS on?#
If the certificate is fine and the padlock still does not appear, suspect mixed content: the page came over HTTPS but loads something over http://. According to MDN, browsers upgrade images, audio and video to HTTPS on their own, but block scripts, CSS, iframes and fetch calls. The result is a broken page or a warning.
Open the browser console (F12): each affected resource shows up with its URL. Change http:// to https:// or to relative paths. As a shortcut, the Content-Security-Policy: upgrade-insecure-requests header asks the browser to upgrade all requests, but it only works if the other side also serves HTTPS.
What about handshake errors?#
If the error is ERR_SSL_VERSION_OR_CIPHER_MISMATCH or ERR_SSL_PROTOCOL_ERROR, negotiation failed before certificate verification. Check:
- Wrong port or protocol: you are speaking HTTPS to a port that serves plain HTTP (or the reverse).
- Old TLS version: servers limited to the deprecated TLS 1.0 or 1.1 are refused by current clients. Use TLS 1.2 or 1.3.
- No certificate configured for that name or port.
Test with openssl s_client -connect example.com:443 -tls1_2 and -tls1_3 to see which version the server accepts.
How to do this on Square Cloud#
On Square Cloud, several of these causes go away because TLS is automatic: a DV certificate is issued and renewed on its own, and HTTP is redirected to HTTPS. Every web application gets a *.squareweb.app subdomain with HTTPS, on every plan.
For a custom domain, from the Standard plan, you create the DNS records shown in the dashboard at your registrar, then follow the validation status there in real time. If validation does not complete, check that the records you created are exactly the ones the dashboard shows. TLS for the domain is also automatic, with support for accented domains (Punycode) and cache purge. The domain stays registered in your name at your registrar.
See the domains page for the step-by-step.
Network
Custom domains, automatic TLS and a global CDN for your app
FAQs
Why does the browser say 'Not secure' when my site has HTTPS?
Two common causes: the certificate has a problem (expired, wrong domain, unknown issuer) or the HTTPS page loads some resource over HTTP, known as mixed content. The browser console shows which one it is.
Can I disable certificate verification to make the error go away?
Only in a throwaway local test. With rejectUnauthorized: false in Node.js or curl -k, anyone on the path can impersonate the server. In production, fix the certificate or install the right CA.
What does NET::ERR_CERT_DATE_INVALID mean?
The certificate has expired or is not valid yet. If the server's certificate is within its dates, check the device clock: a wrong date triggers the same error.
What is the difference between a certificate error and a handshake error?
In a certificate error, the handshake reached verification and the certificate was rejected. In a handshake error, negotiation fails earlier: no shared TLS version or cipher, or the server does not speak TLS on that port.
More in Networking and domains
See all 27 articles- What is a load balancer and how does it work?A load balancer spreads incoming requests across several servers. Learn how it works, the main algorithms, layer 4 vs 7, and a working nginx example.
- DNS records explained: A, CNAME, MX, TXTDNS records are the instructions in a zone that tell the internet where a name points. Learn A, AAAA, CNAME, MX, TXT, NS and CAA with examples and dig commands.
- How to point a domain to your server or appTo point a domain to your server or app, create a CNAME (or A) record in your registrar's DNS panel. Steps, how to test it, and common mistakes.
- Cache-Control headers explainedCache-Control is the HTTP header that sets how long browsers and CDNs keep a response. Learn max-age, no-cache, immutable and ready-made recipes for each file type.