Files
web-terminal/docs/PLAN_ZERO_TOUCH_ROLLOUT.md

12 KiB
Raw Permalink Blame History

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 native-CA wiring in the new CP (§3.1)RE-ASSESSED 2026-07-18: a real (smallmedium) CODE task, the hard prerequisite for the VPS deploy. The P-256 CA keys + certs exist on the VPS (/etc/relay/{device-ca,frp-client-ca}/*.key.pem), BUT: (1) env.ts does not define NATIVE_FRP_CLIENT_CA_* / NATIVE_DEVICE_CA_* (native-ca.ts:16 flags this as the "concrete follow-up"); (2) buildNativeCas exists but is never called from server.ts — the prod boot doesn't wire the native CAs at all (the 246 tests inject dev inProcessP256CaSigners); (3) production wants a KMS resolver and there is no file-backed signer to load the on-disk PEM keys. Task A2-prep (do before deploy): add the NATIVE_* env vars + a file-backed KmsResolver (load PEM P-256 key → sign) + call buildNativeCas in server.ts and thread the CAs into the enroll services, TDD + reviewed. File-key custody (not KMS) is acceptable for the single-owner VPS (mirrors the existing CA_INTERMEDIATE_KEY_PATH file fallback); document the KMS upgrade as future.
  • [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.