From 1ffffc8d26f887732c342b4864adff08a41ff8c7 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Fri, 3 Apr 2026 19:38:09 -0700 Subject: [PATCH] docs: add HTTPS/TLS setup design Design for muxplex setup-tls command with auto-detection: - Tailscale cert (real LE, universally trusted) - mkcert (local CA, zero browser warnings) - Self-signed fallback (works but browser warns) Covers settings integration, CLI flags, service integration, doctor diagnostics, and error handling edge cases. --- docs/plans/2026-04-03-tls-setup-design.md | 203 ++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 docs/plans/2026-04-03-tls-setup-design.md diff --git a/docs/plans/2026-04-03-tls-setup-design.md b/docs/plans/2026-04-03-tls-setup-design.md new file mode 100644 index 0000000..08e5347 --- /dev/null +++ b/docs/plans/2026-04-03-tls-setup-design.md @@ -0,0 +1,203 @@ +# HTTPS/TLS Setup Design + +## Goal + +Add HTTPS support to muxplex so the browser Clipboard API works on non-localhost devices, via a `muxplex setup-tls` command with auto-detection that chooses the best available TLS method. + +## Background + +The browser Clipboard API (`navigator.clipboard`) requires a secure context — either `localhost` or HTTPS. When accessing muxplex from another device on the LAN (the common multi-device use case), clipboard operations silently fail because the connection is plain HTTP. This blocks copy/paste workflows that are central to terminal use. + +Rather than requiring users to manually configure TLS certificates, muxplex should detect what's available on the system and set up the best option automatically. + +## Approach + +Auto-detection with tiered strategies. A single command — `muxplex setup-tls` — detects what's available and does the right thing: + +1. **Tailscale** running + MagicDNS → `tailscale cert` (real Let's Encrypt cert, universally trusted) +2. **mkcert** installed → local CA cert (trusted on this machine, zero browser warnings) +3. **Fallback** → self-signed via Python (browser shows warning, but clipboard works) + +No new Python package dependencies. `ssl` is stdlib, `subprocess` handles mkcert/tailscale CLI calls. uvicorn's built-in SSL support handles the serving side. + +## Architecture + +### Settings Integration + +Two new keys in `settings.py` `DEFAULT_SETTINGS`: + +| Key | Default | Description | +|-----|---------|-------------| +| `tls_cert` | `""` | Path to TLS certificate file. Empty = HTTP. | +| `tls_key` | `""` | Path to TLS private key file. Empty = HTTP. | + +In `serve()`, if both are non-empty and files exist on disk, pass to `uvicorn.run(ssl_certfile=tls_cert, ssl_keyfile=tls_key)`. The server prints `https://` in its startup URL instead of `http://`. + +If only one is set, or files don't exist at the configured paths, warn and fall back to HTTP. + +### CLI Flags + +`--tls-cert` and `--tls-key` flags on `muxplex serve`, using the same None-sentinel pattern as `--host` / `--port`. These override settings.json values. + +`muxplex config set tls_cert /path/to/cert.pem` also works for persistent configuration. + +### Detection Flow + +``` +muxplex setup-tls [--method=auto|tailscale|mkcert|selfsigned] + +Default: --method=auto + +1. Is Tailscale running + MagicDNS enabled? + → YES: Use tailscale cert (real LE cert, universally trusted) + → NO: continue + +2. Is mkcert installed? + → YES: Use mkcert (local CA, trusted on this machine) + → NO: continue + +3. Fallback: Generate self-signed via Python ssl module + → Works but browser shows warning + → Print: "For zero-warning HTTPS, install mkcert or use Tailscale" +``` + +--- + +## Components + +### New Module: `muxplex/tls.py` + +Contains all TLS-related logic: auto-detection, cert generation for each method, cert inspection (expiry, SANs), and status reporting. + +### `muxplex setup-tls` Subcommand + +**What it does:** + +- Generates cert + key to `~/.config/muxplex/cert.pem` and `~/.config/muxplex/key.pem` +- Writes `tls_cert` and `tls_key` paths to `settings.json` +- Prints method used, hostname(s) in the cert, and expiry date +- Prints: "Restart service to apply: `muxplex service restart`" + +**Tailscale variant:** + +- Auto-detects MagicDNS hostname via `tailscale status --self --json` +- Runs `tailscale cert --cert-file ~/.config/muxplex/cert.pem --key-file ~/.config/muxplex/key.pem` +- Includes the local hostname as a reminder to access via `https://spark-1.tail8f3c4e.ts.net:8088` +- Notes the 90-day expiry and suggests: "Run `muxplex setup-tls` again to renew" + +**mkcert variant:** + +- Runs `mkcert -install` (may prompt for sudo/keychain access) +- Generates cert for: `$(hostname)`, `$(hostname).local`, `localhost`, `127.0.0.1`, `::1` +- If Tailscale is detected, also adds the Tailscale IP and MagicDNS name as SANs +- Prints instructions for trusting on other LAN devices: "Copy `$(mkcert -CAROOT)/rootCA.pem` to other devices" + +**Self-signed fallback:** + +- Uses Python `ssl` or `openssl` CLI to generate a basic self-signed cert +- Warns: "Browsers will show a security warning. Install mkcert for trusted certs." + +### `muxplex setup-tls --status` + +Shows current TLS state: method used, cert paths, expiry date, hostnames in the cert. Reuses the same inspection logic as `doctor`'s TLS section. + +### Settings Changes (`muxplex/settings.py`) + +Add `tls_cert` and `tls_key` to `DEFAULT_SETTINGS` with empty string defaults. + +### Serve Changes (`muxplex/cli.py`) + +- Add `--tls-cert` and `--tls-key` CLI flags to `serve` subcommand +- In `serve()`, resolve TLS paths (CLI flag → settings.json → empty) +- If both paths are non-empty and files exist, pass `ssl_certfile` and `ssl_keyfile` to `uvicorn.run()` +- Print `https://` URL on startup when TLS is active + +### Doctor Integration + +`muxplex doctor` shows TLS status: + +- TLS enabled: `TLS: enabled (cert expires 2036-04-01)` +- TLS disabled: `TLS: disabled (clipboard requires HTTPS on non-localhost)` +- Cert expired: `TLS: WARNING — cert expired 5 days ago. Run muxplex setup-tls to renew` + +--- + +## Service Integration + +The service file (systemd/launchd) doesn't need to change — `serve()` already reads `tls_cert` and `tls_key` from `settings.json`. The service runs `muxplex serve` with zero extra flags. + +**URL detection in `muxplex service install`:** + +- TLS enabled: `Service started → https://spark-1.tail8f3c4e.ts.net:8088` +- TLS disabled: `Service started → http://0.0.0.0:8088` + +**Tailscale cert renewal (90-day expiry):** `setup-tls` prints renewal instructions. No auto-cron job — users set that up themselves if wanted. + +--- + +## Error Handling + +### Detection Edge Cases + +| Scenario | Behavior | +|----------|----------| +| Tailscale installed but not connected | Skip to mkcert detection | +| Tailscale connected but HTTPS certs not enabled | Print: "Enable HTTPS Certificates in your Tailscale admin console, then re-run" | +| mkcert installed but `mkcert -install` fails (no sudo, no certutil) | Warn and fall back to self-signed | +| Certs already exist from a previous run | Prompt: "TLS already configured (method: tailscale, expires 2026-07-03). Regenerate? [y/N]" | + +### Runtime Edge Cases + +| Scenario | Behavior | +|----------|----------| +| Cert files in settings but deleted from disk | Warn "TLS cert not found at path, falling back to HTTP" and start without SSL | +| Cert expired | `doctor` warns with expiry info and renewal instructions | +| Port conflict with HTTPS | Existing `_kill_stale_port_holder` behavior handles it | +| Only one of cert/key configured | Warn about incomplete TLS config, fall back to HTTP | + +### WebSocket Considerations + +- The frontend's `connectWebSocket()` already auto-detects `wss:` vs `ws:` from `location.protocol` — no change needed +- The federation proxy's `websockets.connect()` to remote instances already handles both `ws://` and `wss://` since `remote_instances[].url` can be either `http://` or `https://` + +--- + +## Files to Modify + +| File | Change | +|------|--------| +| `muxplex/settings.py` | Add `tls_cert`, `tls_key` to `DEFAULT_SETTINGS` | +| `muxplex/cli.py` | Add `setup-tls` subcommand, `--tls-cert`/`--tls-key` flags, SSL pass-through in `serve()` | +| `muxplex/tls.py` | **New module:** auto-detection, tailscale cert, mkcert, self-signed generation, cert inspection | +| `README.md` | Document `setup-tls` command and TLS configuration | + +No changes needed to `muxplex/main.py` — uvicorn handles SSL transparently. + +## Dependencies + +- **No new Python package dependencies** — `ssl` is stdlib, `subprocess` for mkcert/tailscale CLI +- **mkcert** — optional external tool, detected at runtime +- **Tailscale** — optional external tool, detected at runtime +- **uvicorn SSL** — built-in support, already a dependency + +## Testing Strategy + +- Unit tests for detection logic (mock `shutil.which`, `subprocess.run` for each tool) +- Unit tests for cert inspection (expiry parsing, SAN extraction) +- Integration tests for `serve()` with SSL cert/key paths (valid and invalid) +- CLI tests for `setup-tls --status` output formatting +- Edge case tests: missing files, partial config, expired certs + +## Effort Estimate + +| Phase | Work | Time | +|-------|------|------| +| 1 | Settings + SSL serve | ~2 hours | +| 2 | `setup-tls` + auto-detection + all 3 methods | ~4 hours | +| 3 | Doctor integration + service URL detection | ~1 hour | +| 4 | README + tests | ~2 hours | +| **Total** | | **~9 hours** | + +## Open Questions + +None — all sections validated.