Local HTTPS — mkcert and a Self CA
Local HTTPS — mkcert and a Self CA
Some modern web features simply do not work over HTTP. Service Worker, WebAuthn, parts of the media APIs, and the Secure flag for Cookie SameSite=None are the cases. In production HTTPS is naturally there, but local development needs explicit setup. This piece covers when local HTTPS is needed, the mkcert model, the OS trust stores, alternatives (openssl, ngrok, Caddy local CA), and subtle places like HSTS preload conflicts.
1. Where Local HTTPS is Needed
As browser security models keep tightening, more features only allow https:// or http://localhost.
| Feature | HTTP localhost | HTTP non-localhost | HTTPS |
|---|---|---|---|
| Service Worker, PWA | OK | No | OK |
| WebAuthn | OK | No | OK |
getUserMedia (camera, mic) |
Partial | No | OK |
SameSite=None Cookie |
Secure enforced |
Secure enforced |
OK |
| Cross-origin Cookie, CORS | Limited | Limited | Natural |
localhost itself is treated as a secure context, so some features work. Local HTTPS becomes necessary in these places.
- When using virtual hosts (
app.test,myapp.local). - Testing
SameSite=NoneSecurecookies. - Verifying production-equivalent flows (HSTS, redirects, cookie behavior).
- Trying WebRTC, camera, or microphone access over IP-based access.
2. About mkcert
A local CA tool published by Filippo Valsorda in 2018. A single binary written in Go.
The flow:
- On first run, generate a self-signed CA (root certificate) trusted only on the local machine.
- Register that CA in the OS and browser trust stores.
- Sign domain certificates with that CA.
# Mac
brew install mkcert nss # nss for Firefox trust
mkcert -install
mkcert localhost 127.0.0.1 ::1 myapp.local "*.myapp.local"
# Linux
sudo apt install libnss3-tools mkcert
mkcert -install
mkcert localhost 127.0.0.1 myapp.local
# Windows (PowerShell, scoop or chocolatey)
scoop install mkcert
mkcert -install
mkcert localhost 127.0.0.1 myapp.local
The output is myapp.local+3.pem (certificate) and myapp.local+3-key.pem (private key). Point the dev server at these two files.
3. Vite, Next, or a Plain Node Server
// Vite
import fs from 'node:fs';
export default {
server: {
https: {
cert: fs.readFileSync('./myapp.local+3.pem'),
key: fs.readFileSync('./myapp.local+3-key.pem'),
},
},
};
// Node http2
import http2 from 'node:http2';
http2.createSecureServer({
cert: fs.readFileSync('./myapp.local+3.pem'),
key: fs.readFileSync('./myapp.local+3-key.pem'),
}, app).listen(8443);
4. OS Trust Stores
mkcert -install handles this automatically, but the internals differ per OS.
| OS | Store | Tool |
|---|---|---|
| macOS | System Keychain | security add-trusted-cert |
| Windows | Trusted Root Certification Authorities | certmgr.msc or certutil |
| Linux (Debian / Ubuntu) | /usr/local/share/ca-certificates/ + update-ca-certificates |
ca-certificates package |
| Linux (Fedora / RHEL) | /etc/pki/ca-trust/source/anchors/ + update-ca-trust |
ca-certificates |
Browsers:
- Chrome, Edge, Safari — use the OS trust store as is.
- Firefox — separate NSS database. mkcert auto-registers via the
nsstool.
5. Other Paths
openssl self-signed — enough for one-off, server-to-server, or local single-use cases (CA isn't trusted, so warnings every time):
openssl req -x509 -newkey rsa:4096 -nodes \
-keyout key.pem -out cert.pem \
-days 365 -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
ngrok, localhost.run, cloudflared tunnel — expose a local server as an external HTTPS URL:
| Tool | Note |
|---|---|
| ngrok (2013) | The oldest. Free plus paid static domains. |
| Cloudflare Tunnel | Combines a Cloudflare account and domain. Wide free tier. |
| localhost.run | Expose via SSH alone, no signup. |
| Tailscale Funnel | Expose via a Tailnet domain. |
The advantage is no self CA required (the cert is already trusted). The drawback is the external service dependency.
6. Caddy's Self Local CA
On first run, Caddy can create its own local CA and register it in the OS trust store. The model is the same as mkcert, but it merges naturally with Caddy's reverse-proxy flow.
{
local_certs
}
myapp.local {
reverse_proxy 127.0.0.1:3000
}
This single block makes https://myapp.local work with a trusted certificate. No separate mkcert install.
7. Built-in Options
Some frameworks have their own HTTPS option:
next dev --experimental-https(Next 13.5 onwards).- Vite — set
server.https = trueand it auto-self-signs (browser warning shows).
Built-in is fast but the certificate isn't trusted. For real testing the recommended flow is mkcert or Caddy.
8. hosts File + mkcert
To test on a virtual host instead of localhost, add a hosts mapping:
# Mac / Linux: /etc/hosts
# Windows: C:\Windows\System32\drivers\etc\hosts
127.0.0.1 myapp.local
127.0.0.1 api.myapp.local
mkcert myapp.local "*.myapp.local"
Adding *.myapp.local as a wildcard reuses the same certificate as subdomains grow.
9. Team Sharing is Discouraged
mkcert's CA is safe only when trusted on a single machine. Sharing the same CA across a team is discouraged — if that CA leaks, every machine on the team is exposed. Each developer runs mkcert -install and issues on their own machine.
For HTTPS testing in CI, use a self-signed cert plus NODE_TLS_REJECT_UNAUTHORIZED=0, or an isolated environment like testcontainers. Installing mkcert on a CI machine creates a new CA each time and causes confusion.
10. Common Pitfalls
Firefox not recognizing the cert — without the NSS library (libnss3-tools, nss), Firefox's trust store doesn't get auto-registered. mkcert prints a warning.
HSTS preload conflicts — some TLDs like .dev, .app, .test are in the HSTS preload list, forcing HTTP to redirect to HTTPS. .local or .localhost are relatively safe.
Using a certificate from a machine that doesn't have mkcert — using the same cert on someone else's machine triggers a warning because the OS doesn't know the CA. Issue per machine.
Permissions on ports 80 / 443 — regular users can't bind below 1024 (Linux). Use a high port like 8443, setcap, or port forwarding.
Missing SAN in the certificate — modern browsers reject CN-only certs. Put every host and IP in subjectAltName.
Validity period — mkcert domain certs default to 825 days. Reissue after expiry.
Trust store reset — OS reinstall or login user change makes the CA disappear. Run mkcert -install again.
Closing thoughts
Most local development is fine with the http://localhost secure context. Only when production-equivalent flows like PWA or WebAuthn need to be tested as is, mkcert (single machine) or Caddy local_certs (combined with reverse proxy) becomes worth adopting. For CI, testcontainers is cleaner.
Next
- cloud-emulator-stack
- (end of infra)
Refer to mkcert on GitHub, Caddy Local Certs, openssl x509 docs, ngrok, Cloudflare Tunnel, Tailscale Funnel, MDN Secure Contexts, and HSTS Preload.