fix(metrics): protect metrics reads with auth + read rate limiting, bound history (#228) - #235
Open
jahswillb-dev wants to merge 1 commit into
Open
Conversation
…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>
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.
Problem
apiKeyAuthandrateLimiterboth guard onlyMUTATING_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
GET /api/v1/metrics{ anchors, activeAnchors, pools, totalLiquidity, settlements, pendingSettlements, totalSettledAmount, totalFeesCollected }. Side effect: appends a snapshot to history on each read.GET /api/v1/metrics/history{ snapshots: [...] }— each snapshot is the 8 fields above plustimestamp; supports?since=<ISO-8601>.MAX_HISTORY = 50viaBoundedHistory(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 exposepools,totalLiquidity— asset coverage and liquidity depth → not safe to exposesettlements,pendingSettlements— activity volume and in-flight load → not safe to exposetotalSettledAmount,totalFeesCollected— value throughput and protocol revenue → not safe to exposetimestamp(history only) — sampling cadence → benign alone, but it turns the above into a time seriesBecause 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:
API_KEYorMETRICS_API_KEYset →GET /api/v1/metricsand/historyrequire a matchingx-api-keyheader, 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_KEYis 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 primaryAPI_KEYis also accepted for metrics, so an operator already holding it needs nothing new.Read-path rate limiting (coordinated with the rate-limiter issue)
Added an opt-in
limitReadsflag torateLimiter— defaultsfalse, 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.tsnow declares anApiKeyAuthsecurity scheme (x-api-keyheader) and marks both metrics operationssecurity: [{ 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 run lintclean,npm run buildclean, 43 suites / 509 tests pass (the prior 42 test files plus the newmetricsAuth.test.ts).Out of scope
No new metrics; no monitoring stack; the general rate-limiter store work is a separate issue.
Closes #228.