Committable VPS artifacts for the native reverse-tunnel (no live-ops, no secrets):
- gen-device-ca.sh: EC P-256 self-signed device CA (CA:TRUE pathlen:0, keyCertSign+cRLSign),
openssl ca db + empty CRL, idempotent, --kind frp-client scaffolds the control-channel CA.
- issue-device-cert.sh: P-256 leaf, EKU=clientAuth only, 825d, emits .p12 (-legacy) + .pem/.key/.fullchain,
copy_extensions=none prevents CSR-injected serverAuth/CA:TRUE; CN traversal guard.
- revoke-device.sh: openssl ca revoke -> regenerate crl.pem (revocation enforced via -crl_check).
- frp/{frps.toml.example,frpc.example.toml}: control-channel mTLS+token, disableCustomTLSFirstByte=true.
- nginx/frp-mtls.conf: :8470 TLS-term, ssl_verify_client on + device-CA + ssl_crl, WS-upgrade proxy to :7080.
- nginx/stream-sni-additions.md: the two additive SNI map lines (merge-only, coexistence-safe).
Verified: gen->issue->openssl verify->revoke->CRL chain green in a tmp dir (OpenSSL 3.0.18).
74 lines
4.2 KiB
Markdown
74 lines
4.2 KiB
Markdown
# Native mTLS Tunnel — per-host runbook (V7)
|
||
|
||
Bring one local host onto the tunnel so native clients (iOS / Android / desktop) can reach its base
|
||
app at `https://<name>.terminal.yaojia.wang` over the VPS, gated **only** by device-certificate mTLS.
|
||
See `docs/PLAN_NATIVE_TUNNEL.md` for the full design; this is the short operational checklist.
|
||
|
||
**Trust model: A — single-owner fleet** (all devices/hosts are the operator's). A shared device-CA
|
||
means "the closed set of my devices"; there is **no cross-tenant isolation** — do not enroll
|
||
distrusting third parties on this CA (that is Model B, out of scope for v1).
|
||
|
||
## Files here
|
||
|
||
| File | Install target on the VPS | Purpose |
|
||
|---|---|---|
|
||
| `frps.toml.example` | `/etc/relay/frp/frps.toml` (0600) | frps control `:7000` + vhost `:7080` |
|
||
| `frpc.example.toml` | on each LOCAL host (0600) | dials the VPS, exposes `127.0.0.1:3000` |
|
||
| `../nginx/frp-mtls.conf` | `/etc/nginx/conf.d/frp-mtls.conf` | `:8470` TLS-term + device-CA mTLS + WS proxy |
|
||
| `../nginx/stream-sni-additions.md` | (merge into the live stream map) | the two additive SNI routes |
|
||
|
||
## One-time VPS setup (summarised — see PLAN V1–V5)
|
||
|
||
1. **frps** (`frps.toml.example` → `/etc/relay/frp/frps.toml`): set `auth.token` = `openssl rand -hex 32`.
|
||
2. **frp-client CA** (control-channel mTLS): `bash deploy/scripts/gen-device-ca.sh --kind frp-client`.
|
||
3. **device CA** (data-path mTLS): `bash deploy/scripts/gen-device-ca.sh` (KIND=device default).
|
||
4. **LE wildcard** `*.terminal.yaojia.wang` (V3) → `/etc/relay/frp-tls/{fullchain,privkey}.pem`.
|
||
5. **nginx** `:8470` (`frp-mtls.conf`) + the two stream SNI lines (`stream-sni-additions.md`), then
|
||
`nginx -t && systemctl reload nginx`.
|
||
|
||
## Onboard a host (the repeatable part)
|
||
|
||
1. **frp-client cert** for this host (control mTLS), issued from the frp-client CA:
|
||
```bash
|
||
CA_DIR=/etc/relay/frp-client-ca bash deploy/scripts/issue-device-cert.sh <name> ./out-<name>
|
||
# use out-<name>/<name>.cert.pem + <name>.key.pem for frpc.certFile/keyFile (ignore the .p12)
|
||
```
|
||
2. **frpc.toml** on the host from `frpc.example.toml`: set `subdomain=<name>`, the shared
|
||
`auth.token`, and the frp-client cert/key paths; `chmod 600 frpc.toml`.
|
||
3. **Base app** on the host — the tunnel is safe ONLY with these:
|
||
| Var | Value | Why |
|
||
|---|---|---|
|
||
| `BIND_HOST` | `127.0.0.1` | **Mandatory.** Default `0.0.0.0` exposes an unauth'd shell on the LAN, bypassing mTLS. |
|
||
| `PORT` | `3000` | frpc dials `127.0.0.1:3000`. |
|
||
| `ALLOWED_ORIGINS` | `https://<name>.terminal.yaojia.wang` | bare host; `:443` also matches (normalized). |
|
||
| `IDLE_TTL` | `≥ 86400` | walk-away survival. |
|
||
| `USE_TMUX` | `1` (*nix) / `0` (Windows) | cross-restart PTY survival. |
|
||
4. **Device cert** for whoever will connect (data-path mTLS), issued from the device CA:
|
||
```bash
|
||
P12_PASSWORD='...' bash deploy/scripts/issue-device-cert.sh <person-or-device> ./out-dev
|
||
# deliver out-dev/<...>.p12 SECURELY: scp / AirDrop / Files — NEVER email.
|
||
```
|
||
5. **Verify** (M1): no-cert → TLS handshake failure; `curl --cert …/<dev>.cert.pem --key …/<dev>.key.pem
|
||
-sI https://<name>.terminal.yaojia.wang/live-sessions` → `200`.
|
||
|
||
## Revoke a device
|
||
|
||
```bash
|
||
CA_DIR=/etc/relay/device-ca bash deploy/scripts/revoke-device.sh /path/to/<dev>.cert.pem
|
||
# copy the regenerated /etc/relay/device-ca/crl.pem to the VPS, then ON THE VPS:
|
||
nginx -t && nginx -s reload
|
||
```
|
||
|
||
The revoked cert is rejected fleet-wide the moment nginx reloads the CRL.
|
||
|
||
## Trust-boundary note (Model A)
|
||
|
||
- The **frp-client cert + token is a trusted secret**: leaking a host's frp-client cert **and** the
|
||
shared token lets an attacker register a subdomain (subdomain-takeover / MITM). Keep both `chmod 600`;
|
||
deliver over scp, never email. Revoke a compromised host by rotating the `frp-client-ca` trust
|
||
(re-issue the remaining hosts) — device certs are unaffected.
|
||
- The **device cert** is the data-path gate. Under Model A any valid device cert can reach any
|
||
subdomain and can read `/live-sessions/:id/preview` scrollback — acceptable because every device is
|
||
yours. Do not extend this CA to third parties without moving to Model B (per-tenant CA).
|
||
- **P-256, never Ed25519** for both CAs (nginx `ssl_verify_client` + iOS client-cert compatibility).
|