feat(relay): add authenticated admin API with bearer-token and network-layer modes - #3777
Open
wpfleger96 wants to merge 7 commits into
Open
feat(relay): add authenticated admin API with bearer-token and network-layer modes#3777wpfleger96 wants to merge 7 commits into
wpfleger96 wants to merge 7 commits into
Conversation
wpfleger96
force-pushed
the
wpfleger/admin-api-bearer-auth
branch
2 times, most recently
from
July 31, 2026 17:59
70b7a95 to
26a140c
Compare
The deployment-admin API at /api/admin/v1 exposed every moderation report, product feedback submission, submitter pubkey, and attachment blob to anyone who could reach the listener with the right Host header. Host and Origin matching is a routing constraint, not authentication. BUZZ_ADMIN_HOST now requires BUZZ_ADMIN_TOKEN and the relay fails closed at startup without it, so no deployment can be upgraded into an unauthenticated state. The credential is checked before Host and Origin so an unauthenticated caller cannot use the response code as an oracle for the expected admin host. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The dashboard keeps the operator bearer token in sessionStorage, but its documents and assets were served with no Content-Security-Policy: the existing policy is middleware on /api/admin/v1 only, which the SPA fallback bypasses. A header is used rather than a meta tag because frame-ancestors is ignored in meta. The policy is scoped to the admin host so the public bundle is unaffected, and allows blob: images because attachments are fetched with the token and rendered from object URLs. The browser suite now proves the attachment object URLs are revoked on both races (replacement/unmount and completion after unmount) and that concurrent 401s collapse into a single re-prompt. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The dashboard's security posture section covered the bearer token and the Host/Origin defense-in-depth but not the response-header CSP that now ships with every admin-host SPA response, leaving operators without the frame and script-origin guarantees they are relying on. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The admin document links /favicon.svg, but the admin-host fallback routed only /assets/*, so the icon 404'd on the one host that serves the dashboard. Vite already emits the file at the bundle root; the fallback now admits that exact path and nothing else, so the bundle directory stays unbrowsable. The CSP paragraph also claimed the policy restricts every network destination. CSP does not constrain top-level navigation, so an executing same-origin script can still navigate with the token in a URL. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… deployments Operators whose admin API is protected at the network layer (VPN, firewall, private ingress) can now set BUZZ_ADMIN_INSECURE_NO_AUTH=true to disable bearer auth while keeping Host/Origin checks as defense-in-depth. This is the intended mode for Block's bb-public relay, which runs behind WARP+Okta. Config semantics: - Only the exact value "true" enables disabled mode; any other non-empty value is a startup ConfigError (no silent typo-coercion). - BUZZ_ADMIN_TOKEN and BUZZ_ADMIN_INSECURE_NO_AUTH=true both set is a startup ConfigError (ambiguous intent). - BUZZ_ADMIN_HOST with neither remains the existing fail-closed error. - A prominent WARN is logged on every startup in disabled mode. SPA probe: the dashboard now probes auth mode on first load (one unauthenticated GET to /api/admin/v1/reports). 200 → skip prompt (relay is in insecure_no_auth mode). Anything else → token prompt (existing behavior). This makes the dashboard work without user interaction for network-layer-protected deployments. Tests added: - Rust (config): insecure_no_auth activates, both-set fails, junk values fail, empty string treated as unset (4 tests) - Rust (api::admin): disabled mode passes all routes, still rejects wrong host and mismatched origin (3 tests) - Playwright (auth): probe 200 skips prompt, probe non-200 shows prompt (2 tests) Docs: README, both CHANGELOGs, and both .env.examples rewritten to describe the final two-mode shape once. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The JSDoc stated true = relay accepted (no auth needed) and false = 401. The code returns true when auth is required (non-200 response) and false when the relay returned 200 (insecure_no_auth mode, no token needed). Correct the comment to match the implementation and function name. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
AdminConfig previously modelled the auth mode as token: Option<AdminToken> plus insecure_no_auth: bool, leaving two invalid states representable and bridging the gap with an .expect() on the hot request path. Replace with an AdminAuth::Token(AdminToken) | AdminAuth::InsecureNoAuth enum so the type system rules out the invalid states and the .expect() is deleted. Config-parse behaviour is byte-identical: same error strings, same WARN messages, same fail-closed matrix, same env var names. Existing tests are updated mechanically (constructor call sites and one pattern-match assertion). Also adds a one-sentence note to docs/admin/README.md that token-mode issues one by-design 401 probe per fresh browser session so operators do not alert on it as an attack. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
force-pushed
the
wpfleger/admin-api-bearer-auth
branch
from
July 31, 2026 19:17
3fcbdc0 to
d014e40
Compare
kalvinnchau
approved these changes
Jul 31, 2026
kalvinnchau
left a comment
Contributor
There was a problem hiding this comment.
Re-reviewed at d014e40. The fail-closed config contract, constant-time bearer validation, host/origin ordering, insecure network-boundary mode, dashboard token lifecycle, authenticated attachment fetches, and CSP/static routing are coherent and covered. Deployment dependency is external: land bb-public#339 and wait for Argo rollout before deploying this relay image.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds explicit authentication to the admin moderation API (
/api/admin/v1). Previously the surface was guarded only byHost/Originheader matching — a stopgap that left it open to anyone who could reach the listener.What this changes
Two authentication modes — pick one per deployment:
Token mode (recommended for public-facing / OSS deployments)
BUZZ_ADMIN_TOKENto exactly 64 hex characters (openssl rand -hex 32)/api/admin/v1request requiresAuthorization: Bearer <token>401 WWW-Authenticate: Bearerbefore any DB or media access; the status is the same regardless ofHost, so there is no oracle leaking the expected authority401→ token prompt; the token is kept insessionStoragefor the browser session onlyNetwork-layer mode (for deployments behind a VPN or private ingress, e.g. Block
sbb-public`)BUZZ_ADMIN_INSECURE_NO_AUTH=true(exact value only; any other non-empty value is a startup error)Host/Originchecks remain as defense-in-depthWARNis logged on every startup200→ render directly, no token promptFail-closed defaults (unchanged behavior for operators who do nothing):
BUZZ_ADMIN_HOSTwith neitherBUZZ_ADMIN_TOKENnorBUZZ_ADMIN_INSECURE_NO_AUTH→ startup errorBUZZ_ADMIN_HOST→ admin surface stays absent, token/flag ignored with a startup warningBreaking change
Any deployed relay with
BUZZ_ADMIN_HOSTset refuses to start after upgrade unless one auth variable is also set. Migration:openssl rand -hex 32→ setBUZZ_ADMIN_TOKEN→ rolls bb-public behind WARP+Okta): setBUZZ_ADMIN_INSECURE_NO_AUTH=true` → rollNon-browser automation hitting
/api/admin/v1in token mode must addAuthorization: Bearerafter upgrade.Files changed
crates/buzz-relay/src/config.rs—AdminConfig.token: Option<AdminToken>, newinsecure_no_auth: bool, env parsing with exact-value validation and mutual-exclusion check, boot WARNcrates/buzz-relay/src/api/admin/auth.rs—authorize()skips bearer in disabled mode; Host/Origin checks unconditional;authorize_bearer()takes&AdminTokendirectlycrates/buzz-relay/src/api/admin/mod.rs— test state helpers for both modes; three new disabled-mode regression testscrates/buzz-relay/src/router.rs—AdminConfigfield update in test helperadmin-web/src/api.ts—send()omits auth header (not throws) when no token;probeAuthRequired()for first-load auth-mode detectionadmin-web/src/App.tsx— probe on first load:200→ skip prompt, anything else → prompt (existing)admin-web/tests/auth.spec.ts— two new probe path tests; updated token-mode mocks to return401on unauthenticated requests (matching real relay behavior)admin-web/tests/csp.spec.ts— CSP coverage for/favicon.svg(round 3)crates/buzz-relay/src/router.rs— favicon routing (round 3)docs/admin/README.md— complete rewrite describing two-mode shape, migration, CSP, local devCHANGELOG.md,crates/buzz-relay/CHANGELOG.md— unreleased breaking change for both modes.env.example,deploy/compose/.env.example— both options documented