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.pemandrootCA-key.pemfrom$CAROOTfrom 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 detectedSystemTrustCommand, etc.) are re-initialized ininit()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_HOMEset 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.pemis intentional and tight. Do not relax it. The 0600 on leaf keys is similar. mkcert sets these explicitly viaioutil.WriteFileand 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.pemexactly 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 meansmkcert -uninstall && rm -rf $CAROOT && mkcert -installon 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:
- Set
CAROOTexplicitly. In containers$XDG_DATA_HOMEand$HOMEmay both be empty, in which casegetCAROOT()returns""and mkcert exits.export CAROOT=/tmp/mkcert(or a job-scoped path) fixes this. - Set
TRUST_STORES=system(ornss) 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.peminto 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 yourDownloadsfolder 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-
chmodafter 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 -versionreturns a real version). - [ ]
mkcert -installcompleted without warnings (or warnings have been acted on —brew install nss, etc.). - [ ]
mkcert -CAROOTreturns a path that survives reboot (not/tmp). - [ ] Browser restarted (Firefox, Chrome, Chromium, Safari, Edge — any open).
- [ ]
mkcert localhostissues 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 -installis 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
-ecdsadrops leaf-cert issuance time substantially — RSA-2048 keygen dominates the issue path. Use it if you're scripting many cert issuances.TRUST_STORESrestricts trust-store work.TRUST_STORES=systemskips the per-NSS-profile loop entirely. On a dev box with five Firefox profiles, this turns a 5-second-installinto ~1 second.- Pre-existing CA. If
$CAROOT/rootCA.pemexists, mkcert skips the RSA-3072 keygen entirely. So the slow path is only ever the very firstmkcert -install.
Memory
Sub-50 MB resident peak for any operation, including -install with all
trust stores.
Note
mkcert reads
cacerts(the Java keystore) intokeytool'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-installflow iskeytoolitself, which for very large keystores can hit several hundred MB.
Tip
If you're benchmarking a CI step, the variance comes from
certutilandkeytool, not from mkcert. Profile those if-installis 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.pemto a Git repository. Add*key.pemand especiallyrootCA-key.pemto 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 -installwas 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.
changeitis 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 oncacertsis a common hardening step. - NSS detection is path-list based. Custom Firefox profile
locations are not auto-detected (issue
#521).
Workaround: import
rootCA.pemmanually 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 setsignoreCheckFailure = trueto compensate. The actual install is still verified out-of-band — just not by mkcert in the same process. Documented inmain.goreferencing golang/go#24540.
Important
If you suspect
rootCA-key.pemhas 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-uninstallstep (with the same$CAROOTif you can; otherwise manually remove the entry from each trust store). Re-issue every leaf you still need.
Audit checklist
- [ ]
rootCA-key.pemis mode 0400 and in$CAROOTonly. - [ ] No
rootCA-key.pemin any Git repo (git log --all --full-history -- rootCA-key.pemreturns empty). - [ ] No
rootCA-key.pemin 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):
getCAROOT()resolves the CA directory (env var or platform default).os.MkdirAll(CAROOT, 0755)ensures the directory exists.loadCA()readsrootCA.pem(androotCA-key.pemif present), parsing each viapem.Decodeandx509.ParseCertificate/x509.ParsePKCS8PrivateKey. IfrootCA.pemis missing,newCA()is called to generate one.- If
installMode,m.install()is invoked (and execution stops if no host args are given). IfuninstallMode,m.uninstall()and return. - Otherwise, mkcert checks each enabled trust store —
checkPlatform(),checkNSS(),checkJava()— and printsNote:warnings for any store the CA is missing from. - If
csrPath != "",makeCertFromCSR()runs and returns. - 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 emailmessage. 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.CommonNameis also set to the first host because IIS only displays CommonName.) - Each host arg is dispatched into the right SAN slot:
IPAddresses(ifnet.ParseIPsucceeds),EmailAddresses(if it's parseable as amail.Address),URIs(ifurl.Parsereturns a scheme + host), orDNSNames(default). ExtKeyUsageis built up:serverAuthfor any DNS/IP/URI SAN,clientAuthfor-client,emailProtectionfor 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-filepaths or the default./<name>{.pem,-key.pem,.p12}.
CSR signing (makeCertFromCSR in cert.go):
- Reads the CSR file, decodes the PEM (
CERTIFICATE REQUESTorNEW CERTIFICATE REQUESTtypes only). - Verifies the CSR's self-signature.
- Builds a template from the CSR's
SubjectandExtensions(preserving any requested SAN, KU, EKU). If the CSR has no SAN extension, mkcert fixes it up by settingDNSNamesto the CSR's CommonName (modern browsers ignore CommonName). - Adds
serverAuthEKU unconditionally; addsclientAuthif-clientis set; addsemailProtectionif 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, prefixsql:) orcert8.db(v2, prefixdbm:), and shells tocertutil -A/-D/-Vagainst each. If anycertutilinvocation fails withSEC_ERROR_READ_ONLYon non-Windows, the call is automatically retried withsudo.
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.gothat issues a cert with known inputs and asserts on the resultingx509.Certificatefields. 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:
commandWithSudore-execution: a TODO inmain.goat line ~169 suggestsexecve(2)-ing mkcert as root after-installwould let the in-process trust check work after a fresh install (the system cert pool is loaded once at process start).addCertoverwrite semantics on Windows: a TODO intruststore_windows.goaddCert()notes the current behavior of always usingCERT_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'srequestsrespectsREQUESTS_CA_BUNDLE, Go binaries respectSSL_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 atrootCA.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.pemacross 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.