feat(auth): gate /metrics/prometheus behind optional METRICS_TOKEN#164
Conversation
Both PR #156 reviewers flagged GET /metrics/prometheus as the one remaining always-open data-bearing surface after the fail-closed auth change. Decision (option b of the follow-up): add a dedicated METRICS_TOKEN bearer credential rather than documenting a scrape-network assumption, matching the DASHBOARD_TOKEN posture exactly: - METRICS_TOKEN set (any env): scrape requires Authorization: Bearer <token> (the header Prometheus sends via the authorization scrape config); constant-time comparison; 401 otherwise. - Unset in dev: endpoint stays open (scrape convenience, unchanged). - Unset with NODE_ENV=production: 503 fail closed, mirroring requireDashboardAuth/requireAdminAuth, with a startup warning. The payload remains aggregate and tenant-label-free either way (src/metrics.js). Docs: AUTH.md (fourth auth layer + env table + security note), SECURITY.md new section 8 with a prometheus.yml authorization example, OPERATIONS.md production checklist, QUICK_REFERENCE.md scrape note, .env.example, and docker-compose.yml passthrough. Tests: 7 integration tests covering the dev-open regression, 401 wrong/missing token, 200 with token, and production 503-unconfigured/401-configured paths.
surpradhan
left a comment
There was a problem hiding this comment.
Independent validation report (empirical, run — not just read)
Validated at PR head e8f74a1 in an isolated worktree; gh pr diff 164 is byte-identical to the diff of e8f74a1 vs base 0e09eb7 (no uncommitted work missing from the PR). Node v24.11.1.
1. Gates — all green, counts match the PR's claims exactly
npm run lint → exit 0 (eslint src tests, clean)
npm run test:unit → tests 494 / pass 494 / fail 0
npm run test:integration → tests 271 / pass 271 / fail 0
2. Discriminator probe — the new tests are not vacuous
Reverted src/auth.js + src/server.js to pre-PR (0e09eb7) and re-ran only the METRICS_TOKEN suite:
7 tests: 3 pass, 4 FAIL against old code
✖ unauthenticated scrape returns 401 (old code: 200)
✖ wrong bearer token returns 401 (old code: 200)
✖ scrape returns 503 when unconfigured in prod (old code: 200)
✖ unauth scrape returns 401 (not 503) when set (old code: 200)
Exactly the 4 enforcement tests fail; the 3 that pass on old code are the dev-open regression lock and the two positive-path (correct-token → 200) tests, as expected. PR state restored; suite then re-run 3x at head: 7/7, 7/7, 7/7 (no flakiness).
3. Live-server matrix — real node src/server.js, ephemeral ports, temp DATABASE_PATH
| Probe | Result | Expected |
|---|---|---|
(a) NODE_ENV=production, unset → scrape |
503 {"error":"Metrics not configured","hint":"Set the METRICS_TOKEN…"} |
503 ✔ |
| (a) startup log | 1× METRICS_TOKEN not set — /metrics/prometheus returns 503 (production fails closed) |
warn ✔ |
(a) /health, /ready |
200, 200 | unaffected ✔ |
| (b) production + token: no token / wrong token / correct token | 401 / 401 / 200 | ✔ |
| (b) 200 payload | contains aep_events_received_total; grep -ci tenant = 0; only label keys used: method,route,status,le |
aggregate, tenant-free ✔ |
| (b) startup log | 0 warnings (token set) | ✔ |
| (c) no NODE_ENV, unset | 200 + …is open (dev mode) warning |
dev-open ✔ |
| (d) no NODE_ENV, token set: no token / correct | 401 / 200; /health 200 |
✔ |
4. Edge probes
| Probe | Result |
|---|---|
NODE_ENV=prod (not exactly "production"), unset |
200 — treated as dev, matches .env.example wording ✔ |
/v1/metrics/prometheus |
404 — endpoint stays unversioned, consistent with VERSIONING.md ✔ |
METRICS_TOKEN= (empty string), production |
503 + "not set" startup warning — empty ≡ unset ✔ |
METRICS_TOKEN= (empty string), dev |
200 dev-open ✔ (re-verified on a second clean port after detecting a stale unrelated server on the first port — result held) |
Authorization: <raw token> (no Bearer prefix) |
401 ✔ |
Authorization: Basic <token> |
401 ✔ |
JSON /metrics |
untouched — still requireReadAccess (src/server.js:890), API-key path unaffected ✔ |
5. Docs spot-check
SECURITY.md §8 prometheus.yml snippet is correct: the authorization scrape-config block defaults type: Bearer, so credentials: <token> sends exactly Authorization: Bearer <token> — which extractBearer requires. Section numbering §1–§8 is clean. src/openapi.json does not document /metrics/prometheus (infra endpoint), so no spec drift there.
Findings
- Minor — SETUP.md:544 — the endpoint reference table still reads
| GET | /metrics/prometheus | none | … Unauthenticated — restrict at the network layer if needed. |. Now factually wrong (production: 503/401) and contradicts the PR's own updates to QUICK_REFERENCE/AUTH/SECURITY/OPERATIONS. Fix: change auth column to something likemetrics tokenand the note to the METRICS_TOKEN behavior (legend on line 534 may need a[metrics]entry). - Nit — SECURITY.md:152–157 — the "§2 Set Strong Tokens" snippet generates only
DASHBOARD_TOKEN/ADMIN_TOKEN; addMETRICS_TOKEN=$(openssl rand -hex 32)for consistency with the updated checklist and §8.
No code-level findings: requireMetricsAuth mirrors the existing dashboard/admin idioms (per-request env read so tests can toggle, safeEqual timing-safe compare incl. length-mismatch path, bearer-only extraction), and CHANGELOG's historical "no auth" line is a changelog entry, correctly left as history.
Verdict: Approve (with the two doc touch-ups above — SETUP.md:544 is worth fixing before or immediately after merge since it's the endpoint reference table)
Every load-bearing claim in the PR body was reproduced empirically: gate counts (494/271) exact, all four enforcement tests proven discriminating against pre-change code, full 4-row behavior matrix + edge cases confirmed against live servers, payload confirmed aggregate and tenant-label-free, /health///ready/JSON-/metrics untouched, both startup-warning variants observed.
Posted by the independent validation context (shared gh identity with the author → comment, not formal approval).
surpradhan
left a comment
There was a problem hiding this comment.
Independent code review — PR #164 (feat(auth): gate /metrics/prometheus behind optional METRICS_TOKEN, head e8f74a1)
Reviewed the full diff against the parent commit; ran the gates and live-probed the behavior matrix and bypass variants against a real server.
Findings
Blockers: none.
Suggestion — SETUP.md still documents the endpoint as unauthenticated (stale doc the PR missed)
SETUP.md:544— the API-reference endpoint table still reads:| GET | /metrics/prometheus | none | Prometheus text format 0.0.4. Unauthenticated — restrict at the network layer if needed. |. This is now factually wrong on both counts (production 503s whenMETRICS_TOKENis unset; token required when set) and contradicts AUTH.md/SECURITY.md §8. Suggested fix: Auth column →METRICS_TOKEN bearer, note → "Open in dev when METRICS_TOKEN is unset; 503 in production until it is set. Scrapers send Authorization: Bearer ."SETUP.md:600— the §Observability paragraph ("Wire this into your existing Prometheus/Grafana stack for production monitoring") mentions no token; anyone following it in production hits a 503 with no pointer. Suggested fix: one sentence referencingMETRICS_TOKEN+ SECURITY.md §8.
Nit — src/openapi.json description omits the new gate
src/openapi.json:6— CLAUDE.md designates this spec the API source of truth. Its description names/metrics/prometheusamong the infra endpoints and explicitly documentsADMIN_TOKENfor admin routes, but says nothing about the newMETRICS_TOKENrequirement. Suggested fix: one sentence in the## Authenticationblock (e.g. "The Prometheus scrape endpointGET /metrics/prometheusis gated by theMETRICS_TOKENenv var — bearer token when set; open in dev / 503 in production when unset."). Adding the path object itself is optional — it was never specced (like/ready), a pre-existing gap.
Observations (fine as-is, no action):
- No CHANGELOG entry — consistent with recent server-change precedent (#156/#163 added none).
extractBearertrims whitespace, soBearer <token>(double space) is accepted — pre-existing shared helper, same for admin auth; benign.- Scope check: server is PARKED, but this closes the auth gap both #156 reviewers flagged — security fix, not feature growth. In-policy.
- Historical CHANGELOG lines (880, 1976) describing the old no-auth endpoint are release history and correctly left untouched.
What I verified
- Gates:
npm run lintclean; unit 494/494; integration 271/271 — matches the PR's claims exactly. - Behavior matrix live-probed (real server, not just the suite): dev+unset → 200; production+unset → 503
{error: "Metrics not configured", hint: …}+ startup warning; set → 401 missing token, 401 wrong token, 200 correct bearer; production+set → 401 (not 503) without token. Both startup-warning variants observed in logs. - Bypass probes (all negative):
?token=andX-API-Keyrejected (bearer-only, as documented); trailing-slash and case variants (/Metrics/Prometheus) still route through the gate → 401;/x/../metrics/prometheusand//metrics/prometheus→ 404;/v1/metrics/prometheus→ 404 with and without a valid token (the v1 router never had this route, andtests/integration/server.test.js:4474already regression-locks the 404). - Timing safety:
safeEqual→crypto.timingSafeEqual; the length-mismatch path does a dummy same-length compare whose duration depends only on attacker input length, never the secret. Comparison order (!provided || !safeEqual(...)) matches the admin middleware. - Empty-token edge:
.env.example'sMETRICS_TOKEN=/ compose's${METRICS_TOKEN:-}yield an empty string → falsy → treated as unset (prod 503); an empty string can never become an accepted credential. Same posture asDASHBOARD_TOKEN. - Consistency: 503 envelope (error+hint) mirrors dashboard/admin unconfigured; 401
{error: "Invalid metrics token"}mirrors admin; header comment renumbered 1–4 correctly; module export added. - Test hygiene: outer before/after save+restore with correct
undefinedguards; each inner describe fully re-specifies both env vars so no inter-describe leakage; node:test runs the suites serially (confirmed via per-request status logs in the run output); env restored before the file-levelafter. The four enforcement tests would demonstrably fail against the pre-change always-open route. - Docs: AUTH.md (table, env vars, security note), SECURITY.md §8 (numbering flows §7→§8→checklist;
authorization.credentialsprometheus.yml syntax is correct — bearer is the default type), OPERATIONS.md checklist §8 cross-reference, QUICK_REFERENCE, docker-compose passthrough — all match the implemented behavior. - Bonus hardening: unauthorized scrapes no longer reach
db.getMetrics(auth precedes the DB call). - Commit format
feat(auth): …with no Co-Authored-By trailer;feature/branch prefix is valid per CONTRIBUTING.
Verdict
Request Changes — docs-only: 2 actionable items (1 Suggestion: stale SETUP.md rows; 1 Nit: openapi.json description). The implementation, tests, and security posture are approve-quality; once SETUP.md and the spec description are aligned this is ready.
(Posted with --comment per repo convention — reviewer shares the author's gh identity.)
Addresses all four actionable findings from the two independent reviews: - SETUP.md endpoint table: /metrics/prometheus auth 'none' -> 'metrics' (METRICS_TOKEN bearer) with the dev-open/production-503 behavior; new [metrics] entry in the auth legend. - SETUP.md Observability section: note that production scraping requires METRICS_TOKEN via the Prometheus authorization scrape config (SECURITY.md section 8 pointer). - SECURITY.md section 2 'Set Strong Tokens': mint METRICS_TOKEN alongside DASHBOARD_TOKEN/ADMIN_TOKEN. - src/openapi.json Authentication description: document the metrics token and its unset behavior.
|
Round-1 findings from both reviews addressed in cb34d93:
Observations acknowledged as no-change-needed per the reviews themselves: no CHANGELOG entry (matches #156/#163 precedent), Gates re-run after the fixes: lint clean, unit 494/494, integration 271/271, |
surpradhan
left a comment
There was a problem hiding this comment.
Independent code review — round 2 (PR #164, head cb34d93)
Re-reviewed the delta e8f74a1..cb34d93 (docs-only: SETUP.md, SECURITY.md, src/openapi.json) against my round-1 findings.
Round-1 findings — all resolved
- Suggestion (SETUP.md:544 stale table row) — fixed: Auth column now
metrics, note reads "Open in dev whenMETRICS_TOKENis unset; 503 in production until it is set. Scrapers sendAuthorization: Bearer <token>." Matches the live-verified behavior exactly, and the new[metrics]legend entry at SETUP.md:534 is consistent with the existing[admin]/[none]style. - Suggestion (SETUP.md:600 Observability) — fixed: production scraping now points at
METRICS_TOKEN+ the Prometheusauthorizationscrape config with a SECURITY.md §8 cross-reference (section exists; relative link valid). - Nit (src/openapi.json description) — fixed: the Authentication section now documents the metrics token with accurate unset semantics ("open in development and returns 503 in production (fail closed)"). Path object intentionally not added, consistent with the pre-existing infra-endpoint convention (
/readywas never specced) — as I noted in round 1, that's fine. - Validator's finding (SECURITY.md §2) — also fixed in this commit:
METRICS_TOKEN=$(openssl rand -hex 32)minted and exported alongside the other two tokens, consistent with §8 and the checklist.
What I verified on cb34d93
src/openapi.jsonparses as valid JSON (31 paths, description updated); theGET /openapi.jsonintegration test that fetches and parses the served spec passes.- No stale text remains:
grepfor "Unauthenticated" / "network layer" in SETUP.md → no hits; all remaining repo references to the old no-auth behavior are historical CHANGELOG entries (correctly untouched). - No new inconsistencies introduced — every new doc claim matches the behavior I live-probed in round 1 (dev-open 200 / prod-unset 503 / 401 wrong-or-missing / 200 bearer).
- Gates re-run on the new head:
npm run lintclean; unit 494/494; integration 271/271. - Commit
cb34d93(docs: fix stale metrics-auth claims from PR #164 review round 1) follows the conventional format with no Co-Authored-By trailer.
Verdict
Approve — zero actionable items. All round-1 findings resolved cleanly; docs, spec, and implementation are now consistent.
(Posted with --comment per repo convention — reviewer shares the author's gh identity; treat this as my approval.)
surpradhan
left a comment
There was a problem hiding this comment.
Round-2 validation report (new head cb34d93)
Re-validated at the new PR head cb34d93 in the same isolated worktree. gh pr view confirms headRefOid = cb34d93…, and gh pr diff 164 is byte-identical to git diff 0e09eb7 cb34d93 locally (no uncommitted work missing from the PR).
Delta scope — docs-only, as claimed
git diff e8f74a1 cb34d93 touches exactly 3 files, +6/−5: SECURITY.md, SETUP.md, src/openapi.json. The openapi change is confined to the info.description string — no runtime code, no paths/security objects changed. On that basis I did not repeat the full live matrix; a spot-probe (below) confirms enforcement is unchanged.
Round-1 findings — all resolved (verified line-by-line)
| Finding | Status |
|---|---|
| (validator #1) SETUP.md:544 stale "Unauthenticated — restrict at the network layer" row | Fixed — auth column now metrics, note states dev-open / production-503 / Authorization: Bearer <token>; legend (line 534) gained [metrics] = METRICS_TOKEN bearer |
| (validator #2) SECURITY.md §2 token snippet missing METRICS_TOKEN | Fixed — METRICS_TOKEN=$(openssl rand -hex 32) added and included in the export line |
| (code reviewer) SETUP.md Observability section | Fixed — now instructs setting METRICS_TOKEN, notes the production 503, points to SECURITY.md §8 |
| (code reviewer) openapi.json Authentication description | Fixed — description now documents the metrics token, bearer header, and unset dev-open/production-503 semantics |
Repo-wide re-grep: no remaining stale "unauthenticated" claims about /metrics/prometheus outside CHANGELOG.md (historical record, correctly untouched).
Gates at cb34d93 — green
npm run lint → exit 0 (clean)
npm test → unit: 494/494 pass, 0 fail · integration: 271/271 pass, 0 fail (exit 0)
openapi.json integrity — parses and serves
json.load('src/openapi.json')→ parses OK; METRICS_TOKEN present ininfo.description.- Live server (
node src/server.js, ephemeral port, temp DATABASE_PATH,NODE_ENV=production METRICS_TOKEN=…):GET /openapi.json→ 200, body re-parsed as valid JSON with the new description. - Spot-probe on the same server confirms runtime behavior unchanged:
/metrics/prometheus→ 401 without token, 200 with correct bearer token.
Findings: zero actionable items.
Verdict: Approve — round-1 findings from both reviews are genuinely resolved, the delta is docs-only, gates are green, and the served OpenAPI document is intact.
Posted by the independent validation context (shared gh identity with the author → comment, not formal approval).
Context
Both reviewers of #156 (fail-closed auth) flagged
GET /metrics/prometheusas the one remaining always-open data-bearing surface in production and asked for an explicit decision: keep it open and document the scrape-network assumption, or add an optional metrics token.Decision: option (b) — dedicated
METRICS_TOKENbearer authRationale: #156 established that a forgotten token must never silently expose a surface in production. Documenting a scrape-network assumption would leave
/metrics/prometheusas the single exception to that posture. A dedicated token matches the existingDASHBOARD_TOKENidiom exactly, and Prometheus supports bearer credentials natively (authorization.credentialsscrape config), so the operational cost is one line of scrape config.Behavior matrix (all four live-probed against a real server)
METRICS_TOKENNODE_ENVproductionMetrics not configured(fail closed) + startup warningAuthorization: Bearer <token>The payload remains aggregate and tenant-label-free either way (
src/metrics.js);/health,/readyuntouched.Changes
src/auth.js— newrequireMetricsAuth(fourth auth layer), mirroringrequireDashboardAuth's unset/dev/production branches andrequireAdminAuth's bearer-only + constant-time comparisonsrc/server.js— middleware wired onto the route; route comment rewritten; startup warning (dev-open vs production-503 variants, mirroring theDASHBOARD_TOKENblock)AUTH.md(overview table, env-vars table, security note),SECURITY.md(new §8 withprometheus.ymlexample),OPERATIONS.md(production checklist),docs/QUICK_REFERENCE.md(replaces the "intentionally unauthenticated" note),.env.example,docker-compose.yml(METRICS_TOKENpassthrough)Tests
7 new integration tests (TDD — all four enforcement tests watched failing with
actual: 200before implementation):not configuredGates:
npm run lintclean · unit 494/494 · integration 271/271.Compatibility note
Production deployments that scrape
/metrics/prometheustoday must setMETRICS_TOKENon upgrade (endpoint 503s until then, loudly, with a hint + startup warning). This is the same intentional breaking posture #156 took for the dashboard, and the server is a run-from-source reference implementation with no release pipeline.🤖 Generated with Claude Code