MervCodes

Tech Reviews From A Programmer

How to Fix SSL Certificate Errors in Local Dev

1 min read

How to Fix SSL Certificate Errors in Local Dev

You added https:// to your local server, opened the browser, and were greeted by a wall of red: NET::ERR_CERT_AUTHORITY_INVALID, SSL certificate problem: self-signed certificate, or the dreaded Your connection is not private. If you have spent more than five minutes building web apps, you have hit this. The good news: local SSL errors are almost always a trust problem, not a broken-encryption problem, and they are very fixable once you understand what your machine is actually complaining about.

This guide walks through why these errors happen and gives you battle-tested fixes — from the quick hack you use for a throwaway script to the proper setup you want on a real project.

Why SSL Errors Happen Locally

In production, you get a certificate from a Certificate Authority (CA) like Let's Encrypt. Your browser and operating system ship with a list of CAs they trust, so a cert signed by one of them is accepted silently.

Locally, there is no public CA willing to issue a certificate for localhost or myapp.test. So developers generate their own self-signed certificates. The encryption works perfectly fine — but nobody trusts the signer, because the signer is you. The browser sees a certificate signed by an unknown authority and refuses to proceed.

Most local SSL errors fall into one of these buckets:

  • Untrusted issuer — the cert is self-signed and your OS/browser doesn't trust the signer.
  • Hostname mismatch — the cert was issued for localhost but you're visiting 127.0.0.1, or the cert lacks a Subject Alternative Name (SAN).
  • Expired certificate — self-signed certs often have short lifespans. Modern browsers reject certs from publicly-trusted CAs valid for more than 200 days (as of March 2026, down from the previous ~398-day cap — with further reductions planned); for locally-trusted CAs (such as those installed via mkcert), the effective limit is approximately 825 days.
  • Missing SAN extension — since 2017, Chrome ignores the Common Name field entirely and requires a SAN. Old tutorials that only set CN produce certs Chrome rejects.

Knowing which bucket you're in tells you which fix to reach for.

The Quick Fix (and Why You Shouldn't Rely On It)

If you just need a one-off API call to work, you can tell your client to skip verification:

# curl
curl -k https://localhost:3000/api

# Node.js (per-process, NOT in production)
NODE_TLS_REJECT_UNAUTHORIZED=0 node app.js

# Python requests
requests.get("https://localhost:3000", verify=False)

In the browser, you can click Advanced → Proceed to localhost (unsafe), or type the bypass phrase thisisunsafe directly on a Chrome warning page.

These work, but treat them as duct tape. Disabling verification globally trains you to ignore real certificate problems, breaks tools that legitimately need trust, and can leak into code that ships. Use it to unblock yourself for thirty seconds, then set up a real solution.

The Right Fix: Use mkcert

mkcert is the de facto standard for local HTTPS. It creates a local CA, installs it into your system and browser trust stores, and issues certificates signed by that CA. Because your machine now trusts the CA, every cert it issues is trusted automatically — no warnings.

# macOS
brew install mkcert
brew install nss   # needed for Firefox

# Install the local CA into your trust stores
mkcert -install

# Generate a cert for the hostnames you use
mkcert localhost 127.0.0.1 ::1 myapp.test

That last command produces two files, something like localhost+3.pem (the certificate) and localhost+3-key.pem (the private key). Point your dev server at them:

// Node.js / Express example
const https = require("https");
const fs = require("fs");

https
  .createServer(
    {
      key: fs.readFileSync("localhost+3-key.pem"),
      cert: fs.readFileSync("localhost+3.pem"),
    },
    app
  )
  .listen(3000);

Restart, visit https://localhost:3000, and you should see a clean padlock. mkcert handles SANs correctly, so hostname-mismatch errors disappear too.

Fixing Hostname Mismatch Errors

If you still get ERR_CERT_COMMON_NAME_INVALID after generating a cert, the certificate doesn't list the exact host you're visiting. The fix is to include every name and IP in the SAN list when you generate it:

mkcert localhost 127.0.0.1 ::1 myapp.local "*.myapp.local"

If you're rolling certs manually with OpenSSL, you must add a SAN section — the Common Name alone won't satisfy modern browsers:

openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem \
  -days 365 -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

Also make sure custom hostnames like myapp.test actually resolve. Add them to your hosts file:

# /etc/hosts (macOS/Linux) or C:\Windows\System32\drivers\etc\hosts
127.0.0.1   myapp.test

Trusting a Certificate Manually

Sometimes you inherit a self-signed cert you can't regenerate — say, from a Docker image or a teammate's setup. You can add it to your OS trust store directly.

macOS:

sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain cert.pem

Linux (Debian/Ubuntu):

sudo cp cert.pem /usr/local/share/ca-certificates/myapp.crt
sudo update-ca-certificates

Windows (PowerShell as admin):

Import-Certificate -FilePath "cert.pem" `
  -CertStoreLocation Cert:\LocalMachine\Root

After importing, fully quit and reopen your browser — trust stores are read at startup, so a refresh won't pick up the change. Firefox is the exception: it keeps its own trust store and needs the cert imported via Settings → Privacy & Security → Certificates → View Certificates, or NSS installed so mkcert can do it for you.

Handling Node, Docker, and CI Environments

Browsers aren't the only thing that verifies certs. Node, Python, and other runtimes have their own CA bundles, and they often don't read your OS trust store automatically.

For Node.js, point it at your CA file instead of disabling verification:

export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"

For Docker, copy the CA into the image and update its trust store:

COPY rootCA.pem /usr/local/share/ca-certificates/rootCA.crt
RUN update-ca-certificates

In CI, commit a CA certificate (never the private key of a real CA) or generate a fresh one during the build, then install it before your tests run. This keeps verification on everywhere, which catches genuine misconfigurations early instead of in production.

A Quick Diagnostic Checklist

When a local SSL error appears, work through this in order:

  1. Read the exact error code. AUTHORITY_INVALID means trust; COMMON_NAME_INVALID means hostname; DATE_INVALID means expiry.
  2. Inspect the cert: openssl s_client -connect localhost:3000 -servername localhost shows what's actually being served.
  3. Check the SAN list: openssl x509 -in cert.pem -noout -text | grep -A1 "Subject Alternative Name".
  4. Confirm the CA is installed: look for your mkcert root in Keychain Access / certmgr / /etc/ssl/certs.
  5. Restart the browser after any trust-store change.

Ninety percent of the time, step 1 points you straight to the fix.

FAQ

Is it safe to use curl -k or NODE_TLS_REJECT_UNAUTHORIZED=0? For a throwaway local request, yes. As a habit or anything that touches shared/CI code, no — it disables verification entirely and can mask real attacks or misconfigurations. Prefer trusting a local CA with mkcert.

Why does my cert work in Chrome but not Firefox? Firefox maintains its own certificate store separate from the OS. Install NSS (brew install nss) before running mkcert -install, or import the root CA manually through Firefox's settings.

Why does Chrome reject my OpenSSL cert even though it has a Common Name? Chrome has ignored the Common Name field since version 58. You must include a Subject Alternative Name (SAN) extension listing every hostname and IP. Regenerate with -addext "subjectAltName=..." or just use mkcert, which does it automatically.

Do I need to regenerate certs when they expire? Yes. With mkcert, certs issued by your local CA can be valid for up to approximately 825 days — the 200-day maximum (as of March 2026) applies only to publicly-trusted CAs, not to locally-installed ones. Just rerun the generation command — the CA stays installed, so no new trust prompts.

Can I share one cert across my whole team? You can share a cert and key for convenience, but a cleaner approach is for each developer to run mkcert -install on their own machine. That keeps private keys off shared drives and avoids one expired cert blocking the whole team.

My cert is trusted in the browser but Node still throws UNABLE_TO_VERIFY_LEAF_SIGNATURE. Node doesn't read the OS trust store. Set NODE_EXTRA_CA_CERTS to your CA root ($(mkcert -CAROOT)/rootCA.pem) so Node trusts the same authority your browser does.

Wrapping Up

Local SSL errors look intimidating but almost always come down to trust and hostnames. Reach for curl -k only to unblock yourself for a moment, then install mkcert so your machine trusts a local CA — that single step eliminates the vast majority of warnings across browsers and tools. For everything else, read the error code carefully, verify the certificate's SAN list, and remember that runtimes like Node and Docker need to be told about your CA separately from the OS. Set this up once per machine and HTTPS in local dev becomes a non-event.

Sources

Related Articles