Skip to content

gateway: block bifrost realtime routes at the wrapper (default off) - #1695

Merged
Evanfeenstra merged 2 commits into
mainfrom
gateway-block-realtime-routes
Sep 15, 2026
Merged

Evanfeenstra merged 2 commits into
mainfrom
gateway-block-realtime-routes

Conversation

@Evanfeenstra

@Evanfeenstra Evanfeenstra commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

Two commits:

  1. Block bifrost's realtime routes at the wrapper (the security fix).
  2. Stop tracking the gateway/wrapper/wrapper build artifact.

1. Block realtime routes

What

Blocks bifrost's realtime route family at the wrapper, before it can reach bifrost-http. Off by default; opt back in with BIFROST_ENABLE_REALTIME=1.

Why

Bifrost's realtime WebSocket / WebRTC handlers dial the upstream provider — with the account's real API key — during connection setup, which runs before the per-turn virtual-key check. So an unauthenticated caller who just opens a socket makes the gateway open a provider connection on the org's account.

Verified live on swarm38 (public :8181, port-based SSL):

wss://swarm38.sphinx.chat:8181/v1/realtime?model=gpt-realtime
  → 101 Switching Protocols
  → {"type":"session.created", ...}   # genuine OpenAI greeting, no credential

Sending response.create on that socket returns 401 virtual key is required, so this is not free token generation — the mandatory-VK gate (enforce_auth_on_inference: true) still holds. But it is an unauthenticated upstream-connection / concurrent-session-quota-exhaustion / key-validity-oracle vector, and it is independent of enforce_macaroons (the gateway's macaroon plugin hooks PreLLMHook; realtime runs its own turn pipeline and never reaches it).

This is upstream bifrost behavior (transports/v1.6.2), so the fix lives in our wrapper rather than waiting on an upstream change.

How

  • New isRealtimePath matches on the realtime path segment, so every prefix variant bifrost registers is covered: /realtime, /v1/realtime, /openai/realtime, /openai/v1/realtime, and the /calls, /client_secrets, /sessions subpaths. Exact-segment match means /v1/realtimeless is not blocked.
  • Blocked requests get 403 + a small JSON body and a log line (abuse visibility). Because the wrapper returns before proxying, bifrost-http's realtime handler never runs and no provider socket is dialed.
  • BIFROST_ENABLE_REALTIME (truthy: 1/true/yes/on) re-enables the routes if a swarm ever needs voice. Hive's agents are text-only, so default-off is safe.
  • Routing logic extracted into newRouter() so it is unit-tested against fake upstreams (no live loopback dial).

2. Remove the tracked wrapper binary

gateway/wrapper/wrapper (~8.7MB, host-specific compiled Go binary) was checked into the repo. It's a build output, not source — the Docker image builds the wrapper fresh in the wrapper-builder stage (go build -o /out/wrapper .) and never uses the committed copy. Any local go build ./... overwrites it and produces a spurious multi-MB diff (it did while building this PR). Removed from tracking and added wrapper/wrapper to gateway/.gitignore.

Test

go test ./... in gateway/wrapper passes, including the new realtime_test.go:

  • isRealtimePath blocks the full realtime family (all prefixes, case-insensitive) and does not misfire on substrings.
  • realtimeEnabled env parsing.
  • newRouter: realtime blocked → 403 and bifrost upstream never hit; realtime allowed when enabled → reaches bifrost; normal inference and /_plugin/* unaffected; no-plugin-server → 503.

Also confirmed a local go build . in wrapper/ no longer shows up in git status (ignored).

Notes

  • No change to the VK gate or macaroon enforcement; the realtime block is defense-in-depth removing an attack surface.
  • Needs a gateway release + swarm image bump to reach swarm38.

Bifrost's realtime WebSocket / WebRTC handlers dial the upstream
provider — with the account's real API key — during connection setup,
which runs before the per-turn virtual-key check. So an unauthenticated
caller who just opens `wss://<gateway>/v1/realtime?model=gpt-realtime`
makes the gateway open a provider socket on the org's account (verified
on swarm38: 101 upgrade + a genuine OpenAI `session.created`). The
mandatory-VK check still blocks token generation (`response.create` →
401), so this is not free inference, but it is an unauthenticated
upstream-connection / quota-exhaustion / key-validity vector, and the
gateway's macaroon plugin never sees realtime at all (it hooks
PreLLMHook; realtime runs its own turn pipeline).

Refuse the whole realtime family at the wrapper, before it reaches
bifrost-http, so the provider socket is never dialed. Match on the
`realtime` path segment so every prefix variant is covered
(`/realtime`, `/v1/realtime`, `/openai/**/realtime`, and their
`/calls`, `/client_secrets`, `/sessions` subpaths). Hive's agents are
text-only; set BIFROST_ENABLE_REALTIME=1 to opt back in.

Split the routing decision into newRouter() so it is unit-tested with
fake upstreams (no live loopback dial). Blocked requests get 403 +
a JSON body and are logged for abuse visibility.
`gateway/wrapper/wrapper` is a compiled Go binary (~8.7MB, host-specific)
that was checked into the repo. It is a build output, not source: the
Docker image builds the wrapper fresh in the Dockerfile's wrapper-builder
stage (`go build -o /out/wrapper .`) and never uses the committed copy.
Any local `go build ./...` in wrapper/ overwrites it and produces a
spurious multi-MB diff.

Remove it from tracking and gitignore `wrapper/wrapper` so a local build
no longer shows up as a change.
@Evanfeenstra
Evanfeenstra merged commit 588afcb into main Sep 15, 2026
9 checks passed
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