Local HTTPS
How LPM ships zero-config HTTPS for localhost — root CA, project certs, trust store install.
lpm dev --https serves your dev server over https://localhost. Browsers trust the certificate. No --insecure flag, no warnings, no per-project setup beyond a one-time CA install.
This page covers the design — what gets generated, where it lives, how trust-store installation works on each OS, and how the cert is wired into the dev server.
How it works
1. Generate a root CA (one time, machine-global)
└─ ~/.lpm/certs/rootCA.pem ← cert
└─ ~/.lpm/certs/rootCA-key.pem ← private key (mode 0o600)
2. Install the CA into the OS trust store
└─ macOS: security add-trusted-cert (user login keychain, no sudo)
└─ Linux: /usr/local/share/ca-certificates/ + update-ca-certificates (sudo)
└─ Windows: certutil -addstore Root (UAC)
3. Generate a per-project certificate
└─ <project>/.lpm/certs/cert.pem
└─ <project>/.lpm/certs/key.pem (mode 0o600)
└─ SAN includes localhost, 127.0.0.1, and any --host arguments
└─ Custom-host cert.pem files include a constrained project intermediate
4. Inject env vars into the dev server process
└─ HTTPS=true
└─ SSL_CRT_FILE=<project>/.lpm/certs/cert.pem
└─ SSL_KEY_FILE=<project>/.lpm/certs/key.pem
└─ Plus framework-specific vars (Next.js, Vite, etc.)
5. The dev server uses the cert; browsers trust it because the CA is trusted.You only do steps 1 and 2 once per machine (lpm cert trust). Steps 3–5 happen automatically on every lpm dev --https (or any project where lpm.json > https: true).
File layout
~/.lpm/certs/ ← global, one per machine
├── rootCA.pem ← root CA certificate
└── rootCA-key.pem ← private key, mode 0o600
<project>/.lpm/certs/ ← per-project
├── cert.pem ← server certificate, or leaf + intermediate chain
└── key.pem ← private key, mode 0o600Project certs live under .lpm/certs/ next to package.json. Add .lpm/ to .gitignore if you don't already — generated certs are environment-specific and shouldn't be committed.
Key file safety
Private keys are written with mode 0o600 from creation (create_new + OpenOptionsExt::mode()), not chmod-after-write. This eliminates the TOCTOU window where the file would briefly be world-readable.
If a key file already exists, it's removed first so the create_new succeeds on regeneration. This means lpm cert generate always produces a fresh key — there's no "preserve existing key" path.
Trust store install
lpm cert trust installs rootCA.pem into the OS trust store. Per-OS:
| OS | Mechanism | Elevation? |
|---|---|---|
| macOS | security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db rootCA.pem (user login keychain) | No sudo |
| Linux | Copy to /usr/local/share/ca-certificates/lpm-local-ca.crt, then update-ca-certificates | Sudo (both steps) |
| Windows | certutil -addstore Root rootCA.pem | UAC prompt |
Untrust:
lpm cert uninstallRemoves the CA from the trust store. Doesn't delete the on-disk CA — re-install with lpm cert trust. To wipe the CA itself: rm -rf ~/.lpm/certs/.
Browser support
Once the CA is trusted at the OS level, browsers that use the system trust store (Chrome, Edge, Safari, most Linux browsers) accept LPM-signed certs without warnings.
Firefox uses its own trust store by default. To trust the LPM CA in Firefox, either:
- Set
security.enterprise_roots.enabled = trueinabout:config(Firefox then reads the system trust store), or - Manually import
~/.lpm/certs/rootCA.pemvia Settings → Privacy → Certificates → View Certificates → Authorities → Import.
Adding hostnames
By default, the project cert's Subject Alternative Name (SAN) includes localhost and 127.0.0.1. To serve under a custom hostname:
lpm cert generate --host my-app.local
lpm cert generate --host a.local --host b.local # repeatablePair with a /etc/hosts entry:
127.0.0.1 my-app.localNow https://my-app.local:3000 works in any browser. Useful for projects that need a real hostname (cookies scoped to a domain, OAuth callbacks, etc.).
lpm cert generate regenerates the project cert from scratch — there's no "add a hostname to the existing SAN" path. The default localhost, 127.0.0.1, and ::1 SANs are always included.
When custom hostnames or lpm.json > cert.extraPermittedDns entries are present, cert.pem is written as a leaf-first TLS chain with a project-scoped constrained intermediate. extraPermittedDns adds validated NameConstraints subtrees; it does not add browser SANs. If your root CA was created before intermediate support, LPM may ask you to run lpm cert rotate before issuing custom-host certificates.
Framework integration
LPM detects which framework lpm dev is starting and injects the right env vars:
| Framework | Env vars |
|---|---|
| Next.js | HTTPS=true, SSL_CRT_FILE, SSL_KEY_FILE |
| Vite | VITE_HTTPS=true, VITE_SSL_CERT, VITE_SSL_KEY (or via Vite's https config object) |
| Generic | SSL_CRT_FILE, SSL_KEY_FILE, HTTPS=true |
If your framework needs different vars, you can read the cert paths yourself from the env LPM injects and configure the server manually.
Status check
lpm cert statusReports:
- Root CA — exists / trusted, subject, expiry
- Project cert — exists, expiry, hostnames in SAN, whether renewal is recommended
--json returns the same structurally.
Renewal
Project certs have a long-ish but bounded lifetime. lpm cert status flags needs_renewal: true when expiry is within ~30 days. Regenerate:
lpm cert generateThe CA itself has a much longer lifetime — once you lpm cert trust, it lasts years. Re-running lpm cert trust is safe (idempotent — installs are deduped on cert serial).
Why a per-machine root CA
LPM doesn't ship a baked-in CA. Every machine generates its own root CA on first lpm cert trust. Reasons:
- The private key never leaves the machine. A compromised LPM-distributed CA would be a supply-chain attack against every user.
- Removing trust is a local operation —
lpm cert uninstalldoesn't have to coordinate with anything. - Project certs are signed by your local CA, so they're only trusted on machines that have run
lpm cert trust. Sharing a project cert across machines doesn't accidentally trust its issuer everywhere.
See also
lpm cert— CLI command referencelpm dev --https— most common consumerlpm.jsonhttps — persistent config