Skip to content

fix(metrics): protect metrics reads with auth + read rate limiting, bound history (#228) - #235

Open
jahswillb-dev wants to merge 1 commit into
AnchorNet-Org:mainfrom
jahswillb-dev:fix/228-protect-metrics-reads
Open

fix(metrics): protect metrics reads with auth + read rate limiting, bound history (#228)#235
jahswillb-dev wants to merge 1 commit into
AnchorNet-Org:mainfrom
jahswillb-dev:fix/228-protect-metrics-reads

Conversation

@jahswillb-dev

@jahswillb-dev jahswillb-dev commented Aug 21, 2026

Copy link
Copy Markdown

Problem

apiKeyAuth and rateLimiter both guard only MUTATING_METHODS (POST/PUT/PATCH/DELETE), so every GET was unauthenticated and unlimited — including the two metrics endpoints, which serve aggregate operational data. Whether that data should be public was never a decision; it was inherited from middleware defaults. This PR makes the exposure decision deliberate.

Endpoint response inventory

Endpoint Response Retention
GET /api/v1/metrics { anchors, activeAnchors, pools, totalLiquidity, settlements, pendingSettlements, totalSettledAmount, totalFeesCollected }. Side effect: appends a snapshot to history on each read. n/a
GET /api/v1/metrics/history { snapshots: [...] } — each snapshot is the 8 fields above plus timestamp; supports ?since=<ISO-8601>. Bounded at MAX_HISTORY = 50 via BoundedHistory (already bounded; now pinned by a route-level test).

Field classification

None of the fields is PII or a secret, but all eight are aggregate operational intelligence — participant counts, liquidity totals, settlement volume and protocol-fee revenue sampled over time. In aggregate they reveal the network's throughput, timing and participant base: useful to an operator, and equally useful to someone profiling the network before targeting it.

  • anchors, activeAnchors — participant count / growth → not safe to expose
  • pools, totalLiquidity — asset coverage and liquidity depth → not safe to expose
  • settlements, pendingSettlements — activity volume and in-flight load → not safe to expose
  • totalSettledAmount, totalFeesCollected — value throughput and protocol revenue → not safe to expose
  • timestamp (history only) — sampling cadence → benign alone, but it turns the above into a time series

Because the fields are homogeneous in sensitivity, the answer is one auth gate over both endpoints, not per-field filtering — there is no "safe half" worth the added complexity.

Decision: protected, opt-in

Reads are protected whenever a key is configured, consistent with the existing "locked only once a key is set" model:

  • No key set → metrics stay open (preserves local/dev behaviour and all existing tests).
  • API_KEY or METRICS_API_KEY setGET /api/v1/metrics and /history require a matching x-api-key header, 401 otherwise.

There is no product requirement for a public network-transparency feed, so the conservative default is to protect — while keeping the switch operator-controlled rather than forced on.

How a legitimate scraper still works

A new read-only METRICS_API_KEY is accepted for metrics reads but not for any mutating route (covered by a test). A monitoring scraper inside the deployment boundary gets metrics access without ever holding the write key. The primary API_KEY is also accepted for metrics, so an operator already holding it needs nothing new.

curl -H "x-api-key: $METRICS_API_KEY" http://localhost:3001/api/v1/metrics

Read-path rate limiting (coordinated with the rate-limiter issue)

Added an opt-in limitReads flag to rateLimiterdefaults false, so the global writes-only limiter is unchanged — and enabled it only on the metrics mount (METRICS_RATE_LIMIT_MAX, default 120/min). This stops the history endpoint being used as a cheap load generator.

Ownership: this PR owns read-limiting for metrics only. Extending read limiting to all routes, and the shared multi-instance store, remains owned by the separate rate-limiter issue. This is stated in the code comment, README and CHANGELOG.

Retention bound

History remains bounded to the most recent 50 snapshots; two new route-level tests prove the cap holds and that the oldest entry is evicted rather than accumulating.

OpenAPI

src/openapi.ts now declares an ApiKeyAuth security scheme (x-api-key header) and marks both metrics operations security: [{ ApiKeyAuth: [] }], with the 50-entry retention bound documented.

Tests

  • src/middleware/metricsAuth.test.ts (new) — open access, primary-key protection, read-only key accepted for reads but rejected for writes, rejected reads record no snapshot.
  • src/routes/metrics.test.ts — retention cap (60 reads → 50 snapshots; oldest evicted) and read rate-limiting (429 over budget, history counts against the same budget).
  • src/config.test.ts, src/openapi.test.ts — new config fields and the security scheme.

Verification

npm ci
npm test src/routes
npm run lint && npm run build && npm test

npm run lint clean, npm run build clean, 43 suites / 509 tests pass (the prior 42 test files plus the new metricsAuth.test.ts).

Out of scope

No new metrics; no monitoring stack; the general rate-limiter store work is a separate issue.

Closes #228.

…et-Org#228]

Aggregate metrics (participant counts, liquidity totals, settlement
volume and fees over time) describe the network's operational state.
Exposing that publicly should be deliberate, not a side effect of the
write-only auth middleware, whose MUTATING_METHODS set left every GET
unauthenticated and unlimited.

- Auth: new metricsAuth guards GET /api/v1/metrics and /history. When
  API_KEY or the new read-only METRICS_API_KEY is set, reads require a
  matching x-api-key (401 otherwise); when neither is set they stay
  open, matching the existing write-auth model. METRICS_API_KEY unlocks
  metrics only, so a scraper never needs the write key.
- Rate limiting: opt-in limitReads flag on rateLimiter (default off, so
  global behaviour is unchanged) enabled only on the metrics mount via
  METRICS_RATE_LIMIT_MAX (default 120/min), so the history endpoint is
  not an unlimited load generator. Global read limiting and the shared
  store remain owned by the separate rate-limiter issue.
- Retention: history stays bounded at MAX_HISTORY = 50, now pinned by
  route-level tests (eviction of the oldest entry).
- openapi.ts declares an ApiKeyAuth scheme and marks both metrics
  operations as protected; README/CHANGELOG document the scraper path.

npm run lint, npm run build and npm test (43 suites, 509 tests) pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

The metrics router exposes snapshot history over HTTP with no authentication — reads are unguarded by design

1 participant