Geonode logo
Secure connection errors

How to Fix "Unable to Verify the First Certificate" in Node.js

Confirm the gap with openssl, then repair the server's chain or add trust in Node.js.

Updated

TL;DR

UNABLE_TO_VERIFY_LEAF_SIGNATURE ("unable to verify the first certificate") means a server sent Node.js its leaf alone, without the intermediate that connects it to a trusted CA. The site's operator fixes it by sending the full chain; meanwhile, NODE_EXTRA_CA_CERTS can supply the missing intermediate.

What the error looks like in each client

One server that sends no intermediate, as each tool reports it; Python's version is on the CERTIFICATE_VERIFY_FAILED page.

WhereWhat you see
Node.js 24 http and https (code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE')Error: unable to verify the first certificate; if the root CA is installed locally, try running Node.js with --use-system-ca
Node.js 20 and earlierError: unable to verify the first certificate
Node.js 20 fetch()TypeError: fetch failed ... [cause]: Error: unable to verify the first certificate
npm 11npm error code UNABLE_TO_VERIFY_LEAF_SIGNATURE
curlcurl: (60) SSL certificate problem: unable to get local issuer certificate
gitfatal: unable to access 'https://localhost:48466/repo.git/': SSL certificate problem: unable to get local issuer certificate

Why this happens

Browsers find a missing certificate on their own, and Node.js does not.

Node.js builds the path to a trusted CA only from what the server sends plus its own CA list. Chrome downloads the missing piece from a link in the leaf, so the page opens there while your script fails.

Diagnose your UNABLE_TO_VERIFY_LEAF_SIGNATURE first

Two browser checks and three openssl readings separate a server mistake from interception on your network.

  • Open the URL in Chrome; if it loads without a warning, the certificate is valid and only the delivered chain falls short.

  • Test the host name on SSL Labs; if it reports an incomplete chain, the fault sits on the server, not your network.

  • Run openssl s_client -connect host:443 -servername host -showcerts; if Certificate chain shows only entry 0, nothing beyond the leaf arrived.

  • Read entry 0's i: line; if it names your employer or a filtering product, not a public CA, your network inspects HTTPS.

  • Compare entry 0's i: line with entry 1's s: line; if they differ, the server ships a wrong or outdated intermediate.

Solutions ranked by effectiveness

Ranked by reach: a server change fixes everyone, the other two only your own code.

  1. Most common fix

    Send the full chain from the server

    Applies when the server is yours or you can reach its admin; send them your openssl output. Put the leaf first in the file and its issuers after it, leaving out the root.

    1. With Certbot, point nginx ssl_certificate or Apache SSLCertificateFile at fullchain.pem, not cert.pem.

    2. With another CA, paste its bundle below your certificate in one file, leaf first.

    3. In a Node.js https server, pass that combined file as the cert option.

  2. Check next

    Add the intermediate with NODE_EXTRA_CA_CERTS

    Applies when the server is out of your hands. The variable adds a PEM file to the CAs that every request in the Node.js process trusts.

    1. Download the intermediate from the CA Issuers link in the leaf's Authority Information Access field.

    2. Convert a binary .cer file: openssl x509 -inform DER -in ca.cer -out intermediate.pem

    3. Start Node.js with NODE_EXTRA_CA_CERTS=./intermediate.pem; changing process.env at runtime has no effect.

  3. For developers

    Trust it in one agent only

    Applies when only one client should get the extra trust; axios accepts the same agent as httpsAgent. Setting ca replaces Node's default list, so the code adds the bundled roots back.

    const fs = require("node:fs");
    const https = require("node:https");
    const tls = require("node:tls");
    
    const ca = [...tls.rootCertificates, fs.readFileSync("intermediate.pem", "utf8")];
    const agent = new https.Agent({ ca });
    
    https.get("https://api.example.com/", { agent }, (res) => {
      console.log("HTTP", res.statusCode);
      res.resume();
    }).on("error", (err) => console.log(err.code, err.message));

Stop the UNABLE_TO_VERIFY_LEAF_SIGNATURE from coming back

Habits for teams that run HTTPS servers and for scripts that depend on servers run by others.

  1. After each renewal, test the site from Node.js rather than a browser, which may already hold the intermediate.

  2. Take the chain your CA issues with each renewal; Let's Encrypt may switch intermediates without warning.

  3. Add a chain check to your deploy pipeline, so a config that loses the intermediate never ships.

  4. Note why each NODE_EXTRA_CA_CERTS file exists, and delete it after the site owner repairs the chain.

Node.js proxy code from your dashboardPick Node.js in Geonode's API Code Generator to get code built from your proxy settings.
Try residential proxies

Related errors

Learn more

FAQ

It means the first certificate in the chain, the one issued to the site, could not be checked against any CA Node.js knows. Usually the server forgot to include its intermediate.

npm runs on Node.js, so the same NODE_EXTRA_CA_CERTS file fixes npm too. npm's cafile replaces the default CA list instead, so a cafile holding only the intermediate fails with UNABLE_TO_GET_ISSUER_CERT.

They report OpenSSL's error 20 for the same short chain, while Node.js reports error 21. curl and git print that same text for an unknown root too, so the wording alone can't tell the cases apart.

Only when the missing issuer is a CA your OS trusts, such as a company CA installed by IT. The flag, added in Node.js 22.15 and 23.8, reads only certificates marked as trusted, which rarely include a public site's intermediate.

No. It turns off certificate checks for every TLS connection in the process, hiding this error and exposing traffic to interception; the Node.js docs strongly discourage it.

A proxy that tunnels HTTPS with CONNECT relays the site's certificates unchanged, so the site is the cause. An inspection proxy that re-signs traffic and sends only its own leaf causes it too; the SELF_SIGNED_CERT_IN_CHAIN page covers trusting its CA.

Proxy usage your scripts can check

Geonode's usage endpoint returns the bandwidth you've used, so a scheduled job can log it.