Skip to content

Troubleshoot a certificate the browser will not trust

Each section is a symptom. Start with the one that matches what you are seeing.

Chrome trusts it but Firefox shows a warning

Firefox does not read the OS trust store. It reads its own NSS database, and localca writes to that only when certutil is available.

Check what happened:

state, err := authority.Installed(ctx)
fmt.Println(state.System, state.NSS) // true false → system worked, NSS did not

If NSS is false, install the NSS tools and install again:

sudo apt-get install -y libnss3-tools   # Debian, Ubuntu
sudo dnf install -y nss-tools           # Fedora, RHEL
brew install nss                        # macOS
_, err := authority.Install(ctx, localca.StoreNSS) // user-level, no password prompt

Restart Firefox afterwards. It caches trust decisions for the lifetime of the process.

Installed reports NSS: false even though Firefox trusts it

The NSS state check reports true only when the root is present in every NSS profile found — every Firefox profile directory, plus ~/.pki/nssdb on Linux. A profile created after you installed drags the answer back to false.

Run Install(ctx, localca.StoreNSS) again; it is idempotent and will fill in the profiles that are missing it.

The install returns ErrElevationUnavailable

localca refuses to start a password prompt that nothing can answer. You get this error when the process is not root, and either there is no sudo on PATH or standard input is not an interactive terminal — a systemd service, a CI job, a GUI application, a process with its input piped from somewhere.

Options, in rough order of preference:

  • Run the install once from a terminal. Trust installation is a one-off; the long-running service does not need to do it. Give your tool a command that calls Install and tell the user to run it once.
  • Install into NSS only, which needs no elevation: authority.Install(ctx, localca.StoreNSS). Firefox and Chromium-on-Linux trust the result; Chrome-on-macOS, Safari, Edge and Go's HTTP client do not.
  • Run as root, which the pre-flight accepts without prompting. Reasonable in a container, not on a developer's machine.
  • Fall back to an untrusted certificate and let the user click through, if the workflow survives that.

Detect it with errors.Is(err, localca.ErrElevationUnavailable); it survives wrapping.

Nothing appeared in the terminal and the install just failed

Attach a logger. localca defaults to a discard handler, so by default it says nothing at all — including the line explaining why a password prompt is about to appear and the warning that your root CA is expiring.

a, err := localca.New(cfg, localca.WithLogger(slog.Default()))

Note that sudo, certutil and security write straight to the terminal rather than through your logger, so their messages will not appear in structured log output either way.

certutil is unavailable and I cannot install packages

certutil is a small CLI over libnss3, and libnss3 is usually already present on any machine with a browser. On a Debian-family box you can unpack just the binary, without root and without installing anything:

apt-get download libnss3-tools
dpkg -x libnss3-tools_*.deb ./nsstools
export PATH="$PWD/nsstools/usr/bin:$PATH"

Anything localca runs from that shell now finds certutil and the NSS install works.

If that is not possible either, nothing breaks: the system store is what gates provisioning, so EnsureServed succeeds and only Firefox is left untrusting.

The browser trusted it yesterday and does not today

Two likely causes.

The leaf expired, because nothing renewed it. Leaves last 90 days by default and are re-minted within seven days of expiry — but only when EnsureServed runs, which is normally once at start-up. A process that has been up for longer than the leaf's lifetime is still serving the certificate it loaded on the day it started, expired or not. Restart it, and call EnsureServed again on start-up if you are not already.

The root was re-minted. If the data directory was deleted, moved, or pointed at a temporary path, a new root was created and the old one — still in your trust store — no longer matches. Confirm by comparing the stored root against what your trust store holds:

openssl x509 -in "$DATA_DIR/rootCA.pem" -noout -subject -serial -dates

Then install the new root, and remove the stale entries by hand. They are named <AppName>-localca-<serial>.

I purged and provisioned again, and now nothing trusts the certificate

Purge() deletes the root but leaves the cached leaf, and the next EnsureServed reuses that leaf under a brand-new root. The served chain is broken until the leaf ages out.

Delete the leaf files and provision again:

rm -f "$DATA_DIR"/leaf.pem "$DATA_DIR"/leaf-key.pem

I removed the root from Keychain or ca-certificates and localca will not reinstall it

localca reads system-store membership from trust-install.json, which records its own installs. It does not notice a removal made any other way, so it believes the root is still trusted and skips the install.

Delete the marker and provision again:

rm -f "$DATA_DIR/trust-install.json"

Every EnsureServed call writes a new certificate

You have set LeafTTL to seven days or less. A cached leaf is renewed once it is within seven days of expiry, and that window is fixed — so a leaf that short is already due for renewal the moment it is minted. Raise LeafTTL above seven days; the default of 90 is usually right.

WARN root CA expiring soon on every start-up

The root has less than one leaf lifetime left, so every leaf is being shortened to end when the root does. Renew the root:

_ = authority.Uninstall(ctx, localca.Purge())
rm -f "$DATA_DIR"/leaf.pem "$DATA_DIR"/leaf-key.pem

Then call EnsureServed again. It mints a fresh root and prompts for elevation once more.

A stale .rootCA-key.pem.*.tmp is sitting in the data directory

A write was interrupted before its cleanup ran. localca writes every file to a temporary name and renames it into place, so the real files are intact. Delete the temporary file.

localca: root cert present but key missing

rootCA.pem exists without rootCA-key.pem. localca will not silently replace a root that may still be installed in your trust store, so it refuses to continue.

Remove the root from your trust stores by hand if it is still there — its name is <AppName>-localca-<serial>, and the serial is in the output of:

openssl x509 -in "$DATA_DIR/rootCA.pem" -noout -serial

Then delete rootCA.pem and provision again.

Verifying the chain from the command line

To see what is actually being served, independent of any browser:

openssl s_client -connect localhost:8443 -servername localhost \
  -CAfile "$DATA_DIR/rootCA.pem" </dev/null 2>/dev/null | grep -E 'Verify return code|subject='

Verify return code: 0 (ok) means the leaf chains correctly to the root. If that passes but a browser still complains, the problem is trust-store installation, not the certificate.