Files
muxplex/docs/plans/2026-04-03-tls-setup-design.md
T
Brian Krabach 1ffffc8d26 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.
2026-04-03 22:44:17 -07:00

204 lines
8.3 KiB
Markdown

# 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.