Skip to content

feat(relay): add authenticated admin API with bearer-token and network-layer modes - #3777

Open
wpfleger96 wants to merge 7 commits into
mainfrom
wpfleger/admin-api-bearer-auth
Open

feat(relay): add authenticated admin API with bearer-token and network-layer modes#3777
wpfleger96 wants to merge 7 commits into
mainfrom
wpfleger/admin-api-bearer-auth

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 30, 2026

Copy link
Copy Markdown
Member

Adds explicit authentication to the admin moderation API (/api/admin/v1). Previously the surface was guarded only by Host/Origin header 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)

  • Set BUZZ_ADMIN_TOKEN to exactly 64 hex characters (openssl rand -hex 32)
  • Every /api/admin/v1 request requires Authorization: Bearer <token>
  • A missing, malformed, or wrong credential returns a uniform 401 WWW-Authenticate: Bearer before any DB or media access; the status is the same regardless of Host, so there is no oracle leaking the expected authority
  • The dashboard probes on first load: 401 → token prompt; the token is kept in sessionStorage for the browser session only

Network-layer mode (for deployments behind a VPN or private ingress, e.g. Blocks bb-public`)

  • Set BUZZ_ADMIN_INSECURE_NO_AUTH=true (exact value only; any other non-empty value is a startup error)
  • Bearer auth is disabled; Host/Origin checks remain as defense-in-depth
  • A prominent WARN is logged on every startup
  • The dashboard probes on first load: 200 → render directly, no token prompt

Fail-closed defaults (unchanged behavior for operators who do nothing):

  • BUZZ_ADMIN_HOST with neither BUZZ_ADMIN_TOKEN nor BUZZ_ADMIN_INSECURE_NO_AUTH → startup error
  • Both set together → startup error (ambiguous intent)
  • No BUZZ_ADMIN_HOST → admin surface stays absent, token/flag ignored with a startup warning

Breaking change

Any deployed relay with BUZZ_ADMIN_HOST set refuses to start after upgrade unless one auth variable is also set. Migration:

  • Token mode: openssl rand -hex 32 → set BUZZ_ADMIN_TOKEN → roll
  • Network-layer mode (e.g. Blocks bb-public behind WARP+Okta): set BUZZ_ADMIN_INSECURE_NO_AUTH=true` → roll

Non-browser automation hitting /api/admin/v1 in token mode must add Authorization: Bearer after upgrade.

Files changed

  • crates/buzz-relay/src/config.rsAdminConfig.token: Option<AdminToken>, new insecure_no_auth: bool, env parsing with exact-value validation and mutual-exclusion check, boot WARN
  • crates/buzz-relay/src/api/admin/auth.rsauthorize() skips bearer in disabled mode; Host/Origin checks unconditional; authorize_bearer() takes &AdminToken directly
  • crates/buzz-relay/src/api/admin/mod.rs — test state helpers for both modes; three new disabled-mode regression tests
  • crates/buzz-relay/src/router.rsAdminConfig field update in test helper
  • admin-web/src/api.tssend() omits auth header (not throws) when no token; probeAuthRequired() for first-load auth-mode detection
  • admin-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 return 401 on 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 dev
  • CHANGELOG.md, crates/buzz-relay/CHANGELOG.md — unreleased breaking change for both modes
  • .env.example, deploy/compose/.env.example — both options documented

@wpfleger96
wpfleger96 requested a review from a team as a code owner July 30, 2026 17:30
@cameronhotchkies cameronhotchkies added the triage-ready Appropriate for agentic review label Jul 30, 2026
@wpfleger96 wpfleger96 changed the title feat(relay): require a bearer token on the admin moderation API feat(relay): add authenticated admin API with bearer-token and network-layer modes Jul 30, 2026
@wpfleger96
wpfleger96 force-pushed the wpfleger/admin-api-bearer-auth branch 2 times, most recently from 70b7a95 to 26a140c Compare July 31, 2026 17:59
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 7 commits July 31, 2026 15:16
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
wpfleger96 force-pushed the wpfleger/admin-api-bearer-auth branch from 3fcbdc0 to d014e40 Compare July 31, 2026 19:17

@kalvinnchau kalvinnchau 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.

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.

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

Labels

triage-ready Appropriate for agentic review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants