wj-burnerca
A WaterJuice tool for generating single-shot, name-constrained, self-destroying Certificate Authorities for local development TLS.
You give it one domain. It generates a CA constrained to that domain,
issues a cert covering both <domain> and *.<domain>, verifies the
chain, and destroys the CA private key before exiting.
The metaphor is a burner phone: single-purpose, used briefly, intentionally discarded.
Why
The conventional approach to local TLS — installing a long-lived, unconstrained root CA into your system trust store — leaves that CA's private key on your laptop indefinitely. It can sign certs for any domain. If it leaks, the attacker can MITM anything you visit.
wj-burnerca narrows this in two complementary ways:
- Name constraints (RFC 5280, marked critical): the CA is cryptographically incapable of signing certs outside the one domain you gave it. Modern TLS libraries and browsers enforce this.
- Key destruction: after issuing the cert, the CA private key is deleted.
Installation
# One-shot, no install — fetches into a cache and runs:
uvx wj-burnerca example.com
# Install globally with uv:
uv tool install wj-burnerca
# Install globally with pipx:
pipx install wj-burnerca
Requires Python 3.14+ and an openssl binary on PATH (LibreSSL 3.x
as shipped by macOS, or OpenSSL 3.x — both work).
Quick Start
# Default: 365-day validity, output to ./<domain>/
wj-burnerca example.com
# Shorter validity
wj-burnerca example.com --days 30
# Custom output dir
wj-burnerca example.com --out ./dev/ca/
After a successful run, the output directory contains:
<out>/
rootCA-<domain>.crt # public CA cert; trust this
<domain>.crt # cert with SANs <domain> and *.<domain>
<domain>.key # cert private key (mode 0600)
MANIFEST.txt # human-readable summary, with fingerprints
trust-instructions.md # per-OS trust instructions
Trusting the CA
Open <out>/trust-instructions.md for the right command for your OS.
Prefer user-scoped trust over the system trust store.
export SSL_CERT_FILE="$(pwd)/rootCA-<domain>.crt" # Python tooling
export CURL_CA_BUNDLE="$(pwd)/rootCA-<domain>.crt" # curl
Security model
| Threat | Mitigation |
|---|---|
| CA key stolen → forge certs for arbitrary domains | Critical X.509 nameConstraints — CA cryptographically incapable. |
| CA key stolen at any time after issuance | CA key destroyed at end of run; not on disk to steal. |
| Leaf key stolen → MITM the constrained domain | Accepted; mitigated by short validity and the fact that the constrained domain typically only resolves via /etc/hosts. |
| Half-completed run leaves CA key on disk | All work happens in a tempdir; outputs are moved into place only after chain verification; tempdir is shredded otherwise. |
Implementation notes
- ECDSA P-256 keys throughout. Ed25519 would be the obvious modern default but isn't available in the LibreSSL 3.3.x that ships with macOS; P-256 is the strongest curve common to both libraries.
- The CA validity matches the cert validity, so the burner CA expires when its only cert does.
Exit codes
0— success.1— argument or validation error.2— openssl subprocess failure during generation.3— chain verification failed; tempdir shredded;--outuntouched.4—--outdirectory exists and is non-empty without--force.5— could not destroy CA private key. Path is printed loudly.
See the Command Line Usage page for the full flag reference, and Release Notes for what's changed.