Files
web-terminal/docs/DEPLOY_RELAY.md
Yaojia Wang 340ea48478 docs(relay): deployment build & ops plan (DEPLOY_RELAY.md)
Grounded assessment that the relay service is pre-production (every cross-package
seam is a fail-closed stub, no server/TLS/DB/orchestration). Documents the target
topology + trust boundaries, the 7 wiring gaps to close (P1 socket server, P5
injection into P1/P3, pg/redis stores, real KMS CA custody, agent/relay-web build,
F2 principal + F6 replay-epoch residuals), a phased build order (dev-run → staging
→ prod), an accurate per-service env reference, infra checklist, and an ops runbook.
2026-07-03 14:16:30 +02:00

11 KiB
Raw Permalink Blame History

Deploying the rendezvous-relay service — build & ops plan

Status: PRE-PRODUCTION — not runnable as-is. The 7 relay packages (P1P6 + contracts) are libraries wired through injection seams that currently hold fail-closed stubs. There is no server process, no TLS/subdomain ingress, no Postgres/Redis/KMS wiring, and no orchestration. "Deploying" the relay means building the integration + infra layer the packages were designed to receive. This doc is the buildable spec: architecture, the seam-by-seam gaps, a phased order, the env/config reference, and the ops runbook. Security posture of the packages themselves is covered in REVIEW_RELAY_SECURITY.md (findings F1F6 fixed).


1. Target architecture

A rendezvous relay: the agent runs on the user's own host (no inbound ports there) and dials out to the relay; the browser connects to a per-tenant subdomain; the relay muxes the two and does an opaque byte splice — it only ever sees ciphertext (INV2). End-to-end crypto (P4) is between browser and agent; auth/tenant-isolation (P5) gates every upgrade; the control-plane (P3) is the registry + CA + revocation publisher.

   Browser (P6, xterm)                              User's host machine
        │ WSS  <sub>.term.<domain>                        │
        │ capability token (subprotocol/cookie) + DPoP    │ shell / node-pty
        ▼                                                 ▲ loopback (opaque, INV11)
 ┌──────────────────────────┐   agent mux (mTLS,     ┌────┴───────────────┐
 │  RELAY DATA PLANE (P1)    │◀──SPIFFE cert)─────────│   AGENT (P2)       │
 │  term-relay/data-plane    │   dial-OUT             │   dist/cli.js, ws  │
 │  • TLS + WSS ingress      │                        │   E2E host endpoint│
 │  • onUpgrade → P5 authz   │                        │   replay sealer    │
 │  • subdomain → hostId     │                        └────────────────────┘
 │  • OPAQUE splice (INV2)   │                                 ▲ enroll/pair
 │  • Redis sub relay:revoc. │                                 │
 └───────┬──────────────────┬┘                                 │
         │ P5 authz (lib)    │ Redis pub/sub                    │
         ▼                   ▼                                  │
 ┌──────────────────┐  ┌─────────┐   ┌──────────────────────────┴─┐
 │ relay-auth (P5)  │  │  Redis  │◀──│  CONTROL PLANE (P3)         │
 │ tokens+DPoP,     │  │ relay:  │   │  control-plane, Fastify     │
 │ step-up, mTLS/   │  │ revoc.  │   │  • accounts/hosts/sessions  │
 │ SPIFFE, revoke   │  └─────────┘   │  • pairing + /enroll (CA)   │──▶ Postgres
 └──────────────────┘                │  • issues agent SPIFFE certs│──▶ KMS (CA key)
                                     │  • publishes KillSignals    │
                                     └─────────────────────────────┘

Trust boundaries: the relay is UNTRUSTED for payload confidentiality (E2E, INV2); it IS trusted for availability + authz enforcement. The control-plane holds the CA (must be KMS-custodied). Agents authenticate to the relay with SPIFFE mTLS; browsers authenticate with short-lived capability tokens (minted by P5 after human auth) bound to a DPoP key. Never expose any port without TLS.


2. Components & runtime

Pkg Role Runtime today External deps
relay-contracts frozen schemas/codecs library
relay-e2e (P4) browser↔agent E2E crypto library (isomorphic) @noble/*
relay-auth (P5) authz, tokens+DPoP, step-up, mTLS, revoke library audited
term-relay (P1) stateless relay data-plane + mux library — no server (needs ws, tls)
control-plane (P3) accounts/hosts/pairing/CA/revocation Fastify app, in-memory default fastify, pg, ioredis, KMS
agent (P2) host agent: dial, pair, mTLS, E2E, replay CLI web-terminal-agentdist/cli.js ws
relay-web (P6) browser bundle (xterm, preview) needs build + static serve @xterm/*

3. The wiring gaps (what must be BUILT to deploy)

Each is an injected seam currently holding a safe stub. In rough dependency order:

  1. P1 socket server (biggest gap). term-relay/data-plane/relay-node.ts takes WebSocketLike
    • AgentListener as injected seams — there is no actual listener. Build a relay-run/ entry that: (a) terminates browser WSS on BIND_PORT and adapts each socket to WebSocketLike; (b) accepts agent mTLS connections on AGENT_BIND_PORT (verify SPIFFE client certs against AGENT_CA_CHAIN_PATH) and adapts them to AgentListener/mux; (c) calls authorizeUpgrade (→ P5 onUpgrade) and splices. Wire the Redis relay:revocations subscriber to closeStream.
  2. Inject P5 into P1 + P3. P1's Authorizer must call relay-auth onUpgrade/onReattach with a real EnforceDeps (registries backed by P3, revocation store, buckets, audit). P3's refuseAllVerifier must be replaced with relay-auth verifyCapabilityToken + the real CAPABILITY_SIGN_PUBKEY_B64. Close F2 here: the P1 upgrade adapter currently hardcodes principal: null — for any host with a step-up policy it must resolve + pass the authenticated principal, else those hosts (correctly) 403. See relay-stepup-needs-principal-wiring.
  3. P3 durable stores. Swap createMemoryStores for the Postgres adapter (accounts/hosts/sessions registries) + run migrations; point the revocation bus at real Redis (ioredis).
  4. Real KMS for the CA key. boot/ca-wiring.ts uses a dev in-process Ed25519 signer ("NOT a real KMS"). Provide a KmsResolver backed by cloud KMS/HSM; the intermediate key must be non-exportable (§3.1). CA_INTERMEDIATE_KMS_KEY_REF is the ref.
  5. Agent build + config. Build dist/cli.js; configure RELAY_URL/ENROLL_URL/SUBDOMAIN/ HOST_ID/LOCAL_TARGET_URL/STATE_DIR; enroll (redeem pairing code → SPIFFE cert + hostContentSecret).
  6. relay-web build + serve. npm --prefix relay-web run build; serve the bundle from each tenant subdomain (same origin as the WSS endpoint so Origin/CSWSH checks pass).
  7. F6 replay transport (residual). The recoverable-replay ring buffer must persist each generation's epoch and serve the epoch matching those exact frames (relay-web/.../manage-page.ts loadReplay is still a throwing stub). Until wired, the preview/replay feature is inert (fail-closed).

4. Phased build order

  • Phase 0 — single-host dev run (one process, self-signed). Reuse the e2e/harness wiring: a ws server hosting the relay-node, control-plane with in-memory stores, P4/P5 wired for real, self-signed TLS, agent dialing localhost, relay-web served locally. Goal: an actually-running end-to-end relay to click through. No pg/redis/KMS.
  • Phase 1 — single-tenant staging. Real Postgres + Redis; real (staging) TLS cert for one subdomain; close F2 principal wiring; agent on a second machine dialing in; behind Tailscale.
  • Phase 2 — multi-tenant production. Wildcard TLS *.term.<domain> + subdomain routing; real KMS CA custody; horizontal relay nodes (stateless, share Redis); migrations/rollback; metering; F6 replay transport; monitoring/alerting on the audit sink + revocation latency budget.

5. Environment / config reference (accurate to the code)

control-plane (P3)control-plane/src/env.ts, all required unless defaulted: PG_URL, REDIS_URL, CAPABILITY_SIGN_PUBKEY_B64 (P5 verify pubkey), CA_INTERMEDIATE_KMS_KEY_REF, CA_INTERMEDIATE_CERT_PATH, NODE_MTLS_TRUST_BUNDLE_PATH, BASE_DOMAIN, HEARTBEAT_TTL_SEC=15, PAIRING_TTL_SEC=600, PAIRING_MAX_REDEEM_ATTEMPTS=5. Routes: POST /accounts, POST /accounts/:id/pairing-codes, POST /accounts/:id/status, GET /accounts/:id/hosts, DELETE /hosts/:hostId, POST /enroll. (No start script / listen entry exists yet — add one.)

term-relay data-plane (P1)term-relay/data-plane/config.ts: BASE_DOMAIN, BIND_HOST, BIND_PORT (browser WSS), TLS_CERT_PATH, TLS_KEY_PATH, AGENT_BIND_PORT, AGENT_CA_CERT_PATH, AGENT_CA_CHAIN_PATH (verify agent mTLS), RELAY_NODE_ID, HEARTBEAT_INTERVAL_MS, INITIAL_WINDOW_BYTES, MAX_FRAME_BYTES, ROUTE_TTL_MS. (Library — the process that reads this config and listens must be built, gap #1.)

relay-auth (P5)RELAY_AUTH_VERIFY_PUBKEY (base64url raw Ed25519) via loadVerifyKeyFromEnv; RELAY_TRUST_DOMAIN for SPIFFE.

agent (P2)RELAY_URL, ENROLL_URL, HOST_ID, SUBDOMAIN, LOCAL_TARGET_URL, STATE_DIR.


6. Infra checklist

  • TLS: browser-facing wildcard cert *.term.<domain> (Phase 2) or per-subdomain (Phase 1); separate CA + trust bundle for agent mTLS (AGENT_CA_* / NODE_MTLS_TRUST_BUNDLE_PATH).
  • DNS: wildcard *.term.<domain> → relay node(s); <control-plane host>.
  • Postgres: accounts/hosts/sessions schema + migrations (adapter unbuilt).
  • Redis: relay:revocations pub/sub (P3 publishes, P1 subscribes) — the INV12 teardown path.
  • KMS/HSM: non-exportable CA intermediate signing key (CA_INTERMEDIATE_KMS_KEY_REF).
  • Ingress for agents: agents dial OUT (mTLS) — expose only AGENT_BIND_PORT + browser BIND_PORT.
  • Orchestration: Dockerfiles + compose/k8s; relay nodes are stateless (crash-safe, INV7).

7. Security/ops runbook (deploy invariants)

  • Origin/CSWSH: the browser WSS handshake MUST validate Origin against the tenant subdomain (relay-auth onUpgrade step 1 enforces it — pass the correct allowedOrigins). Never disable.
  • Never expose without TLS; the relay hands a shell to whoever authenticates. Prefer Tailscale for the agent path.
  • Key custody (INV9): P5 signing key and the CA key never in process memory / logs; KMS refs only.
  • Revocation latency (INV12): monitor time from revoke() → stream teardown; alert on budget miss.
  • mTLS trust anchors: agent certs verified against a pinned CA bundle — never a peer-supplied chain (relay-auth verifyAgentCert gates on the registry too).
  • Audit sink (INV10): zero-payload; ship to append-only storage; alert on deny spikes / cross-tenant-attempt.
  • Close before prod: F2 principal wiring (gap #2) and F6 replay-epoch transport (gap #7).

8. TL;DR

There is no one-command deploy today. Minimum to get a running relay = Phase 0 (gaps #1, #2, #5, #6 with in-memory stores + self-signed TLS). Production = all gaps + Postgres/Redis/KMS/wildcard-TLS

  • orchestration. The security-critical logic is done and audited; what remains is integration + infra.