metrics: rate limit /metrics per client address before Basic Auth (closes #101) #180

Merged
clawbot merged 1 commits from issue-101-metrics-ratelimit into next 2026-10-01 22:09:15 +02:00
Collaborator

Rate limits /metrics per client address, ahead of Basic Auth, for #101.

  • MetricsRateLimit() in internal/middleware uses httprate.LimitBy: 30 requests a minute per client address, every request counted. Over the limit the answer is 429 Too Many Requests with httprate's fixed body, the same whatever the credentials were.
  • The key comes from realIP(), the existing trusted-proxy logic, so a client that is not on a private or loopback address cannot choose its own key with X-Real-IP or X-Forwarded-For.
  • routes.go adds it to the /metrics router from #172, before MetricsAuth().
  • README documents the limit below the environment variable table.

Not visible in the diff: httprate sets X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset on every /metrics response, and Retry-After on a 429.

Disclosures:

  • Judgement call: IPv6 addresses in one /64 share an allowance (httprate's CanonicalizeIP), since a client usually holds a whole /64 and could otherwise rotate through it.
  • Judgement call: 30 a minute; one scraper every 15 seconds uses 4.
  • Not fixed, already there: realIP() takes the first X-Forwarded-For entry, so behind a trusted proxy that appends to a client-sent header and sets no X-Real-IP, the client chooses its key. Needs its own issue.
  • Unchanged: /metrics answers 404 when it is not configured and 401 when it is, so whether it is configured stays visible.

Model: opus-5-5

Rate limits `/metrics` per client address, ahead of Basic Auth, for https://git.eeqj.de/sneak/dnswatcher/issues/101. - `MetricsRateLimit()` in `internal/middleware` uses `httprate.LimitBy`: 30 requests a minute per client address, every request counted. Over the limit the answer is `429 Too Many Requests` with httprate's fixed body, the same whatever the credentials were. - The key comes from `realIP()`, the existing trusted-proxy logic, so a client that is not on a private or loopback address cannot choose its own key with `X-Real-IP` or `X-Forwarded-For`. - `routes.go` adds it to the `/metrics` router from https://git.eeqj.de/sneak/dnswatcher/pulls/172, before `MetricsAuth()`. - README documents the limit below the environment variable table. Not visible in the diff: httprate sets `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` on every `/metrics` response, and `Retry-After` on a 429. Disclosures: - Judgement call: IPv6 addresses in one /64 share an allowance (httprate's `CanonicalizeIP`), since a client usually holds a whole /64 and could otherwise rotate through it. - Judgement call: 30 a minute; one scraper every 15 seconds uses 4. - Not fixed, already there: `realIP()` takes the first `X-Forwarded-For` entry, so behind a trusted proxy that appends to a client-sent header and sets no `X-Real-IP`, the client chooses its key. Needs its own issue. - Unchanged: `/metrics` answers 404 when it is not configured and 401 when it is, so whether it is configured stays visible. Model: opus-5-5
clawbot added the needs-review label 2026-10-01 21:41:12 +02:00
clawbot self-assigned this 2026-10-01 21:41:12 +02:00
Author
Collaborator

Review failed on c6f1943.

  • internal/middleware/middleware.go, the key function in MetricsRateLimit(): when a trusted proxy reports an IPv4 client in IPv6-mapped form (::ffff:203.0.113.1, as some proxies do when one socket takes both IPv4 and IPv6), httprate.CanonicalizeIP cuts it to its /64, which is :: for every such address. All IPv4 clients behind that proxy then share one allowance, so any one of them can lock every other one, the Prometheus scraper included, out of /metrics with 30 requests a minute. Acceptable: an IPv6-mapped IPv4 address is turned back into the plain IPv4 address before the /64 grouping, with a case in TestMetricsRateLimitKeysOnClientAddress where two different clients reported that way by the trusted proxy each get their own allowance.

Model: opus-5-5

Review failed on c6f1943. - `internal/middleware/middleware.go`, the key function in `MetricsRateLimit()`: when a trusted proxy reports an IPv4 client in IPv6-mapped form (`::ffff:203.0.113.1`, as some proxies do when one socket takes both IPv4 and IPv6), `httprate.CanonicalizeIP` cuts it to its /64, which is `::` for every such address. All IPv4 clients behind that proxy then share one allowance, so any one of them can lock every other one, the Prometheus scraper included, out of `/metrics` with 30 requests a minute. Acceptable: an IPv6-mapped IPv4 address is turned back into the plain IPv4 address before the /64 grouping, with a case in `TestMetricsRateLimitKeysOnClientAddress` where two different clients reported that way by the trusted proxy each get their own allowance. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-01 21:53:38 +02:00
clawbot force-pushed issue-101-metrics-ratelimit from c6f1943bb9 to 369f5cea1a 2026-10-01 21:58:10 +02:00 Compare
Author
Collaborator

Rework for #180 (comment), now at 369f5ce:

  • IPv6-mapped IPv4 from a trusted proxy: fixed. The rate limit turns ::ffff:203.0.113.1 back into 203.0.113.1 before the /64 grouping. TestMetricsRateLimitKeysOnClientAddress has a new case where two clients the trusted proxy reports that way each get their own allowance.

Rebased onto current next.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/dnswatcher/pulls/180#issuecomment-108563, now at 369f5ce: - IPv6-mapped IPv4 from a trusted proxy: fixed. The rate limit turns `::ffff:203.0.113.1` back into `203.0.113.1` before the /64 grouping. `TestMetricsRateLimitKeysOnClientAddress` has a new case where two clients the trusted proxy reports that way each get their own allowance. Rebased onto current `next`. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-01 21:58:20 +02:00
Author
Collaborator

Review passed on 369f5ce.

Model: opus-5-5

Review passed on 369f5ce. Model: opus-5-5
clawbot added 1 commit 2026-10-01 22:08:59 +02:00
/metrics is behind a password, and REPO_POLICIES.md requires rate
limiting on password logins. Each client address may now send it 30
requests a minute, counted by httprate before Basic Auth, so failed
logins use up the allowance and a request over it gets 429 without
the password being checked. The address is the one the existing
trusted-proxy logic in internal/middleware works out, with IPv6
addresses grouped by /64; an IPv4 address a proxy reports in
IPv6-mapped form counts as the plain IPv4 address. A Prometheus
server scraping every 15 seconds sends 4 requests a minute.

Model: opus-5-5
clawbot force-pushed issue-101-metrics-ratelimit from 369f5cea1a to 879d41eb2b 2026-10-01 22:08:59 +02:00 Compare
clawbot merged commit ed0f56f144 into next 2026-10-01 22:09:15 +02:00
clawbot deleted branch issue-101-metrics-ratelimit 2026-10-01 22:09:15 +02:00
clawbot removed the needs-review label 2026-10-01 22:09:15 +02:00
Sign in to join this conversation.