Skip to content

fix(agent-runner): bound unauthenticated ACP bridge handshakes - #708

Open
kayahk wants to merge 2 commits into
Paca-AI:masterfrom
siedlegroup:security/acpbridge-hello-limit
Open

kayahk wants to merge 2 commits into
Paca-AI:masterfrom
siedlegroup:security/acpbridge-hello-limit

Conversation

@kayahk

@kayahk kayahk commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Why

/agent-bridge/ws is the daemon-facing WebSocket for acp-type agents. It calls
websocket.Accept before it can check any credential — by design, the agent
id and bridge token arrive in the first hello frame. Two gaps made the
unauthenticated window effectively unbounded:

  1. conn.SetReadLimit(10 * 1024 * 1024) was set before the hello was read.
    A peer with no token could make the server buffer a bridge-sized frame.
  2. Nothing limited concurrent unauthenticated connections. helloTimeout
    (10 s) bounded one socket's lifetime, not how many could be opened, so an
    anonymous flood could exhaust memory/fds on the agent-runner pod.

With the shipped deployment sizes (1 replica, 512 MiB limit), ~50 concurrent
10 MiB pre-auth frames were enough to OOMKill the process — which also serves the
environment/terminal endpoints, so it takes live bridges and environment
operations down with it. This matters more once an edge layer exempts the path
from browser authentication, since then the endpoint is reachable anonymously.

What changed

  • The hello frame is read under a 1 KiB limit (helloReadLimit). The
    documented 10 MiB bridge limit (bridgeMessageReadLimit) is applied after
    the token validates, so legitimate event payloads are unchanged.
  • A process-wide budget of 128 pending handshakes (helloSlots,
    maxPendingHellos) refuses excess upgrades with 503 before Accept —
    there is no socket to allocate when the service is saturated. The slot is
    released as soon as the token checks out, an early return happens, or the
    handler panics.
  • docs/ai-agent/agent-runner-service.md documents the handshake bound, so
    anyone adding an auth exemption for this path knows not to remove it.

No configuration, interface or protocol change; apps/acp-bridge is untouched.

Tests

services/agent-runner/internal/acpbridge/server_hello_test.go:

  • TestHandleWSRejectsOversizedPreAuthFrame — a frame above the hello limit is
    closed with 1009 (StatusMessageTooBig). Without the fix the same frame is
    read at 10 MiB and fails JSON decoding with 1007 instead.
  • TestHandleWSRefusesWhenHandshakeBudgetIsExhausted — with the budget full,
    the endpoint answers 503. Without the fix it answers 426 (upgrade
    required), i.e. it accepts the connection.
  • TestHandleWSReleasesHandshakeSlotAfterRejectedHello — a rejected hello
    returns its slot, so failures cannot leak the budget.

Both failure modes were reproduced against the unpatched handler before the fix.

go build ./..., go vet ./internal/... and go test ./internal/... -count=1
pass (e2e suites not run — they need Docker/Postgres).

Follow-ups (not in this PR)

  • A configurable limit, or per-source accounting, if 128 proves too tight for
    many bridges behind one NAT.
  • Until a release carries this, keep the bridge on a host-local port-forward or
    a browser-authenticated edge rather than exempting the path.

/agent-bridge/ws accepts the WebSocket upgrade before it can check any
credential: the agent id and bridge token arrive in the first frame, and
the token is validated only afterwards. Two things made that window
unbounded.

The 10 MiB read limit was set before the hello was read, so an
unauthenticated peer could allocate a bridge-sized frame. The hello is
now read under a 1 KiB limit, raised to 10 MiB only after the token
checks out.

Nothing limited concurrent unauthenticated connections either, so
anonymous sockets could sit in the 10 s hello window as fast as they
could connect. A process-wide budget of 128 pending handshakes now
refuses excess upgrades with 503 before the socket is accepted, and the
slot is released as soon as the token validates (or the connection
ends).

A legitimate bridge is unaffected: its hello is a few hundred bytes, it
sends it immediately, and it never competes for more than one slot.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — two minor suggestions inline.

Reviewed changes

  • Pre-auth read limit — helloReadLimit (1 KiB) now bounds the hello frame; the 10 MiB bridge limit is only restored after the bridge token validates.
  • Handshake budget — a 128-slot process-wide semaphore gates handleWS before websocket.Accept, answering 503 when saturated and releasing the slot on every exit path, including panics.
  • Docs — documents the handshake bound and warns against removing it when exempting the path from edge authentication.
  • Tests — three new unit tests cover the oversized pre-auth frame (1009), budget exhaustion (503), and slot release after a rejected hello.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

…e post-auth gap

Copilot review on Paca-AI#708: wsjson.Read answers 1007 for a malformed hello, not
the handler 4400, and the post-auth read-limit raise has no oversized-frame
coverage yet.
@kayahk

kayahk commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

CI on this PR has never executed

Both agent-runner-pr-ci runs on this branch are action_required with zero jobs:

  • c0f4c506 → run 42070968396 (action_required, 17:04Z)
  • 8f3bf99c → run 42072652875 (action_required, 17:14Z)

POST /actions/runs/42072652875/approve answers 403 Must have admin rights to Repository, so I cannot release the run. Until a repo admin approves one of them (or re-runs the workflow), the only evidence for this change is local:

  • go test -count=1 ./internal/acpbridge/ at 8f3bf99c → ok … 1.236s
  • go vet ./internal/acpbridge/ → clean

That covers the package only. Lint and the full agent-runner suite have not run against this head, so I am not claiming them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant