# mkcert Administrator & Developer Guide

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`)

```text
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.

```yaml
# 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

```bash
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

```bash
mkcert -CAROOT
```

#### Back up the CA

```bash
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

```bash
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

```bash
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:

```bash
# 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.

```text
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](https://github.com/FiloSottile/mkcert/issues/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](https://github.com/golang/go/issues/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`:

```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`):

```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`):

```go
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](https://github.com/golang/go/issues/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:

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

Cross-compile:

```bash
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](https://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](https://github.com/FiloSottile/mkcert/issues/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:

```dockerfile
# 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
```

```yaml
# 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](https://github.com/FiloSottile/mkcert/issues/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.

---

_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. Unofficial; not affiliated with or reviewed by the project._
