feat(server): v2 secure-session-core Task 3 - secure relay WS
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 4m3s
Build and Test / Build Agent (Windows) (push) Successful in 7m48s
Build and Test / Security Audit (push) Successful in 4m20s
Build and Test / Build Summary (push) Has been skipped

SPEC-002 Phase 1 Task 3 (specs/v2-secure-session-core), code-reviewed APPROVED.

- viewer_ws_handler: verify the session-scoped VIEWER token (validate_viewer_token
  sig+exp+purpose) + token_blacklist.is_revoked + session_id claim == requested
  session, before upgrade. Raw login JWTs no longer accepted on the viewer plane
  (closes audit CRITICAL #2; closes the *mechanism* of CRITICAL #1).
- mint_viewer_token: authz gate is_admin() || has_permission("view") -> 403.
- Agent identity binding: validate_agent_api_key returns AgentKeyAuth; a cak_-
  verified agent rebinds to the key's machine identity (fails closed if
  unresolvable), so a key for machine X cannot seize machine Y's session slot.
- Frame caps on both WS upgrades (agent 4 MiB, viewer 64 KiB) - closes WS-OOM HIGH.
- Viewer->agent input throttle (200 ev/s token bucket, bounded try_send) - closes
  input-injection MEDIUM.
- Startup managed-session reconcile clarified.

KNOWN FOLLOW-UPS (tracked todos): (1) authz STRENGTH - the "view" permission is
held by every default role incl. viewer, and a viewer token grants input control,
so the gate should be "control" or a VIEW_ONLY/CONTROL token split; CRITICAL #1 is
mechanism-closed, strength pending decision. (2) revoke minted viewer tokens on
logout (currently bounded only by 5-min TTL). Not cargo-check-verified (no toolchain
on the authoring host).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 19:13:03 -07:00
parent 41691bfb2c
commit 0f258788f9
7 changed files with 340 additions and 64 deletions

View File

@@ -1,10 +1,14 @@
# v2 Secure Session Core — Implementation Plan
> Spec created: 2026-05-29
> Status: in progress — Tasks 1-2 DONE 2026-05-29; Task 3 (relay WS) next.
> Status: in progress — Tasks 1-3 DONE 2026-05-29 (Task 3 code-reviewed APPROVED). REQUIRED follow-up
> before Phase-1 exit: viewer-token authz STRENGTH — the gate uses `view` (held by EVERY default role
> incl. `viewer`) but a viewer token grants input CONTROL; flip to `control`, or split VIEW_ONLY/CONTROL
> tokens (proto already models SCREEN_CONTROL vs VIEW_ONLY). PENDING Mike. Also: nothing revokes a minted
> viewer token on logout (bounded by 5-min TTL) — follow-up todo. Task 4 (rate limiting + single-use codes) next.
> CARRY-FORWARD: Task 3 MUST add a viewer-token AUTHORIZATION check (admin/permission gate) — Task 2
> fixed only the token *mechanism*; the authz gate is what actually closes audit CRITICAL #1. Policy
> (admin-only vs admin-or-view-permission) pending Mike's decision.
> fixed only the token *mechanism*; the authz gate is what actually closes audit CRITICAL #1.
> Policy DECIDED (Mike, 2026-05-29): admin-or-view-permission (`is_admin() || has_permission(...)`).
> Parent: `docs/specs/SPEC-002-v2-modernization-architecture.md` (Phase 1)
> Keystone: Tasks 14 are the "get-right-first" secure auth/session core — every audit CRITICAL/HIGH
> is closed there. Tasks 57 deliver the product capability on top. Do them in order.
@@ -87,7 +91,29 @@ Reference: `relay/mod.rs:224` (`validate_agent_api_key` — the CRITICAL), `auth
---
## Task 3 (KEYSTONE): Secure relay WS handlers + bounded relay
## Task 3 (KEYSTONE) [IMPLEMENTED 2026-05-29 — self-reviewed; no Rust toolchain on this machine, not yet `cargo check`-verified]: Secure relay WS handlers + bounded relay
> [IMPLEMENTED] Viewer WS now verifies the session-scoped VIEWER token
> (`validate_viewer_token`: sig+exp+`purpose`) + `token_blacklist.is_revoked` +
> `session_id` claim == requested session, before upgrade — raw login JWTs are no
> longer accepted (closes CRITICAL #1 mechanism + #2). Authz gate added to
> `mint_viewer_token`: `is_admin() || has_permission("view")` → 403 envelope on
> failure (closes CRITICAL #1 — uses the EXISTING `view` permission from GC's
> catalog; no new permission defined). Agent WS now binds persistent reattach to
> the authenticated machine identity: `validate_agent_api_key` returns an
> `AgentKeyAuth` enum carrying the `cak_` key's machine `agent_id` (resolved via
> new `db::machines::get_machine_by_id`); a mismatched query-string `agent_id` is
> ignored, a per-agent key whose machine can't be resolved fails closed
> (503). Frame caps set on BOTH upgrades (agent 4 MiB, viewer 64 KiB via
> `max_message_size`/`max_frame_size`) (closes WS-OOM HIGH). Viewer→agent input
> throttled to 200 events/sec/viewer via a refilling token bucket + non-blocking
> bounded `try_send` (drop/coalesce on overflow) (closes input-injection MEDIUM).
> Startup managed-session reconcile retained + clarified (persistent machines →
> offline in-memory sessions). Removed `#[allow(dead_code)]` on
> `validate_viewer_token` and `AuthenticatedUser::has_permission`. No token/secret
> logged; runtime `sqlx::query`/`query_as`. Files: `server/src/relay/mod.rs`,
> `server/src/api/sessions.rs`, `server/src/db/machines.rs`, `server/src/auth/mod.rs`,
> `server/src/auth/jwt.rs`, `server/src/main.rs`.
Files touched: `server/src/relay/mod.rs`, `server/src/session/mod.rs`.
@@ -99,8 +125,10 @@ Files touched: `server/src/relay/mod.rs`, `server/src/session/mod.rs`.
must enforce a real permission predicate, not just `AuthenticatedUser`: `user.is_admin() ||
user.has_permission(<policy>)`. GC's role model (`admin|operator|viewer`) + permissions table already
exist (`server/src/auth/mod.rs`), so honoring the intra-tenant role distinction is cheap. **Policy
decision (Mike): admin-only, or admin-or-`view_sessions`-permission.** Multi-tenant client-access
isolation stays deferred to Phase 4; this is only the intra-tenant role gate.
DECIDED (Mike, 2026-05-29): admin-or-view-permission** `user.is_admin() || user.has_permission(<the
session-view/control permission in GC's catalog — use the real name; define one if absent>)`. Enforce
at the minting endpoint `mint_viewer_token` (the authz decision point); the WS then trusts the
session-scoped token. Multi-tenant client-access isolation stays deferred to Phase 4.
- **`agent_ws_handler`** (`relay/mod.rs:55`): authenticate via per-agent key OR support code only
(Task 2). Persistent reattach must bind to the authenticated machine identity, not a query-string
`agent_id` alone (`session/mod.rs:98`).