Secure

Authenticate callers, protect the control plane, and verify the boundary before exposing a self-hosted router.

3 min readEdit this page

The safe local default relies on the operating system: BitRouter listens on loopback and accepts an anonymous local caller. A shared deployment needs an explicit identity boundary before the listener becomes reachable.

Authenticate callers

server:
  listen: "127.0.0.1:4356"
  skip_auth: false

Changing skip_auth requires a daemon restart. Make that change and verify the 401 check below before widening network access.

Mint a brvk_ virtual key against the same database the daemon uses:

bro key sign --user alice --db sqlite:///var/lib/bitrouter/bitrouter.db

The plaintext secret is shown once. A caller sends it as a bearer token:

curl https://router.internal.example.com/v1/chat/completions \
  -H "Authorization: Bearer brvk_..." \
  -H "Content-Type: application/json" \
  -d '{"model":"openai:gpt-5","messages":[{"role":"user","content":"Hello"}]}'

With skip_auth: false, BitRouter requires a virtual-key-shaped credential, resolves its stored hash, and checks that the key is active and unexpired. The resulting caller identity is attached to metering and telemetry.

bro key sign defaults its database path from the current shell, while the daemon resolves relative database paths from the config directory. Pass --db explicitly on a deployed host so the key is not written to a different SQLite file.

The self-hosted CLI does not currently expose mint-time flags for per-key spend, request-rate, or expiry limits. Do not assume those controls exist merely because the database schema has fields for them.

Protect the administration path

Daemon-control commands use a Unix socket rather than the HTTP API:

server:
  control_socket: "/run/bitrouter/bitrouter.sock"

Anyone who can write to that socket can reload, restart, or stop the process. Put it in a directory owned by the service user and keep control commands on the host.

Keep these boundaries separate:

BoundaryControl
Client to reverse proxyTLS and network policy
Caller to BitRouterskip_auth: false and brvk_ keys
Operator to daemonControl-socket filesystem permissions
Request contentGuardrails and workflow policy
Provider credentialsEnvironment or secret-store permissions

Public read endpoints

GET /health and GET /v1/models are not protected by the inference authentication hook. If model and provider discovery is sensitive, restrict those paths at the reverse proxy or network layer. GET /metrics is not a substitute for telemetry in the shipped binary; use OpenTelemetry.

Production checklist

Before allowing traffic from another machine:

  1. Set listen explicitly and keep BitRouter on loopback when a local reverse proxy can own the public interface.
  2. Set skip_auth: false before changing network reachability.
  3. Mint and test a virtual key against the daemon's actual database.
  4. Terminate TLS without buffering long-lived model streams.
  5. Commit only ${VAR} references, never provider secrets.
  6. Run as an unprivileged service user and restrict the control socket.
  7. Validate the pinned config in CI and again on the target host.
  8. Back up state before an upgrade that may need rollback.

Verify that an unauthenticated inference request is rejected:

curl -s -o /dev/null -w '%{http_code}\n' \
  -X POST http://127.0.0.1:4356/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"openai:gpt-5","messages":[{"role":"user","content":"hi"}]}'

Expect 401. Testing /health or /v1/models does not verify authentication because those reads remain available without a virtual key.

How is this guide?

On this page