Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

mkcert Administrator & Developer Guide

Generated April 2026 from FiloSottile/mkcert at 1c1dc4e

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is the operator's, security architect's, and contributor's reference for mkcert at commit 1c1dc4e. It covers architecture, the complete configuration surface, deployment in shared and CI environments, operational concerns (rotation, backup, multi-CA management), the security model and threat surface, performance characteristics, full developer internals, and the integration points other systems care about.

For day-to-day usage (issuing a cert, troubleshooting browser trust), see the mkcert User Guide in the same output directory.

1. Architecture Overview

mkcert is a single statically-linked Go binary. It has no daemon, no IPC, no persistent state outside two PEM files on disk, and no network calls. Every invocation does one of three things: print metadata, manage CA trust in OS/browser/Java trust stores, or sign a leaf certificate using a locally-stored RSA-3072 root CA.

The binary is roughly 41 KB of source across seven Go files. Trust-store integration is split across platform-specific files behind Go's filename-based build tag convention (truststore_darwin.go, truststore_linux.go, truststore_windows.go, plus truststore_nss.go and truststore_java.go for cross-platform stores).

Note

The architectural design choice that drives everything else is "no daemon, no shared state". Each invocation reads rootCA.pem and rootCA-key.pem from $CAROOT from scratch, performs one operation, and exits. This makes mkcert trivial to embed in scripts, CI pipelines, and developer onboarding flows — but it also means there is no centralized audit log of issued certificates. If you need that, wrap mkcert in a logging script.

Component diagram

+--------------------------------------------------------------+
|                       mkcert (single binary)                  |
|                                                               |
|  +------------+   +-----------------+   +-------------------+ |
|  | main.go    |   | cert.go         |   | truststore_*.go   | |
|  | (CLI/flag, |-->| (CA + leaf      |-->| (per-platform     | |
|  |  dispatch) |   |  cert issuance) |   |  trust store I/O) | |
|  +------------+   +-----------------+   +-------------------+ |
|         |                  |                       |          |
|         v                  v                       v          |
|  +-------------+    +-------------+      +-------------------+|
|  | flag pkg    |    | crypto/x509 |      | system trust:     ||
|  | env vars    |    | crypto/rsa  |      |   security        ||
|  | (CAROOT,    |    | crypto/ecdsa|      |   (macOS Keychain)||
|  |  TRUST_*,   |    | encoding/pem|      |   crypt32.dll     ||
|  |  JAVA_HOME) |    | go-pkcs12   |      |   (Windows)       ||
|  +-------------+    +-------------+      |   tee+update-ca-* ||
|                                          |   (Linux)         ||
|                                          | nss: certutil     ||
|                                          | java: keytool     ||
|                                          +-------------------+|
+--------------------------------------------------------------+
              |
              v
        +------------------------+
        | $CAROOT (per-user dir) |
        |   rootCA.pem  (0644)   |
        |   rootCA-key.pem (0400)|
        +------------------------+

Component inventory

Component Path Role
CLI entry point + dispatch main.go Parse flags, validate flag combos, instantiate the mkcert struct, call Run.
CA + leaf cert issuance cert.go Generate root CA, generate leaf keys (RSA-2048 / ECDSA-P256), construct x509.Certificate templates, encode PEM/PKCS#12, sign CSRs.
macOS system trust store truststore_darwin.go Shells to security add-trusted-cert and security trust-settings-{import,export}; mutates the trust-settings plist.
Linux system trust store truststore_linux.go Detects ca-trust layout (/etc/pki/ca-trust/..., /usr/local/share/ca-certificates/..., etc.), copies the CA into the right path, runs the right update-ca-* command.
Windows system trust store truststore_windows.go Calls crypt32.dll directly via syscall.NewLazyDLL (CertOpenSystemStoreW, CertAddEncodedCertificateToStore, CertEnumCertificatesInStore, CertDeleteCertificateFromStore).
NSS (Firefox/Chromium) truststore_nss.go Locates certutil and NSS profile dirs (Firefox profiles + Chromium ~/.pki/nssdb + snap variants); shells to certutil -A/-D/-V.
Java keystore truststore_java.go Reads $JAVA_HOME, locates keytool and cacerts, calls keytool -importcert/-delete/-list with the hardcoded changeit storepass.
Module manifest go.mod Module path filippo.io/mkcert, Go 1.18, three direct deps (see Section 9).

Technology stack

Layer Choice Why
Language Go 1.18+ Static binary, easy cross-compilation, strong stdlib crypto/x509.
Distribution Single binary per OS/arch No runtime, no installer; package managers wrap the same binary.
Crypto primitives Go stdlib crypto/rsa, crypto/ecdsa, crypto/x509 Audited, deterministic, well understood.
PKCS#12 software.sslmate.com/src/go-pkcs12 v0.2.0 Stdlib has read-only PKCS#12; this dep adds encoding.
IDN / punycode golang.org/x/net/idna Normalizes IDN domains so certs cover the canonical form.
macOS plist howett.net/plist v1.0.0 Reads/writes the trust-settings plist for security trust-settings-import.

Data flow: mkcert -install

  user                     mkcert binary                    OS/browser
  ----                     -------------                    ----------
   │                            │                                │
   │  $ mkcert -install         │                                │
   │ ─────────────────────────▶ │                                │
   │                            │  getCAROOT()                   │
   │                            │  os.MkdirAll(CAROOT, 0755)     │
   │                            │  if rootCA.pem missing:        │
   │                            │     newCA() → write 0644+0400  │
   │                            │  loadCA() → parse PEM          │
   │                            │  if storeEnabled("system"):    │
   │                            │ ─── installPlatform() ────────▶ keychain/registry/ca-cert dir
   │                            │  if storeEnabled("nss"):       │
   │                            │ ─── installNSS()      ────────▶ certutil -A on each NSS DB
   │                            │  if storeEnabled("java"):      │
   │                            │ ─── installJava()     ────────▶ keytool -importcert
   │                            │                                │
   │  ⚡️ "now installed in …"    │                                │
   │ ◀───────────────────────── │                                │

Data flow: mkcert <hosts>

  user                       mkcert                            disk
  ----                       ------                            ----
   │                            │                                │
   │  $ mkcert example.test     │                                │
   │ ─────────────────────────▶ │                                │
   │                            │  loadCA()  ◀──────────────── rootCA.pem + rootCA-key.pem
   │                            │  validate hosts (DNS/IP/URI/email)
   │                            │  generateKey(false)            │
   │                            │  build x509.Certificate tpl    │
   │                            │  CreateCertificate(tpl, caCert,│
   │                            │     pub, caKey)                │
   │                            │  PEM-encode cert (0644) ─────▶ ./<name>.pem
   │                            │  PEM-encode key  (0600) ─────▶ ./<name>-key.pem
   │                            │  print summary + expiry        │
   │ ◀───────────────────────── │                                │

Note

mkcert never serializes state between invocations. All in-memory variables (hasJava, hasCertutil, the detected SystemTrustCommand, etc.) are re-initialized in init() blocks on every run by inspecting the environment and binary paths. This is what gives mkcert its consistent behavior across freshly-cloned dotfiles, CI runners, and long-lived developer machines — but it also means changes in environment (e.g. JAVA_HOME set after install) are picked up immediately on the next invocation, which is sometimes surprising.

2. System Requirements

mkcert is intentionally minimal. Beyond the binary, the requirements are all about which trust stores you want it to talk to.

Hard requirements

Requirement Detail
OS Linux (any distro), macOS (any maintained version), or Windows. The Windows trust-store integration calls into crypt32.dll, which is part of every Windows install.
Architecture amd64, arm, or arm64 for prebuilt binaries. Build from source for anything else.
Disk < 30 MB for the binary; CA storage typically < 10 KB.
Memory Minimal (single-shot CLI). Issuing a cert peaks well under 50 MB RSS.

Soft requirements (per trust store)

Trust store Requires Why
OS / system (any platform) Root or sudo access Modifying the system trust store needs elevated privileges on every supported OS. mkcert's commandWithSudo() helper wraps invocations in sudo when not already running as UID 0.
NSS (Firefox / Chromium / Chrome) certutil on PATH (or installed via brew install nss on macOS) NSS uses its own DB format (cert9.db or older cert8.db); the only supported way to mutate it is via certutil.
Java $JAVA_HOME set, with bin/keytool and lib/security/cacerts (or jre/lib/security/cacerts) reachable mkcert calls keytool -importcert against the cacerts file with the hardcoded password changeit.

Note

mkcert never installs certutil, keytool, or any package itself. It detects what's available and prints a remediation instruction when a trust store is detected but missing its tooling — for example, Install "certutil" with "apt install libnss3-tools" and re-run "mkcert -install". This makes failure modes obvious.

Detected paths (Linux system trust)

mkcert auto-detects the Linux system-trust layout by checking for the existence of well-known directories, in this exact priority:

Directory SystemTrustFilename template Refresh command
/etc/pki/ca-trust/source/anchors/ …/%s.pem update-ca-trust extract
/usr/local/share/ca-certificates/ …/%s.crt update-ca-certificates
/etc/ca-certificates/trust-source/anchors/ …/%s.crt trust extract-compat
/usr/share/pki/trust/anchors …/%s.pem update-ca-certificates

If none exist, mkcert reports Installing to the system store is not yet supported on this Linux 😣 and falls through to NSS/Java.

Detected paths (NSS)

mkcert searches three Chromium NSS database paths plus an OS-specific list of Firefox profile dirs, then a list of Firefox install paths to decide if NSS is "interesting" on this machine.

Path category Searched paths
Chromium NSS DBs ~/.pki/nssdb, ~/snap/chromium/current/.pki/nssdb, /etc/pki/nssdb
Firefox profiles (macOS) $HOME/Library/Application Support/Firefox/Profiles/*
Firefox profiles (Linux) $HOME/.mozilla/firefox/*, $HOME/snap/firefox/common/.mozilla/firefox/*
Firefox profiles (Windows) %USERPROFILE%\AppData\Roaming\Mozilla\Firefox\Profiles
Firefox binaries (probed) /usr/bin/firefox, /usr/bin/firefox-nightly, /usr/bin/firefox-developer-edition, /snap/firefox, /Applications/Firefox.app, /Applications/FirefoxDeveloperEdition.app, /Applications/Firefox Developer Edition.app, /Applications/Firefox Nightly.app, C:\Program Files\Mozilla Firefox

If a Firefox profile uses an NSS DB version 2 (cert8.db), mkcert prefixes the path with dbm:; for v3 (cert9.db), sql:.

3. Configuration Reference

mkcert has zero configuration files. Everything is either a CLI flag or one of three environment variables.

CLI flags (complete)

Flag Type Default Description
-install bool false Create the local CA (if missing) and install it into all enabled trust stores.
-uninstall bool false Remove the local CA from all enabled trust stores. CA files on disk are NOT deleted.
-CAROOT bool false Print the resolved CAROOT directory and exit.
-help bool false Print short and advanced usage and exit.
-version bool false Print the mkcert version and exit.
-cert-file FILE string (auto: ./<name>.pem) Path to write the leaf certificate to.
-key-file FILE string (auto: ./<name>-key.pem) Path to write the leaf private key to.
-p12-file FILE string (auto: ./<name>.p12) Path to write the PKCS#12 bundle to. Only meaningful with -pkcs12.
-client bool false Add extKeyUsage = clientAuth to the leaf cert (in addition to serverAuth if any DNS/IP/URI SANs are present).
-ecdsa bool false Generate the leaf key as ECDSA P-256 instead of the default RSA-2048. The CA key is unaffected (always RSA-3072).
-pkcs12 bool false Output a PKCS#12 (.p12 / .pfx) bundle. The bundle includes the leaf, the leaf key, and the root CA. Encrypted with the hardcoded password changeit.
-csr CSR string "" Sign the supplied CSR file (PEM, type CERTIFICATE REQUEST or NEW CERTIFICATE REQUEST) instead of generating a fresh key.

Flag combination rules (enforced in main.go)

Combination Result
-install -uninstall ERROR: you can't set -install and -uninstall at the same time
-CAROOT + (-install or -uninstall) ERROR: you can't set -[un]install and -CAROOT at the same time
-csr + (-pkcs12 or -ecdsa or -client) ERROR: can only combine -csr with -install and -cert-file
-csr + positional host arg(s) ERROR: can't specify extra arguments when using -csr

Environment variables (complete)

Variable Type Default Description Required?
CAROOT path (per-platform; see table below) Override the directory where rootCA.pem and rootCA-key.pem are stored. Used by every command. No
TRUST_STORES comma-list (all detected) Allow-list of trust stores. Valid entries: system, java, nss. Empty/unset = all. No
JAVA_HOME path (read from environment) Required only if you want Java trust integration. mkcert looks for $JAVA_HOME/bin/keytool[.exe] and $JAVA_HOME/lib/security/cacerts (or …/jre/lib/security/cacerts for older JDKs). Java only
LocalAppData path (Windows-managed) Used to compute the default CAROOT on Windows. Windows
XDG_DATA_HOME path unset Used to compute the default CAROOT on Linux when set. No
HOME path (shell-managed) Used to compute the default CAROOT on Linux/macOS. macOS, Linux
USERPROFILE path (Windows-managed) Used to compute Firefox profile path on Windows. Windows

Default CAROOT resolution (getCAROOT() in main.go)

1. If $CAROOT is non-empty → use it as-is.
2. Else if runtime.GOOS == "windows" → $LocalAppData/mkcert
3. Else if $XDG_DATA_HOME != ""  → $XDG_DATA_HOME/mkcert
4. Else if runtime.GOOS == "darwin" and $HOME != ""  → $HOME/Library/Application Support/mkcert
5. Else if $HOME != ""  → $HOME/.local/share/mkcert
6. Else  → "" (mkcert exits with "failed to find the default CA location")

File outputs

File Owner Mode Purpose
$CAROOT/rootCA.pem invoking user 0644 The root CA certificate. Safe to share.
$CAROOT/rootCA-key.pem invoking user 0400 The root CA private key. Equivalent to "anyone with this can MITM your machine".
./<name>.pem (or -cert-file) invoking user 0644 A leaf certificate.
./<name>-key.pem (or -key-file) invoking user 0600 A leaf private key.
./<name>.p12 (or -p12-file) invoking user 0644 A PKCS#12 bundle (cert + key + CA), encrypted with changeit.

Important

The 0400 permission on rootCA-key.pem is intentional and tight. Do not relax it. The 0600 on leaf keys is similar. mkcert sets these explicitly via ioutil.WriteFile and assumes the umask is sane; verify after install on shared / CI hosts.

Hardcoded constants you might care about

Constant Value Where Rationale
Root CA validity 10 years cert.go newCA(): NotAfter: time.Now().AddDate(10, 0, 0) Long enough to outlast most dev machines, short enough to age out.
Leaf cert validity 2 years 3 months cert.go makeCert(): expiration := time.Now().AddDate(2, 3, 0) Below the 825-day ceiling Apple platforms enforce.
Root CA key size RSA-3072 cert.go generateKey(true) NIST 128-bit security level for long-lived CAs.
Leaf key size RSA-2048 (default) or ECDSA-P256 (-ecdsa) cert.go generateKey(false) RSA-2048 is the broad-compat default; ECDSA P-256 is smaller and faster.
Root CA filename rootCA.pem main.go const rootName Consistent across platforms.
Root CA key filename rootCA-key.pem main.go const rootKeyName Consistent across platforms.
Java keystore password changeit truststore_java.go storePass The de facto Java cacerts default.
PKCS#12 password changeit cert.go pkcs12.Encode(…, "changeit") The de facto keystore default.
Sudo prompt Sudo password: main.go commandWithSudo() Custom prompt when shelling to sudo --prompt=….

4. Deployment & Scaling

mkcert is a developer tool. There is no server to deploy, no service to manage. The interesting deployment questions are: how to provision it across a team, how to use it in CI, and how to manage multiple CAs.

Single-developer machine

The default install — package manager + mkcert -install — is the documented happy path. Done once, lasts until the next OS reinstall.

Team rollout

Three viable patterns, in increasing rigor:

Pattern A: every developer makes their own CA

Each dev runs brew install mkcert; mkcert -install. CAs are machine-local. Pros: simplest, no shared secrets. Cons: certificates issued on Alice's machine aren't trusted on Bob's machine without Alice exporting rootCA.pem and Bob importing it.

Pattern B: shared root CA, distributed via secrets manager

The team agrees on a single CA. One person creates it (mkcert -install), tars $CAROOT, and stores the archive in your team password manager (1Password, Vault, etc.). New developers download it, untar into their $CAROOT, run mkcert -install. Pros: shared trust across the team — one cert works for everyone. Cons: rootCA-key.pem is now a shared secret with the same blast radius as giving every dev a copy of the CA. Anyone with access can MITM any HTTPS connection on any team machine.

Caution

If you adopt Pattern B, treat rootCA-key.pem exactly like a production database credential. Rotate it whenever someone leaves the team; revoke and regenerate when you suspect compromise. There is no revocation mechanism in mkcert itself — rotation means mkcert -uninstall && rm -rf $CAROOT && mkcert -install on every machine and re-issuing every leaf.

Pattern C: per-developer CA + shared "team trust" CA

Each developer keeps a personal CA (default $CAROOT), and you additionally provision a "team CA" under a different $CAROOT for shared dev environments (review apps, integration test stacks). The team CA's rootCA.pem (public only) ships with team scripts; the team CA's private key lives only on a CI agent or a single trusted host. Pros: blast radius of a leaked personal CA is one machine. Cons: more moving parts.

CI / ephemeral environments

mkcert works fine in CI, but the defaults aren't right for ephemeral runners. Two changes:

  1. Set CAROOT explicitly. In containers $XDG_DATA_HOME and $HOME may both be empty, in which case getCAROOT() returns "" and mkcert exits. export CAROOT=/tmp/mkcert (or a job-scoped path) fixes this.
  2. Set TRUST_STORES=system (or nss) so mkcert doesn't try to talk to a Java install or browser that isn't there. This avoids a pile of warnings and the extra time spent probing.
# Example GitHub Actions step
- name: Issue dev cert
  env:
    CAROOT: ${{ runner.temp }}/mkcert
    TRUST_STORES: system
  run: |
    mkdir -p "$CAROOT"
    mkcert -install
    mkcert -cert-file cert.pem -key-file key.pem localhost

Tip

For CI test jobs that just need to verify mkcert-issued certs, you often don't need to install the CA into any trust store at all. Generate the CA once, copy rootCA.pem into the test image, and point your test client at it explicitly (e.g. curl --cacert rootCA.pem). This avoids the whole "modifying trust stores in a container" problem.

Multi-CA layouts

You can run any number of CAs side-by-side by varying CAROOT. mkcert treats each $CAROOT as fully independent — the CA, the key, the certificates, the trust-store entries (each CA installs a uniquely-named entry: mkcert development CA <serial>).

Use case $CAROOT
Personal dev $HOME/.local/share/mkcert (default)
Client X work $HOME/.mkcert-clientx
CI ephemeral $RUNNER_TEMP/mkcert
One-off e2e test $(mktemp -d)/mkcert

Scaling concerns

There aren't really any. mkcert is single-shot, single-user, single-host. A 10,000-developer org running mkcert is doing 10,000 independent things; nothing in mkcert limits that.

The one practical limit: each NSS profile is a separate certutil call, and certutil is not fast. On a developer with five Firefox profiles, mkcert -install may take a few seconds. This is the only operation in mkcert with anything resembling a runtime concern. See Section 6.

5. Operations

Routine operations

Install or refresh the CA

mkcert -install

Idempotent. Run as often as you like — if the CA is already installed in a given store, mkcert prints a 👍 and moves on.

List the CA location

mkcert -CAROOT

Back up the CA

tar -czf "$HOME/mkcert-ca-backup-$(date +%F).tar.gz" -C "$(mkcert -CAROOT)" .

Note

The backup contains rootCA-key.pem, the private key for a CA your machine trusts unconditionally. Encrypt it at rest. A backup file sitting in your Downloads folder is a credential. Treat it like one.

Rotate the CA

mkcert -uninstall                # remove from trust stores
rm -rf "$(mkcert -CAROOT)"       # delete CA files
mkcert -install                  # generate + install fresh CA
# Re-issue every leaf cert your services use.

This invalidates every previously-issued certificate (they were signed by a now-deleted CA). Plan for downtime / re-issue.

Restore a CA from backup

mkcert -uninstall
rm -rf "$(mkcert -CAROOT)"
mkdir -p "$(mkcert -CAROOT)"
tar -xzf mkcert-ca-backup.tar.gz -C "$(mkcert -CAROOT)"
chmod 0400 "$(mkcert -CAROOT)/rootCA-key.pem"
chmod 0644 "$(mkcert -CAROOT)/rootCA.pem"
mkcert -install

Verify: mkcert example.test, then check the issued cert's issuer matches your backup CA.

Tip

Always re-chmod after restore. Tar archives can preserve permissions but many backup paths (e.g. cloud sync) do not.

Scheduled / ad-hoc maintenance

Cadence Task
Once per dev machine mkcert -install (after brew install mkcert or equivalent).
Per project Issue the cert(s) the project needs; commit nothing — both the leaf cert and (especially) the leaf key are local-only.
Annually (suggested) Confirm $CAROOT is in your machine's backup set (it isn't, by default, on most platforms — ~/.local/share and macOS Application Support are user-scoped but not always backed up).
Every 2 years Re-issue leaf certs that are about to expire. mkcert prints the expiry date on issuance; nothing else tracks it.
Every 10 years (or on team turnover) Rotate the CA.

Pre-flight checklist for a new dev machine

  • [ ] mkcert installed via package manager (mkcert -version returns a real version).
  • [ ] mkcert -install completed without warnings (or warnings have been acted on — brew install nss, etc.).
  • [ ] mkcert -CAROOT returns a path that survives reboot (not /tmp).
  • [ ] Browser restarted (Firefox, Chrome, Chromium, Safari, Edge — any open).
  • [ ] mkcert localhost issues a cert and the cert verifies in your browser.

Logs and diagnostics

mkcert has no log files. All output goes to stdout/stderr via Go's log and fmt. The only place to capture it is your shell. Two diagnostic techniques worth knowing:

# Verbose: re-issue an existing cert and watch what mkcert says about
# trust-store state
mkcert example.test 2>&1 | tee mkcert.log

# Confirm trust at the OS level (Linux)
ls /usr/local/share/ca-certificates/ 2>/dev/null | grep -i mkcert
trust list --filter ca-anchors 2>/dev/null | grep -A1 mkcert

Note

mkcert -install is the canonical "tell me if anything is wrong" check. Re-running it on a working machine produces all 👍 lines and no warnings; any 🦊⚠️/☕️⚠️/⚠️ tells you which store has a problem.

6. Performance

mkcert is single-shot and short-lived. There is no steady-state performance to characterize — only one-off latencies.

Typical timings (rough order)

Operation Time Bottleneck
mkcert -version < 10 ms Process startup.
mkcert -CAROOT < 10 ms Process startup + env reads.
mkcert <host> (existing CA, RSA-2048 leaf) 50-200 ms Mostly RSA-2048 key generation.
mkcert -ecdsa <host> (existing CA) 20-50 ms ECDSA-P256 keygen is fast.
mkcert -install (new CA, all stores) 1-5 s RSA-3072 keygen (~1 s) + per-store I/O + per-NSS-profile certutil shell-out (~200 ms each).
mkcert -install (CA exists, all 👍) < 200 ms Re-checking each store.
mkcert -uninstall (all stores) 0.5-3 s Per-NSS-profile certutil -D.

Performance levers

  • -ecdsa drops leaf-cert issuance time substantially — RSA-2048 keygen dominates the issue path. Use it if you're scripting many cert issuances.
  • TRUST_STORES restricts trust-store work. TRUST_STORES=system skips the per-NSS-profile loop entirely. On a dev box with five Firefox profiles, this turns a 5-second -install into ~1 second.
  • Pre-existing CA. If $CAROOT/rootCA.pem exists, mkcert skips the RSA-3072 keygen entirely. So the slow path is only ever the very first mkcert -install.

Memory

Sub-50 MB resident peak for any operation, including -install with all trust stores.

Note

mkcert reads cacerts (the Java keystore) into keytool's memory, not its own — even on a JDK with hundreds of trusted roots, mkcert's own working set stays small. The big memory consumer in the -install flow is keytool itself, which for very large keystores can hit several hundred MB.

Tip

If you're benchmarking a CI step, the variance comes from certutil and keytool, not from mkcert. Profile those if -install is slow.

7. Security

mkcert's security posture is unusual: it is, by design, a tool that installs a private CA that your machine will trust completely. Used correctly, it's safer than the alternative (clicking through HTTPS warnings during dev). Used carelessly, it punctures TLS for every site you visit.

Threat model

  • In scope: local dev/test traffic on a single developer's machine or a team-controlled dev environment.
  • Out of scope: anything customer-facing, anything traversing the public internet, anything where the entity verifying the cert is not also the entity that ran mkcert -install.
  • Adversary: another user on the same machine, malware on the dev box, or a stolen/cloned rootCA-key.pem.

What mkcert protects you from

  • The "self-signed cert exception" habit. Once mkcert is installed, legitimate dev certs Just Work and any spurious browser warning is meaningfully suspicious again.
  • Inconsistent dev environments. Every team member gets a cert that validates the same way as production-grade trust paths.

What mkcert does NOT protect you from

  • Compromise of rootCA-key.pem. Anyone who reads that file from your disk can issue trusted certificates for any domain on any machine that trusts your CA. There is no revocation mechanism in mkcert — recovery is full rotation.
  • Misuse on production. mkcert certs are not publicly trusted. Using them outside dev creates a false sense of security and forces users to disable TLS validation, which is much worse than starting with Let's Encrypt.
  • Shared machines. If two users share a Linux machine, the system trust store is shared. User A's mkcert installation makes User B's Chrome trust User A's CA. Users with sudo can install CAs that affect every other user.

Caution

The single most common mkcert security mistake is committing rootCA-key.pem to a Git repository. Add *key.pem and especially rootCA-key.pem to your global .gitignore. If this happens once, rotate the CA immediately — assume the key is compromised. Force-push doesn't undo it; the key is in the reflog and the Git host's cache.

Warning

Never use mkcert for public-facing services. The CA is trusted only on machines where mkcert -install was run. Any user without that install will see a "not trusted" error and is being trained to ignore it. Use Let's Encrypt or a commercial CA for anything an end user hits.

Built-in security properties

Property How it's enforced
Private CA key permissions ioutil.WriteFile(rootKeyName, …, 0400) in cert.go newCA().
Per-user CA storage Default $CAROOT is per-user ($HOME/..., $LocalAppData/…). No machine-global default.
Subject differentiation CA OU and CN include <user>@<host>, so each developer's CA is distinguishable when listed.
Validity ceilings 10y CA, 2y3m leaf — well below the most aggressive platform max (Apple 825-day).
MaxPathLenZero=true on the CA The CA cannot issue intermediates that issue further certs — no chained-CA risk.
KeyUsage = CertSign only on the CA The CA cannot be used to sign anything else (e.g. CRLs, or mistaken use as a leaf).
IDN normalization All hostnames are run through idna.ToASCII before being put in the cert, preventing homograph mismatches.
Strict hostname regex (?i)^(\*\.)?[0-9a-z_-]([0-9a-z._-]*[0-9a-z_-])?$ — rejects exotic input that browsers might handle differently from mkcert.

Sudo behavior

mkcert escalates via sudo when needed and prints a custom prompt (Sudo password:) so users see why. If sudo isn't available and the process isn't already running as UID 0, mkcert prints a one-shot warning and continues with reduced trust-store coverage rather than failing.

Warning: "sudo" is not available, and mkcert is not running as root. The
(un)install operation might fail. ⚠️

Known gaps and design caveats

  • No CRL or OCSP. Issued leaf certs cannot be revoked through any protocol the trust stores understand. Recovery from a leaked leaf key requires either letting the cert expire (up to 2y3m) or rotating the CA.
  • Hardcoded PKCS#12 password. changeit is universal but trivial to guess, so PKCS#12 bundles are not a secure-at-rest format. Treat them as you would unencrypted PEM key files.
  • Java keystore password is also hardcoded to changeit. mkcert cannot manage Java keystores that have been re-passworded — and changing the storepass on cacerts is a common hardening step.
  • NSS detection is path-list based. Custom Firefox profile locations are not auto-detected (issue #521). Workaround: import rootCA.pem manually via Firefox UI.
  • Cert pool caching during -install. After installing the CA, the system cert pool that was loaded at process start is stale. The in-process check (m.checkPlatform()) would falsely report failure; mkcert sets ignoreCheckFailure = true to compensate. The actual install is still verified out-of-band — just not by mkcert in the same process. Documented in main.go referencing golang/go#24540.

Important

If you suspect rootCA-key.pem has been compromised (a stolen laptop, a misconfigured backup, an accidental commit): rotate immediately. mkcert -uninstall && rm -rf "$(mkcert -CAROOT)" && mkcert -install, then on every other machine that ever imported your old CA, do the same -uninstall step (with the same $CAROOT if you can; otherwise manually remove the entry from each trust store). Re-issue every leaf you still need.

Audit checklist

  • [ ] rootCA-key.pem is mode 0400 and in $CAROOT only.
  • [ ] No rootCA-key.pem in any Git repo (git log --all --full-history -- rootCA-key.pem returns empty).
  • [ ] No rootCA-key.pem in your home directory backup that's synced to a third party (Dropbox / iCloud / OneDrive).
  • [ ] mkcert isn't being used to issue certs that get embedded in shipped products or services.
  • [ ] When using a shared CA (Pattern B above), there is a written rotation policy and an offboarding step that revokes access to the shared backup.

8. Developer Internals

This section is for people modifying mkcert, packaging it, or auditing it. Source paths reference commit 1c1dc4e.

Repository layout

mkcert/
├── AUTHORS                  Author list (used by `go.mod` directly).
├── LICENSE                  BSD-3-Clause.
├── README.md                Project overview and quick install instructions.
├── go.mod                   Module path filippo.io/mkcert; Go 1.18.
├── go.sum                   Verified module checksums.
├── main.go                  CLI entry point, flag parsing, dispatch, getCAROOT(),
│                            commandWithSudo, storeEnabled, the mkcert struct.
├── cert.go                  generateKey, newCA, loadCA, makeCert, makeCertFromCSR,
│                            printHosts, fileNames, randomSerialNumber, caUniqueName.
├── truststore_darwin.go     macOS Keychain integration (security CLI + trust-settings plist).
├── truststore_linux.go      Linux ca-trust integration (filesystem + update-ca-* commands).
├── truststore_windows.go    Windows root store (crypt32.dll via syscall).
├── truststore_nss.go        NSS DB integration (Firefox + Chromium via certutil).
└── truststore_java.go       Java cacerts integration (keytool).

12 files total, 41,233 source characters across 7 Go files (the rest is license/docs/go.sum). Single Go module, single binary output.

Architecture deep-dive

mkcert is built around a single struct, mkcert, declared in main.go:

type mkcert struct {
    installMode, uninstallMode bool
    pkcs12, ecdsa, client      bool
    keyFile, certFile, p12File string
    csrPath                    string

    CAROOT string
    caCert *x509.Certificate
    caKey  crypto.PrivateKey

    ignoreCheckFailure bool
}

main() parses flags, validates combinations, populates this struct, and calls (&mkcert{...}).Run(flag.Args()). Everything else hangs off that method.

Run flow (in main.go):

  1. getCAROOT() resolves the CA directory (env var or platform default).
  2. os.MkdirAll(CAROOT, 0755) ensures the directory exists.
  3. loadCA() reads rootCA.pem (and rootCA-key.pem if present), parsing each via pem.Decode and x509.ParseCertificate / x509.ParsePKCS8PrivateKey. If rootCA.pem is missing, newCA() is called to generate one.
  4. If installMode, m.install() is invoked (and execution stops if no host args are given). If uninstallMode, m.uninstall() and return.
  5. Otherwise, mkcert checks each enabled trust store — checkPlatform(), checkNSS(), checkJava() — and prints Note: warnings for any store the CA is missing from.
  6. If csrPath != "", makeCertFromCSR() runs and returns.
  7. Otherwise, the positional args are validated. Each is tested for IP, email (mail.ParseAddress), URI (url.Parse), and DNS hostname (an IDN-normalized regex match). Invalid args fatal-error with the "<arg>" is not a valid hostname, IP, URL or email message.
  8. makeCert(args) issues the leaf.

Key generation (generateKey in cert.go):

func (m *mkcert) generateKey(rootCA bool) (crypto.PrivateKey, error) {
    if m.ecdsa {
        return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    }
    if rootCA {
        return rsa.GenerateKey(rand.Reader, 3072)
    }
    return rsa.GenerateKey(rand.Reader, 2048)
}

Note that -ecdsa flips both the leaf and the CA to ECDSA P-256. However, newCA() calls generateKey(true) only at install-time, so by the time you ever issue an ECDSA leaf, the CA was already generated as RSA-3072 (because -ecdsa typically isn't passed to the very first -install). In practice the CA is RSA-3072 for everyone. If you pass -ecdsa to the initial mkcert -install, the CA will also be ECDSA; this is unusual.

CA issuance (newCA in cert.go) builds a certificate template with:

  • 10-year validity.
  • Subject: O=mkcert development CA, OU=<user>@<host>, CN=mkcert <user>@<host>. The CN is required by iOS to display the CA in "Certificate Trust Settings" (referencing internal issue #47).
  • KeyUsage = CertSign, IsCA = true, MaxPathLenZero = true.
  • A 128-bit random SerialNumber.
  • A SHA-1 SubjectKeyId derived from the public key bits.

The CA is self-signed via x509.CreateCertificate(rand.Reader, tpl, tpl, pub, priv) (note: tpl, tpl — template is its own parent). It's PEM-encoded and written to $CAROOT/rootCA.pem (mode 0644). The key is PKCS#8-encoded and written to $CAROOT/rootCA-key.pem (mode 0400).

Leaf issuance (makeCert in cert.go):

  • 2y3m validity (time.Now().AddDate(2, 3, 0)).
  • Subject: O=mkcert development certificate, OU=<user>@<host>. (For -pkcs12, Subject.CommonName is also set to the first host because IIS only displays CommonName.)
  • Each host arg is dispatched into the right SAN slot: IPAddresses (if net.ParseIP succeeds), EmailAddresses (if it's parseable as a mail.Address), URIs (if url.Parse returns a scheme + host), or DNSNames (default).
  • ExtKeyUsage is built up: serverAuth for any DNS/IP/URI SAN, clientAuth for -client, emailProtection for any email SAN.
  • Signed via x509.CreateCertificate(rand.Reader, tpl, m.caCert, pub, m.caKey).
  • Encoded as PEM (cert and key separately) or PKCS#12 (cert + key + CA), written to the -cert-file/-key-file/-p12-file paths or the default ./<name>{.pem,-key.pem,.p12}.

CSR signing (makeCertFromCSR in cert.go):

  • Reads the CSR file, decodes the PEM (CERTIFICATE REQUEST or NEW CERTIFICATE REQUEST types only).
  • Verifies the CSR's self-signature.
  • Builds a template from the CSR's Subject and Extensions (preserving any requested SAN, KU, EKU). If the CSR has no SAN extension, mkcert fixes it up by setting DNSNames to the CSR's CommonName (modern browsers ignore CommonName).
  • Adds serverAuth EKU unconditionally; adds clientAuth if -client is set; adds emailProtection if the CSR includes email addresses.
  • Signs with the CA. Output is the cert only (no key — the requester already has it).

Trust-store integration patterns

Each truststore_*.go file conforms to the same informal interface (no Go interface declared; just consistent method names on *mkcert):

func (m *mkcert) checkPlatform() bool   // does the system store have our CA?
func (m *mkcert) installPlatform() bool // add to system store; return success
func (m *mkcert) uninstallPlatform() bool

func (m *mkcert) checkNSS() bool   // for NSS — same pattern, in truststore_nss.go
func (m *mkcert) installNSS() bool
func (m *mkcert) uninstallNSS()

func (m *mkcert) checkJava() bool   // for Java — same pattern, in truststore_java.go
func (m *mkcert) installJava()
func (m *mkcert) uninstallJava()

macOS (truststore_darwin.go) shells to the security CLI and uses howett.net/plist to mutate the trust-settings plist. The plist manipulation is necessary because security add-trusted-cert alone doesn't fully express "trust for SSL server auth" on modern macOS — the trust-settings plist holds the per-cert policy data (referencing golang/go#24652).

Windows (truststore_windows.go) calls crypt32.dll directly via syscall.NewLazyDLL — no certutil.exe dependency, no PowerShell. The exported procs are: CertOpenSystemStoreW (to open ROOT), CertAddEncodedCertificateToStore (to add), CertEnumCertificatesInStore

  • CertDuplicateCertificateContext + CertDeleteCertificateFromStore (to delete by serial number).

Linux (truststore_linux.go) decides which ca-trust layout the distro uses by checking for the existence of well-known directories (see Section 2 table), then writes the CA via tee (with sudo) and runs the matching refresh command.

NSS (truststore_nss.go) iterates over all detected NSS DBs (Chromium

  • each Firefox profile), distinguishes v2 vs. v3 NSS DB formats by looking for cert9.db (v3, prefix sql:) or cert8.db (v2, prefix dbm:), and shells to certutil -A/-D/-V against each. If any certutil invocation fails with SEC_ERROR_READ_ONLY on non-Windows, the call is automatically retried with sudo.

Java (truststore_java.go) reads $JAVA_HOME, finds keytool and cacerts, and shells to keytool -importcert/-delete/-list with -storepass changeit. If keytool fails with java.io.FileNotFoundException on non-Windows, it retries with sudo (preserving JAVA_HOME in the env passed to sudo).

Code patterns and conventions

Pattern Where Notes
fatalIfErr(err, msg) main.go The standard error-handling convention: any unrecoverable error becomes log.Fatalf("ERROR: %s: %s", msg, err), exit code 1. Used everywhere.
fatalIfCmdErr(err, cmd, out) main.go Same idea but for shelled-out commands; includes the command name and combined output in the error.
pathExists(path) bool main.go Wrapper around os.Stat returning a bool. Used liberally to test optional paths.
binaryExists(name) bool main.go Wrapper around exec.LookPath. Used to detect optional tools.
commandWithSudo(cmd...) *exec.Cmd main.go Standard escalation pattern. Returns a plain command if running as root or if sudo isn't available; otherwise wraps in sudo --prompt=Sudo password: --.
init() per file truststore_*.go Each platform/store file has an init() that probes the environment and sets package-level booleans (hasNSS, hasJava, hasKeytool, SystemTrustCommand, etc.). This is what makes the dispatch in Run() cheap.
Per-platform variable shadowing FirefoxProfiles, CertutilInstallHelp, NSSBrowsers Declared in each truststore_<os>.go with platform-specific values. Go's filename-based build tags handle the routing.

Testing strategy

There are no tests in the repo at this commit. The project's test strategy is, in practice, "the binary either works on real machines or it doesn't" — which works because the trust-store integrations are heavily user-visible (a broken one shows up immediately as a browser warning) and because the cert-generation paths use only stdlib crypto/x509 routines that are themselves well-tested.

If you're contributing, the patterns to follow are:

  • For changes to crypto / cert-template logic in cert.go: add a small _test.go that issues a cert with known inputs and asserts on the resulting x509.Certificate fields. The cert template is declarative enough that this is straightforward.
  • For trust-store changes: there's no good way to test these without a real environment. PRs typically include "tested on macOS 14, Ubuntu 22.04, Windows 11" in the description.
  • For flag-parsing changes in main.go: capture stdout/stderr and assert on the exit error message for each invalid combination.

Build and release

Build:

go build -ldflags "-X main.Version=$(git describe --tags)"

Cross-compile:

GOOS=linux  GOARCH=amd64 go build -ldflags "-X main.Version=$(git describe --tags)" -o mkcert-linux-amd64
GOOS=darwin GOARCH=arm64 go build -ldflags "-X main.Version=$(git describe --tags)" -o mkcert-darwin-arm64
GOOS=windows GOARCH=amd64 go build -ldflags "-X main.Version=$(git describe --tags)" -o mkcert-windows-amd64.exe

Releases ship pre-built binaries via GitHub Releases at github.com/FiloSottile/mkcert/releases. There are also redirects at dl.filippo.io/mkcert/<version>?for=<os>/<arch>.

Potential improvements (acknowledged TODOs)

Source-based TODOs and design notes from the code:

  • commandWithSudo re-execution: a TODO in main.go at line ~169 suggests execve(2)-ing mkcert as root after -install would let the in-process trust check work after a fresh install (the system cert pool is loaded once at process start).
  • addCert overwrite semantics on Windows: a TODO in truststore_windows.go addCert() notes the current behavior of always using CERT_STORE_ADD_REPLACE_EXISTING (dwAddDisposition=3), asking whether replace-on-add is always the right call. In practice, yes — but the comment is preserved in case someone hits a regression.
  • Tracked feature requests: issue #229 (customize CA name) is the most-requested change that hasn't shipped.

Contributing

Standard Go project conventions apply. Open an issue first for anything non-trivial. Coding style follows gofmt. License is BSD-3-Clause; PRs should add the standard copyright header used at the top of every Go file in the project.

9. Integration Points

mkcert is the small piece of a TLS-in-dev workflow. The interesting integrations are with everything else.

Direct dependencies (from go.mod)

Module Version Purpose
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 idna.ToASCII for IDN normalization.
software.sslmate.com/src/go-pkcs12 v0.2.0 PKCS#12 (.p12 / .pfx) encoding. The Go standard library only decodes; this library encodes.
howett.net/plist v1.0.0 Apple plist read/write — needed to mutate the macOS trust-settings plist.
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 (indirect) Transitive via go-pkcs12.
golang.org/x/text v0.3.7 (indirect) Transitive via golang.org/x/net.

No runtime dependencies — everything is statically linked into the binary.

External tools (called via exec.Command if present)

Tool When Required for
security (macOS) installPlatform/uninstallPlatform on darwin macOS system trust integration.
update-ca-trust / update-ca-certificates / trust extract-compat (Linux) installPlatform/uninstallPlatform on linux Linux system trust refresh. The exact one is auto-detected.
tee (Linux) installPlatform on linux Writing the CA cert into /usr/local/share/ca-certificates/ (or whichever path) with sudo.
rm (Linux) uninstallPlatform on linux Removing the CA cert file with sudo.
certutil (NSS) checkNSS/installNSS/uninstallNSS Firefox + Chromium trust on Linux/macOS. macOS uses a Homebrew-installed certutil (brew install nss).
keytool (Java) checkJava/installJava/uninstallJava Java trust integration. Found via $JAVA_HOME/bin/keytool[.exe].
brew (macOS) init() in truststore_nss.go (fallback path) Probing for certutil when neither standard PATH nor /usr/local/opt/nss/bin/certutil works.
sudo Throughout All operations that touch system trust on non-Windows.

Consumers (i.e., what uses mkcert output)

mkcert outputs are vanilla PEM and PKCS#12 — anything that consumes those formats works unchanged. The integrations that matter:

Consumer Files it wants Notes
Go net/http (http.ListenAndServeTLS) leaf cert PEM + leaf key PEM Pass paths directly.
Node.js https.createServer leaf cert PEM + leaf key PEM fs.readFileSync both, pass as {cert, key}.
nginx leaf cert PEM + leaf key PEM ssl_certificate and ssl_certificate_key directives.
Caddy leaf cert PEM + leaf key PEM tls cert.pem key.pem directive.
Vite / webpack-dev-server leaf cert PEM + leaf key PEM server.https = {key, cert}.
IIS (Windows) PKCS#12 bundle Use mkcert -pkcs12. Password is changeit.
Java apps PKCS#12 bundle or trust-store-installed CA Use mkcert -pkcs12 for app-specific certs, or mkcert -install to add CA to cacerts.
curl trust the CA explicitly curl --cacert "$(mkcert -CAROOT)/rootCA.pem".
Docker / containers mount rootCA.pem into image -v "$(mkcert -CAROOT)/rootCA.pem:/etc/ssl/certs/mkcert.pem" or copy at build time.

Containers and orchestration

mkcert itself isn't usually run inside a container — you run it on the host that owns $CAROOT, and you ship the CA cert (public, rootCA.pem) into containers that need to trust it. Common patterns:

# In a dev Dockerfile, trust the host's mkcert CA at build time:
COPY rootCA.pem /usr/local/share/ca-certificates/mkcert.crt
RUN update-ca-certificates
# Or at runtime, mount it read-only:
services:
  app:
    volumes:
      - "${HOME}/.local/share/mkcert/rootCA.pem:/etc/ssl/certs/mkcert.pem:ro"
    environment:
      - SSL_CERT_FILE=/etc/ssl/certs/mkcert.pem

Tip

Many language runtimes have their own CA trust paths separate from the OS — Node respects NODE_EXTRA_CA_CERTS, Python's requests respects REQUESTS_CA_BUNDLE, Go binaries respect SSL_CERT_FILE. If you can't get a containerized client to trust your mkcert CA via the system store, set the runtime-specific env var and point it at rootCA.pem.

Comparison to adjacent tools

Tool Use when
mkcert Local dev, you control the machines, you want green padlocks for any hostname.
Let's Encrypt / acme.sh Public-facing services with reachable DNS. mkcert and Let's Encrypt are not substitutes for each other.
cert-manager (Kubernetes) Managed PKI inside a cluster — issues short-lived certs at scale, supports ACME, supports its own internal CA. mkcert is single-machine; cert-manager is cluster-scoped. Issue #350 discusses this comparison.
smallstep / step-ca A real internal CA with revocation, ACME support, RBAC. The right answer when "the team uses mkcert" stops scaling.
OpenSSL req -x509 One-off self-signed certs without trust-store integration. mkcert's value-add over plain OpenSSL is the trust-store side, not the cert generation.

Note

If your team starts treating mkcert as production PKI — sharing rootCA-key.pem across many machines, never rotating, embedding it in CI without rotation policy — that's a signal you've outgrown mkcert and should plan a migration to step-ca or cert-manager. mkcert is a deliberate "small, single-purpose dev tool" project; it does not aspire to be a full PKI.