NET::ERR_CERT_AUTHORITY_INVALID (tested)

NET::ERR_CERT_AUTHORITY_INVALID means Chrome could not build a chain from the site's certificate to a root it trusts. The usual causes are a self-signed certificate, a private CA, or a server that forgets to send its intermediate. We built each case with OpenSSL, served it, and recorded the exact error from Chromium, curl, Node and Python.

1. What the error means

Every HTTPS certificate is signed by an issuer. The browser follows the issuers up to a root certificate in its trust store. If that walk does not end at a trusted root, Chromium shows NET::ERR_CERT_AUTHORITY_INVALID and (per the Chrome help docs, not measured in headless mode) the page says "Your connection is not private".

It is a trust problem, not an encryption problem. The TLS handshake worked. The browser just does not believe who signed the certificate.

2. Our test setup

We used OpenSSL to make a private root CA, an intermediate CA signed by it, and several leaf certificates for localhost. A Node https.createServer served each one on port 8990. Then four clients loaded https://localhost:8990/.

# private root CA
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout root.key -out root.crt -days 365 -subj "/CN=Test Root CA" \
  -addext basicConstraints=critical,CA:TRUE \
  -addext keyUsage=critical,keyCertSign,cRLSign

# self-signed leaf
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout ss.key -out ss.crt -days 30 -subj /CN=localhost \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

# expired leaf (OpenSSL 3.4+)
openssl x509 -req -in expired.csr -CA root.crt -CAkey root.key \
  -out expired.crt -extfile leaf.ext \
  -not_before 20250101000000Z -not_after 20250201000000Z

3. Results: what each client reports

No CA file was given to any client here. This is what a visitor or a default client sees.

CaseChromium 153curl (exit 60)Node fetch cause.codePython verify_code
Self-signed leafnet::ERR_CERT_AUTHORITY_INVALIDself-signed certificateDEPTH_ZERO_SELF_SIGNED_CERT18 self-signed certificate
Leaf signed by private CAnet::ERR_CERT_AUTHORITY_INVALIDunable to get local issuer certificateUNABLE_TO_VERIFY_LEAF_SIGNATURE20 unable to get local issuer certificate
Leaf only, intermediate missingnet::ERR_CERT_AUTHORITY_INVALIDunable to get local issuer certificateUNABLE_TO_VERIFY_LEAF_SIGNATURE20 unable to get local issuer certificate
Leaf + intermediate (full chain), private rootnet::ERR_CERT_AUTHORITY_INVALIDunable to get local issuer certificateUNABLE_TO_GET_ISSUER_CERT_LOCALLY20 unable to get local issuer certificate
Expired leaf, private CAnet::ERR_CERT_AUTHORITY_INVALIDunable to get local issuer certificateCERT_HAS_EXPIRED20 unable to get local issuer certificate
Wrong hostname, private CAnet::ERR_CERT_AUTHORITY_INVALIDunable to get local issuer certificateUNABLE_TO_VERIFY_LEAF_SIGNATURE20 unable to get local issuer certificate
Expired self-signednet::ERR_CERT_AUTHORITY_INVALIDself-signed certificateCERT_HAS_EXPIRED18 self-signed certificate
Wrong hostname, self-signednet::ERR_CERT_AUTHORITY_INVALIDself-signed certificateDEPTH_ZERO_SELF_SIGNED_CERT18 self-signed certificate

Two things stand out:

Node 24 also adds a hint to some messages: self-signed certificate; if the root CA is installed locally, try running Node.js with --use-system-ca. Python's full message was [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1029).

Per the Chromium docs (not measured, because we did not touch the trust store), once the root is trusted, an expired certificate shows NET::ERR_CERT_DATE_INVALID and a hostname mismatch shows NET::ERR_CERT_COMMON_NAME_INVALID. Our clients that we could point at the CA (next section) behaved exactly that way.

4. Trusting a private CA for one client (no system changes)

For a development or internal CA, give the client the root certificate. All three methods worked for the self-signed and private-CA cases (HTTP 200):

curl --cacert root.crt https://localhost:8990/
NODE_EXTRA_CA_CERTS=root.crt node app.js
python3 -c "import ssl,urllib.request as u; \
  print(u.urlopen('https://localhost:8990/', \
  context=ssl.create_default_context(cafile='root.crt')).status)"

For a self-signed leaf, pass the leaf itself as the CA file. That also worked.

With the CA trusted, the hidden errors came out:

Case (root trusted)curlNode fetchPython
Expired leafcertificate has expiredCERT_HAS_EXPIRED10 certificate has expired
Wrong hostnameno alternative certificate subject name matches target hostname 'localhost'ERR_TLS_CERT_ALTNAME_INVALID62 Hostname mismatch, certificate is not valid for 'localhost'.
Intermediate missingunable to get local issuer certificateUNABLE_TO_VERIFY_LEAF_SIGNATURE20 unable to get local issuer certificate

Note the last row. Trusting the root does not fix a missing intermediate. The client still cannot link the leaf to the root.

5. Missing intermediate: serve the full chain

This is the most common cause on real public sites. The server sends only its own certificate and leaves out the intermediate. Check what the server sends:

openssl s_client -connect example.com:443 -servername example.com </dev/null \
  | sed -n '/Certificate chain/,/---/p'

With only the leaf configured we saw one entry (0 s:CN=localhost, i:CN=Test Intermediate CA) and Verify return code: 21 (unable to verify the first certificate). After serving cat leaf.crt intermediate.crt > fullchain.crt we saw two entries and Verify return code: 0 (ok). curl, Node and Python then all returned 200 with the root as CA.

A client-side workaround also worked: a bundle of root + intermediate passed to curl --cacert, NODE_EXTRA_CA_CERTS or Python's cafile gave 200. Fix the server instead; you cannot ship a bundle to every visitor.

6. Does Chrome fetch missing intermediates (AIA)?

Yes, we saw it happen. Our test leaf had an Authority Information Access caIssuers URL pointing at a logging server on port 8991. Chromium 153 requested /int.der once per page load, with its normal HeadlessChrome user agent. curl, Node fetch and Python never requested it.

This is why a site with a broken chain can work in Chrome and still fail in curl, Node, Python, Java and many mobile apps. Do not rely on AIA fetching. Serve the full chain.

7. How to fix it

To keep a valid certificate from quietly expiring, see how to check SSL certificate expiry. Once HTTPS loads, you can review HSTS in nginx, which, per the Chromium docs, removes the "proceed anyway" link from this error page.

Frequently asked questions

Why does Chrome say ERR_CERT_AUTHORITY_INVALID when my certificate is expired?

Chrome reports the trust problem first. In our test an expired certificate from an untrusted CA showed ERR_CERT_AUTHORITY_INVALID in Chromium 153. Per the Chromium docs, ERR_CERT_DATE_INVALID appears when the issuer is trusted.

The site works in Chrome but curl says 'unable to get local issuer certificate'. Why?

Most likely the server does not send its intermediate certificate. Chromium fetched the missing intermediate from the certificate's AIA URL in our test; curl, Node and Python did not. Serve the full chain.

How do I make Node.js trust my private CA?

Start Node with NODE_EXTRA_CA_CERTS=/path/to/root.crt. In our Node 24 test that turned DEPTH_ZERO_SELF_SIGNED_CERT and UNABLE_TO_VERIFY_LEAF_SIGNATURE into a normal 200 without touching the system store.

What do DEPTH_ZERO_SELF_SIGNED_CERT and UNABLE_TO_VERIFY_LEAF_SIGNATURE mean?

DEPTH_ZERO_SELF_SIGNED_CERT: the server sent one self-signed certificate. UNABLE_TO_VERIFY_LEAF_SIGNATURE: Node could not find the leaf's issuer, either a private CA or a missing intermediate. Both appeared in our tests.

Is it safe to click 'Proceed anyway'?

Only on a machine or server you control and know uses a self-signed or private certificate. On a public site the error can also mean someone is intercepting the connection. Per the Chromium docs, if the site uses HSTS, Chrome does not offer the link at all.

Written with AI assistance. Measured on 2026-10-02 with headless Chromium 153.0.8010.12 (Playwright), curl 8.14.1 (OpenSSL 3.5.7), Node.js 24.21.0 fetch and Python 3.13.5 ssl/urllib. Certificates were made with OpenSSL 3.5.7 and served by a Node https server on localhost. Nothing was added to any system trust store. Behaviour with a trusted root in Chromium is from the Chromium docs, not measured. PlainHeaders is our extension; OpenSSL, curl, Python, Node.js and Chromium are free.