Local HTTPS
How LPM CLI 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 LPM CLI terminates TLS in front of 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. Discover and verify the child server endpoint
└─ Child can stay on plain HTTP
└─ Listener must belong to the launched process tree
5. Start an LPM CLI-owned TLS frontend
└─ Browser HTTPS/WSS → LPM CLI frontend → child HTTP/WS
└─ Browser trusts the frontend certificate because the CA is trustedYou 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). The child must serve plain HTTP in this mode; disable framework-level HTTPS and let LPM CLI own TLS termination.
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 CLI-generated certs without warnings.
Firefox uses its own trust store by default. To trust the LPM CLI 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:<resolved-port> 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 CLI may ask you to run lpm cert rotate before issuing custom-host certificates.
Framework independence
LPM CLI does not inject HTTPS, SSL_CRT_FILE, SSL_KEY_FILE, or framework-specific certificate variables into the child. It discovers the child endpoint first, then terminates HTTPS and secure WebSockets at its own frontend.
Port selection is separate from certificate setup. Vite receives a strict managed port when LPM CLI must assign one; Next.js, Nuxt, SvelteKit, Remix, and Astro receive their native port argument; generic servers receive PORT. If a child ignores an explicit managed port, startup fails before the browser, proxy, or tunnel can target the wrong listener.
With lpm dev --https --port 4000, 4000 is the browser-facing TLS port and the plain-HTTP child receives a separate internal port. Without --port, the frontend asks the OS for an available port, avoiding a probe-then-bind race.
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 CLI 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 shared CA distributed by LPM CLI 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