12 KiB
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.
- 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). - 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. - Public
/enrollreachability — CP is loopback; an nginx vhost must proxy/enroll,/device/enroll,/renewover public TLS.curl -i https://<enroll-host>/enrollshould give a 4xx (validation), not 502. - frps trusts
frp-client-CAon the:7000control channel (NOT device-CA). Inspect frpstrustedCaFile. managebearer +CAPABILITY_SIGN_PUBKEY_B64set (else the verifier isrefuseAlland no pairing code can be minted).FRP_AUTH_TOKENidentical on agent side and frps.- nginx :8470 dNSName→Host binding deployed (njs
getCertSub+map→ 403 on mismatch) — the load-bearing isolation control for the device path. - Login/
device:enrollroute — expected: absent today; confirm (defines phone-track start).
4. TRACK A — Host auto-enroll (small–medium; 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.1–3.6 (CA material, CP build, public /enroll, frps CA, manage bearer, token) |
VPS | S–M | 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
- Pre-flight §3 (esp. §3.1 CA material) — a spike; decides whether Track A is "deploy" or "also build a signer."
- Track A (A1–A8) → M-Host: fresh Mac, one command, reboot-durable, self-renewing. Ships the host half of the requirement.
- B1 (login/bearer) → unblocks everything phone-side.
- B3 (iOS) → M-Device-iOS: iPhone auto-enrolls, no
.p12. - B4 (Android) → M-Device-Android.
- 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 (small–medium)
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.tsdoes not defineNATIVE_FRP_CLIENT_CA_*/NATIVE_DEVICE_CA_*(native-ca.ts:16 flags this as the "concrete follow-up"); (2)buildNativeCasexists but is never called fromserver.ts— the prod boot doesn't wire the native CAs at all (the 246 tests inject devinProcessP256CaSigners); (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 theNATIVE_*env vars + a file-backedKmsResolver(load PEM P-256 key → sign) + callbuildNativeCasinserver.tsand thread the CAs into the enroll services, TDD + reviewed. File-key custody (not KMS) is acceptable for the single-owner VPS (mirrors the existingCA_INTERMEDIATE_KEY_PATHfile fallback); document the KMS upgrade as future. - [R2] frps control-channel CA mismatch — must be
frp-client-CA, notdevice-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.0re-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.