Skip to content

fix(security): allow /metrics to require a bearer token - #138

Open
fjaeckel wants to merge 1 commit into
mainfrom
security/metrics-exposure
Open

fix(security): allow /metrics to require a bearer token#138
fjaeckel wants to merge 1 commit into
mainfrom
security/metrics-exposure

Conversation

@fjaeckel

Copy link
Copy Markdown
Owner

Problem

GET /metrics is served without any authentication. Against a locally-run instance it returned 340 lines of Prometheus output to an unauthenticated client, including per-route request counts and latency histograms, Go runtime and GC internals, process memory/FD counts, and DB connection-pool stats. Route-level counters leak usage volume and endpoint inventory; go_info/process_* leak build and host details useful for fingerprinting.

Change

  • New internal/api/middleware/metrics_auth.goMetricsAuthMiddleware(token) compares the presented credential with subtle.ConstantTimeCompare. It accepts either Authorization: Bearer <token> or the bare token, and rejects everything else with a bodyless 401.
  • cmd/api/main.go reads METRICS_TOKEN and applies the middleware to the /metrics route when it is set.
  • .env.example documents the variable.
  • internal/api/middleware/metrics_auth_test.go — 7 sub-tests covering the bearer form, the bare form, a missing header, a wrong token, a case-mismatched scheme, an empty presented value, and a token that is a prefix-extension of the expected value.

Deliberate design decision

When METRICS_TOKEN is unset the endpoint stays open, and the server logs a warning at startup:

/metrics is exposed without authentication; set METRICS_TOKEN to require a bearer token

Failing closed by default would silently break existing scrapers on upgrade — a deployment whose Prometheus is on an internal network would start returning 401 with no code change on their side, and the failure surfaces as a gap in dashboards rather than an error anyone reads. The warning makes the exposure visible in logs while leaving the operator in control of when to turn the gate on. If you would rather fail closed and accept the upgrade break, that is a one-line change and I am happy to make it.

Verification

  • go test ./... — 24 packages pass
  • gofmt -l clean
  • Manual: with METRICS_TOKEN set, curl /metrics401; curl -H 'Authorization: Bearer $TOKEN' /metrics200 with the metric body

Generated by Claude Code

The Prometheus endpoint was registered with no authentication. It exposes Go
runtime stats, DB pool stats, per-path request counters and auth
attempt/failure counters -- useful reconnaissance for anyone who can reach the
port, and the user/registration counters make it a slow enumeration oracle.
Live-confirmed: GET /metrics with no credentials returned 200 with 340 metric
lines.

METRICS_TOKEN now gates the endpoint behind a bearer token, compared in
constant time so response timing cannot leak it prefix-wise. Both 'Bearer
<token>' and a raw token are accepted, since Prometheus' bearer_token_file
sends the former while simple scrape configs often send the latter.

When METRICS_TOKEN is unset the endpoint stays open and a startup warning names
the setting. That keeps existing deployments working: the intended topology
scrapes over the internal Docker network with the API port unpublished, where
the endpoint is already unreachable from outside. Failing closed would silently
break those scrapes on upgrade, so this is opt-in with a loud default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GukWfyJMY28qv2CJjxFvKF
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.

2 participants