Trust & security model¶
localca deliberately makes one strong trade-off: it installs a certificate
authority you control into your machine's trust stores. This page explains what that
means, why it's safe when done this way, and where the boundaries are.
The problem it solves¶
A browser will not trust a certificate unless it chains to a CA already in its trust
store. For a public website you get that from a public CA (Let's Encrypt et al.). For
localhost and LAN IPs there is no public CA that will issue to you, and operating a
publicly-trusted CA is out of reach for a small org. The mkcert insight: run a local
CA, trusted only on your machine, and mint whatever local certs you need offline.
Per-machine root, never a shared key¶
Every install generates its own root key pair, stored under the data dir with the
key file at 0600. localca never ships or generates a shared root.
This is the single most important rule. A CA root key that is bundled into a distributed binary (or otherwise shared between machines) is a universal man-in-the-middle key: anyone who extracts it can forge a trusted certificate for any site and intercept traffic for every user who trusts that root. So the root:
- is generated locally, per machine;
- never leaves the machine;
- has a blast radius of exactly one machine;
- has nothing central to revoke.
That is the mkcert trade-off, stated plainly: convenience and offline operation in exchange for a trust anchor that lives on your laptop and must be protected like any other private key.
Consent is the OS elevation prompt¶
Installing a root into the system trust store requires OS elevation, which localca
obtains through sudo at an interactive terminal on Linux and macOS (Windows is
not supported). localca treats that prompt as the
consent gate: it never installs a system-trusted root silently. The NSS (Firefox) path
is user-level and needs no elevation, only certutil.
Every install is reversible: Authority.Uninstall removes the root from the trust
stores, and Purge additionally deletes the stored key.
The system store is the anchor; NSS is best-effort¶
localca considers the root "trusted enough" to serve when the system store trusts
it, because that store is read by Chrome, Safari, Edge, and Go's own HTTP client. The NSS
store (Firefox, some Chromium builds) is installed alongside when certutil is
available, but it never gates provisioning: many hosts (headless servers, CI) have no
certutil, and requiring it would break them. Firefox trust is therefore a bonus, not a
prerequisite.
Who can read the root key¶
The root key is a 0600 file owned by the user who provisioned it, and that is the whole
of its protection. Anything running as that user can read it: another program, a shell
script, a compromised dependency in a build you run. localca does not put it in a keychain
or behind a passphrase; why it is a file
explains that choice.
The consequence to hold on to is that a local development CA moves the security boundary to
"who can run code as me on this machine". If that boundary is already broken, the CA key is
not the biggest problem. It is still a problem, and the answer is Uninstall and re-provision
rather than anything cleverer.
Short-lived, scoped leaves¶
Server certificates are minted short-lived (90 days by default) and scoped to exactly the hostnames/IPs you ask for, with no long-lived wildcards. A leaf is re-minted when it nears expiry or when your host list changes.
A leaf is never issued with a NotAfter beyond its root's: a certificate that
outlives the CA that signs it produces a confusing chain-validation error in clients.
As the root approaches the end of its ~10-year life, localca clamps each minted leaf
to the root's expiry and logs a WARN ("root CA expiring soon") so an operator knows to
renew the root before it lapses. Once it has lapsed, minting a leaf is refused with an
error naming the root's NotAfter: clamping to an expired root would produce a leaf whose
validity ends before it begins.
Durable writes¶
The root key and cert, and each cached leaf, are written atomically: localca
writes to a temp file in the data dir, sets the file mode (0600 for keys), then renames
it over the target. Because a rename is atomic on a single filesystem, a crash or
power-loss mid-write can never leave a truncated key at the target. A reader always sees
either the complete previous material or the complete new material, so an interrupted
Install can't brick the local CA by half-writing its root key.
A private CA for mutual TLS¶
Everything above is about the installed root. A Private CA, from NewPrivate, is a
second kind with a different job: a service runs it to enrol other machines for mutual TLS.
It serves the service's own TLS, and signs a client certificate for each machine from a
certificate signing request (CSR) the machine sends.
That job is why the two kinds never mix. An installed root is trusted by its machine for every HTTPS site, with no name constraints. If it signed CSRs, and a CSR could ask for a website's name or for server use, any machine allowed to enrol could get a certificate its signing host would trust as any website. So:
- A private root is never installed. A
Privatehas noInstall. Its files have names anAuthoritynever reads, andNewPrivaterefuses a data directory anAuthorityalready uses.localcacannot stop you installing it by hand, sinceRoot.PEMis exactly what a trust store takes, but it never does so itself. - An installed root never signs a CSR. An
Authorityhas no method that takes one. - A CSR contributes only its public key.
SignClientchecks that the CSR's signature verifies, which proves the machine holds the key, and that its subject is exactly the CommonName the caller expects. A CSR asking for anything more, such as a SAN, server use, CA powers or a challenge password, is refused, not trimmed. The certificate is built from the caller's request: that name,ClientAuthonly,DigitalSignatureonly, not a CA, no SANs.
The second rule is what bounds the first. A ClientAuth-only certificate is refused as a
server certificate, even when it chains to a root the verifier trusts. That was measured on
2026-10-21 against Go 1.27.1's crypto/tls, OpenSSL 3.0.13, curl 8.5.0 and Chrome 153; the
spike report
has the method and the output. So even a private root installed by hand gives a CSR no way to
impersonate a site. The server certificates a private root also issues come only from its
owner, for hosts the owner names.
What changes is the blast radius. An installed root's key compromises one machine. A
private root's key compromises every machine enrolled with it, and localca keeps no record
of what it signed. The defence is the caller's: record each certificate's fingerprint when
it is issued, accept only recorded ones, and replace the record on renewal. A stolen key can
then mint certificates, but nothing accepts them.
What it is not¶
- Not a public CA. The root is trusted only where you install it. It cannot and must not be used to issue certificates other people rely on.
- Not detection of out-of-band changes. System-store membership is tracked in a
marker file reflecting
localca's own installs. If you remove the root through other means, re-run the install: the same idempotent, mkcert-style behaviour. - Not the whole story. This is Layer 1 of a two-layer strategy. A future public-ACME path (Layer 2) can issue publicly-trusted certs for real hostnames resolving to local IPs, with nothing installed, but it needs standing infrastructure, so the local CA remains the permanent offline / CI / air-gapped fallback.
The full list of what localca will not do, including the platforms where trust
installation does not work at all, is in limitations.