Skip to content

feat(server): require a bearer token on the http transport - #105

Merged
plusky merged 1 commit into
mainfrom
feat/http-token
Aug 18, 2026
Merged

feat(server): require a bearer token on the http transport#105
plusky merged 1 commit into
mainfrom
feat/http-token

Conversation

@plusky

@plusky plusky commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Ruoqa-mcp-compatible scoped bearer authentication for the HTTP transport (#32, fleet alignment). BREAKING: http (the default transport) now refuses to start without BUGWARDEN_HTTP_TOKEN (write scope) or BUGWARDEN_HTTP_READ_TOKEN (read scope); --insecure-no-auth is the explicit escape hatch (CLI-only — I9).

Contract: env-only tokens (argv is ps-readable), ≥32 printable non-space ASCII, read≠write, deny-by-default resolved before any port binds or audit file exists, constant-time comparison with both tokens always compared, one byte-identical empty 401 + WWW-Authenticate: Bearer for every refusal cause including unrouted paths and duplicate Authorization headers (I2). Read scope reuses I13's WRITE_TOOLS set: filtered tools/list, and a refused write call answers the router's own "tool not found" with zero upstream requests — a hidden tool is indistinguishable from a nonexistent one, now also under a failing audit sink (the refusal gate checks routing first; this closed a pre-existing sibling leak for policy-removed discovery tools). stdio ignores tokens. principal stays None — a deployment credential is not a caller; #32 remains open for (caller, RRID).

Adversarial review before opening

Security lens: 29 bypass probes all byte-identical 401s; no session-scope escalation; I12 clean under RUST_LOG=trace. Its two proven findings — an oracle under failing-sink+closed_all and the main.rs scope wiring being unpinned (deleting it left 463 tests green) — are fixed, each with a killing test. Docs lens: falsified man-page preamble, unqualified allowed-hosts rationale (in three places), unrunnable examples, stale MCP client config — all corrected; the gate's closing of server/discover is now a recorded DESIGN.md decision.

The auth core was rewritten independently after review flagged verbatim overlap with ruoqa-mcp's GPL source: 0 identical doc-comment lines, 0 identical function bodies remain (mechanical difflib check); the compatible part is the wire contract, not the code.

Verification

Rebased on current main. All five AGENTS.md commands independently re-run: fmt/clippy×2 clean, 471 passed / 0 failed, cargo deny fully green. 24/24 mutations killed. Assets regenerated byte-stable.

Over stdio the client owns the process, so the principal is whoever
launched it. Over http the port was the whole access control, and under
server-held key custody that means anyone who reaches the port is served
with the deployment's Bugzilla credential. http is now deny-by-default.

Two environment-only tokens define two scopes: BUGWARDEN_HTTP_TOKEN
reaches every tool the policy serves, BUGWARDEN_HTTP_READ_TOKEN the
complement of WRITE_TOOLS — the same set read-only mode removes from the
router (I13), so the per-caller split cannot drift from the
per-deployment one. Neither value enters Cli, so I12 holds structurally
rather than through a redacting Debug impl.

The gate is an axum layer wrapping the whole router, ahead of rmcp: an
unauthenticated caller reaches no MCP handling, and an unrouted path is
refused identically to /mcp — one empty 401 with WWW-Authenticate:
Bearer, byte-identical however the credential was wrong (I2). Duplicate
Authorization headers are refused the same way. A read-scope caller is
served a filtered listing and the router's own unknown-tool error for a
write tool, so a hidden tool is indistinguishable from a nonexistent one
— including under a failing audit sink, where the refusal gate now
checks routing first (this also stops a failing sink from answering
differently for policy-removed discovery tools).

Resolved first of all in main, ahead of the policy load, the Bugzilla
client, the identity preflight and the audit sink, so a
misconfiguration binds no port and neither creates nor rotates an audit
file.

The contract is wire-compatible with ruoqa-mcp's bearer scheme for
fleet alignment (#32); the implementation is bugwarden's own.

Partial delivery of #32: this authenticates a deployment, not a caller,
so ClientInfo.principal stays None and #32 stays open until a verified
caller identity fills it.

BREAKING CHANGE: the http transport, which is the default, refuses to
start without BUGWARDEN_HTTP_TOKEN or BUGWARDEN_HTTP_READ_TOKEN. A bare
`bugwarden --bugzilla-server ...` that worked before now exits 1. Pass
--insecure-no-auth to keep the old behaviour on a trusted network; it
has no environment variable, because I9 lets flags tighten only.

Refs: #32
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