Files
web-terminal/docs/PLAN_ZERO_TOUCH_ROLLOUT.md
Yaojia Wang 232ef22535 docs: zero-touch enrollment rollout & integration plan
Execution plan grounded in current VPS reality: pre-flight checks, Track A
(host auto-enroll via the agent) and Track B (phone auto-enroll), risks, and
the honest one-bootstrap-tap constraint. R1 (CA material) verified present on VPS.
2026-07-18 13:32:05 +02:00

162 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Zero-Touch Enrollment — Rollout & Integration Plan
> **This is the EXECUTION plan** (deploy + wire + fill the gaps), grounded in the *current*
> deployment reality. The DESIGN blueprint is `docs/PLAN_TUNNEL_AUTOMATION.md`; this doc says
> what to actually do next, in order, to reach the product requirement.
>
> **Product requirement (operator, 2026-07-18):** ① 主机端启动即自动连服务器 + 自动管理证书(零手动装证书);② 客户端(手机)连接时自动处理好证书(零手动 .p12)。
---
## 0. The honest constraint (read first)
The **first** enrollment of each identity cannot be zero *human* actions: issuing a tunnel cert to
an unknown machine/phone with no auth = anyone who reaches the endpoint gets a cert. So exactly **one
authenticated human action at bootstrap is unavoidable**: paste a one-time pairing code (host) / log
in once (phone). **Everything after** — renew, rotate, reboot-survival — is genuinely silent.
> "Zero-touch" here honestly means **"one bootstrap tap, then never again"**, not "no human step ever."
> Every step below is designed so that single tap is the *only* manual action for the product lifetime.
---
## 1. Current state (verified 2026-07-18)
| Piece | State |
|---|---|
| `control-plane/` (PKI/CA + `/enroll` + `/device/enroll` + `/renew`) | **Built, 246 tests pass.** Loopback `127.0.0.1:8080`, fail-closed prod boot (needs real KMS CA material). |
| `agent/` host agent (`pair <CODE> --install`) | **Built, 267 tests pass.** launchd user-agent; keygen→CSR→enroll→frpc.toml→2 units→supervised frpc. |
| iOS device-enroll library (SecureEnclave→CSR→cert) | **Built + unit-tested**, but **zero app callers** (no UI). |
| Android / Desktop device-enroll | **None** (Android from scratch; Desktop deferred). |
| Server **login route + `device:enroll` bearer mint** | **MISSING — hard blocker for the phone track.** `loginToAccountId` is a deny-all stub. |
| VPS control-plane | **NOT running** (`:8080` down); code on VPS is the **older `feat/relay-phase1`** branch. |
| VPS tunnel data path (frps + nginx:8470 mTLS + LE wildcard cert) | **UP** (from M1). LE `*.terminal.yaojia.wang` valid → 2026-10-05. |
| My manual `mac1` host tunnel | **Live**, but static hand-issued certs — bypasses the control-plane. Control cert correctly from **frp-client-CA**, so CA lineage already matches the agent path (low migration risk). |
**Bottom line:** the automated system is ~built but **not deployed and not wired**; the phone track has a
real server-code hole (login/bearer). My `mac1` is a manual stand-in to be retired.
---
## 2. Architecture (target flows)
**Host** (`web-terminal-agent pair <CODE> --install`): generate P-256 key (never leaves host, 0600) →
PKCS#10 CSR → `POST /enroll {code, machineId, agentPubkey, csr}` → CA returns frp-client leaf
(`dNSName SAN = <sub>.terminal.yaojia.wang`) + caChain → SHA-256-verify-download pinned `frpc` v0.61.1 →
write `frpc.toml` (mTLS control channel, `serverName=frp.terminal.yaojia.wang`, one http proxy →
`127.0.0.1:PORT`) + base-app `ALLOWED_ORIGINS` → install **two launchd units** (base app + supervised
agent) → reboot-durable. **Renew** silently at ~2/3 TTL via `POST /renew` (mTLS w/ current cert).
**Phone**: one-time **account login** → short-lived `device:enroll` bearer → app generates a
**non-exportable hardware key** (Secure Enclave / StrongBox) → hardware-signed CSR →
`POST /device/enroll {csr, subdomain, deviceName, attestation?}` (Bearer) → CA verifies token +
account-owns-subdomain (deny-by-default) + PoP + cap/rate-limit → returns device leaf + caChain →
app stores identity against the hardware key, presents on the **already-wired mTLS path**. Silent renew
via `POST /device/:id/renew`.
**Three PKI roots (do not conflate):** frp-client-CA (P-256, host control channel, frps:7000) ·
device-CA (P-256, device data path, nginx:8470 `ssl_verify_client`) · relay agent-CA (Ed25519, legacy).
---
## 3. PRE-FLIGHT: deployment unknowns to verify (blockers before any build)
Run against VPS `8.138.1.192`. **These gate feasibility — resolve before committing to the tracks.**
1. **CP CA material** — does production KMS/CA material exist for the *native* CAs the new CP needs
(`NATIVE_FRP_CLIENT_CA_*`, `NATIVE_DEVICE_CA_*`, `CA_INTERMEDIATE_*`)? CP is **fail-closed**: no real
material ⇒ it won't boot (or falls back to dev placeholder leaves = unusable). **This is the #1 risk.**
The relay-phase1 deploy had `/etc/relay/ca/{root,intermediate}` + a P-256 device-CA + frp-client-CA
on disk — confirm these can back the new CP's signer (KMS refs vs on-disk keys).
2. **CP branch/deploy** — the newer CP (with the enroll/renew endpoints as-tested) must be checked out
& built on the VPS (currently `feat/relay-phase1`). Confirm PG + Redis reachable + migrated.
3. **Public `/enroll` reachability** — CP is loopback; an nginx vhost must proxy `/enroll`,`/device/enroll`,
`/renew` over public TLS. `curl -i https://<enroll-host>/enroll` should give a 4xx (validation), not 502.
4. **frps trusts `frp-client-CA`** on the `:7000` control channel (NOT device-CA). Inspect frps `trustedCaFile`.
5. **`manage` bearer + `CAPABILITY_SIGN_PUBKEY_B64`** set (else the verifier is `refuseAll` and no pairing
code can be minted).
6. **`FRP_AUTH_TOKEN`** identical on agent side and frps.
7. **nginx :8470 dNSName→Host binding** deployed (njs `getCertSub` + `map` → 403 on mismatch) — the
load-bearing isolation control for the device path.
8. **Login/`device:enroll` route** — expected: **absent today**; confirm (defines phone-track start).
---
## 4. TRACK A — Host auto-enroll (smallmedium; mostly deploy + wire)
**Goal:** fresh Mac → one pairing code → `pair --install` → auto-connect + self-managing cert, reboot-durable.
| # | Task | Where | Effort | Notes / risk |
|---|---|---|---|---|
| A1 | Resolve pre-flight §3.13.6 (CA material, CP build, public `/enroll`, frps CA, manage bearer, token) | VPS | SM | Gated by §3.1 KMS/CA material — the real unknown. |
| A2 | Deploy + run the new control-plane in **production mode** (real CA material, PG/Redis, systemd unit) | VPS `control-plane/` | M | Replace the old relay-phase1 CP; keep loopback:8080. |
| A3 | nginx vhost proxying `/enroll` `/device/enroll` `/renew` → 127.0.0.1:8080 over the LE wildcard TLS | `deploy/nginx` + VPS | S | Add to existing :443 SNI / a dedicated enroll host. |
| A4 | Mint one **pairing code** (`POST /accounts/:id/pairing-codes`, `manage` bearer) | VPS | S | MVP = manually pasted code (RFC 8628 device-grant deferred). |
| A5 | **Wire auto-renew into the native run-loop** — call `createCertRotator`/`renewCert` from `superviseNative` | `agent/src` (**only real host code gap**) | S | Logic already built+tested; today `superviseNative` only *monitors* freshness → cert dies at 24h. TDD it. |
| A6 | Build the agent (`dist/cli.js`) + set env (`ENROLL_URL`,`TUNNEL_DOMAIN`,`TUNNEL_ZONE=terminal`,`FRP_AUTH_TOKEN`,`PORT`,`BIND_HOST=127.0.0.1`, dummy `RELAY_URL`) | Mac | S | |
| A7 | Run `web-terminal-agent pair <CODE> --install`; verify 2 launchd units + `https://<sub>.terminal.yaojia.wang` live via device cert | Mac | S | |
| A8 | **Retire manual `mac1`** — bootout `com.webterm.tunnel.{baseapp,frpc}`, remove `frpc.toml`/certs | Mac | S | Only after A7 green. |
| A9 | (optional) `machineId` dedup so reinstall keeps the same subdomain | `agent/src`, `control-plane/registry` | S | Deferred; cosmetic (`status` output). |
**Deliverable A:** computer start → auto-connect + auto-managed/renewing cert; one pairing code at first
setup, silent thereafter. **Blocking risk = §3.1 (production CA material).**
---
## 5. TRACK B — Phone auto-enroll (build-heavy; server hole first)
**Goal:** phone → one login → device cert auto-issued to hardware key → connect, no `.p12`, ever.
| # | Task | Where | Effort | Notes / risk |
|---|---|---|---|---|
| B1 | **Build + deploy the CP login route + `device:enroll` bearer mint** (`auth/session.ts mintDeviceEnrollToken`, add `/login` or `/auth`, add `enroll` to `CapabilityRightSchema`) | `control-plane/src/auth` | M | **HARD BLOCKER — do first; nothing in the phone flow is reachable without a bearer.** Even a single-operator credential seam unblocks it (full OIDC later). |
| B2 | Bridge: after login, mint bearer + scope to the account's subdomain(s) (deny-by-default already enforced by CP) | `control-plane` | S | |
| B3 | **iOS**: wire `KeychainClientIdentityStore.enroll` into an enrollment screen (login → bearer → CSR → `/device/enroll` → store SE identity → present on existing mTLS path) + rotation scheduler | `ios/` | M | Library exists + unit-tested; this is UI + flow wiring. |
| B4 | **Android**: build the enroll library from scratch (StrongBox keygen + CSR + `/device/enroll` client) + present via existing `X509KeyManager` | `android/` | L | Mirror the iOS ClientTLS package; no existing code. |
| B5 | (optional) scan-to-enroll: make the pairing QR carry a `device:enroll` credential (QR & device-enroll are disjoint today) | app + CP `pairing` | M | UX nicety; not required for correctness. |
| B6 | Desktop (Electron) programmatic OS-keychain install | `desktop/src` | M | **Deferred** — Chromium only reads certs from the OS store; today assumes pre-install. |
**Deliverable B:** phone → login once → connects with an auto-issued, hardware-bound, auto-renewing cert.
---
## 6. Deliberately deferred (layer on later, no rework)
Attestation verifiers (Apple App Attest / Android Key-Attestation) — mandatory only before a 2nd
distrusting tenant · signed X.509 **CRL** + VPS reload webhook (MVP relies on 24h passive revocation) ·
**B2** per-tenant name-constrained intermediates (gate for a 2nd customer) · RFC 8628 device-grant
(approve-in-app) wrapping the same `/enroll` · Windows service · agent autoupdate · legacy `.p12`
dual-trust migration window.
---
## 7. Suggested order & milestones
1. **Pre-flight §3** (esp. §3.1 CA material) — a spike; decides whether Track A is "deploy" or "also build a signer."
2. **Track A (A1A8)****M-Host**: fresh Mac, one command, reboot-durable, self-renewing. *Ships the host half of the requirement.*
3. **B1 (login/bearer)** → unblocks everything phone-side.
4. **B3 (iOS)****M-Device-iOS**: iPhone auto-enrolls, no `.p12`.
5. **B4 (Android)****M-Device-Android**.
6. Later: attestation → CRL → B2 (per-tenant) = gate for a 2nd distrusting customer.
## 8. Top risks
- **[R1] Production CA/KMS material for the new CP (§3.1)** — **VERIFIED 2026-07-18: DOWNGRADED.** The
P-256 CA private keys + certs already exist on the VPS (`/etc/relay/{ca(root+intermediate),device-ca,
frp-client-ca}/*.key.pem`). The new CP wants them wired as `NATIVE_FRP_CLIENT_CA_*` / `NATIVE_DEVICE_CA_*`
KMS-ref + cert-DER (the old `control-plane.env` only carries the intermediate ref). So this is a
**config-wiring task** (point CP at the existing keys, possibly via a file/local-KMS shim — confirm
`control-plane/src/boot/native-ca.ts` + `ca-wiring.ts` accept a file path vs strict KMS), **not** a
"build a CA" sub-project. No longer a showstopper for Track A.
- **[R2] frps control-channel CA mismatch** — must be `frp-client-CA`, not `device-CA`; crossed = silent
1006/handshake failure (this class of bug already cost days in M1).
- **[R3] nginx dNSName→Host binding** — the load-bearing isolation control; needs positive **and**
negative tests (leaf-A must be refused for subdomain-B) even in MVP.
- **[R4] Phone login trust seam (B1)** — the whole phone track hinges on a credential mechanism; a weak
seam here is a security hole (it mints certs). Scope it deliberately even for single-operator.
- **[R5] BIND_HOST loopback** — mandatory on every host; `0.0.0.0` re-opens an unauth shell on the LAN
bypassing mTLS. The agent enforces it (S-GATE) — keep it enforced.
---
_Grounded against `develop` + VPS `8.138.1.192` as of 2026-07-18. Design source: `docs/PLAN_TUNNEL_AUTOMATION.md`._