Skip to content

Latest commit

 

History

History
 
 

Folders and files

README.md

Hawk Relay

An authenticated, per-env WebSocket relay that lets an authorized operator attach to a running in-cluster eval pod in real time. The relay is a transparent L4 byte pipe over Kubernetes pods/portforward — it never parses or interprets the application protocol (e.g. Inspect ACP).

It is structured like the middleman/ service (sibling top-level dir, src/ layout, uv + pyproject.toml, multi-stage Dockerfile, gunicorn + UvicornWorker). The ECS Fargate / ALB / RBAC infrastructure lives in infra/hawk/relay.py; the container listens on port 8080 and answers GET / for health checks.

Security: authentication before the upgrade

relay.gate.PreUpgradeGate is pure-ASGI middleware that runs before the WebSocket handshake:

  1. Validate the Hawk token (reusing hawk.core.auth.jwt_validator) → 401.
  2. Validate the Origin header against the allow-list → 403.
  3. Authorize the run (model-group write access) → 403, then resolve its runner pod → 404 when the run has no live pod (e.g. it already finished). The authorization step runs first, so a 404 only ever reaches a caller already authorized for the run.

On failure it returns an HTTP status via the ASGI WebSocket Denial Response extension and never sends websocket.accept, so no upgrade occurs (JupyterHub CVE GHSA-w3vc-fx9p-wp4v). The token is read only from the Authorization: Bearer header — no ?token= query fallback, which would leak the token into access logs.

Module seams

Module Responsibility Owner
auth.py Hawk-token extraction + validation → AuthContext this PR
origin.py Pre-upgrade Origin allow-list check this PR
gate.py Pre-upgrade authn + Origin ASGI gate (401/403 before upgrade) this PR
authz.py Per-run authz + server-side pod resolution/pin Task 13
addressing.py Client-named port (Model A) + "acp" alias resolution Task 14
forwarder.py Portforward byte passthrough + isolation + keepalive Task 15
audit.py Connection/decision audit incl. target port Task 16

Configuration

Environment variables are prefixed HAWK_RELAY_ (see relay/settings.py). The JWT settings (HAWK_RELAY_TOKEN_ISSUER, HAWK_RELAY_TOKEN_AUDIENCE, HAWK_RELAY_TOKEN_JWKS_URI, …) mirror the Hawk API's model_access_token_* values so the relay validates the same operator tokens.

Observability

Errors report to Sentry (SENTRY_DSN), traces export via OpenTelemetry → AWS X-Ray (gated by HAWK_OTEL_TRACING_ENABLED, using the shared hawk.core.tracing), and logs are emitted as JSON carrying the active trace id. Each accepted connection becomes one relay.attach span (run id, pod, target port, principal, close reason); gate denials (401/403) are spans too. All three signals scrub operator tokens before anything leaves the process (relay.observability.scrubbing). Datadog ingests the X-Ray traces downstream.

Session-cap metrics land in CloudWatch (namespace Hawk/Relay) via EMF log lines (relay.observability.metrics): active sessions (global and per-principal, the limiter's live ZCARDs), admission rejections by reason, and session duration by close reason. A CloudWatch alarm (infra/hawk/relay.py) fires when the session limiter fails open (Valkey error or unconfigured).

uv sync
uv run pytest
uv run ruff check . && uv run ruff format . --check && uv run basedpyright