Rate limit password attempts on /metrics (closes #104) #109

Merged
clawbot merged 1 commits from issue-104-metrics-rate-limit into next 2026-10-04 06:19:07 +02:00
Collaborator

Closes #104.

/metrics now goes through the same httprate middleware as POST /api/v1/reports, with its own allowance of 60 requests a minute per client address, a constant in backend/internal/server/routes.go. The limit runs before the basic auth, so past it the answer is 429 and the password is not checked; under it nothing changes. backend/README.md says so.

How the client address reaches the backend, unchanged by this PR:

  1. nginx takes the client address from X-Forwarded-For only on a request from one of the container's TRUSTED_PROXIES (the set_real_ip_from lines bin/entrypoint.sh writes); otherwise it is the connecting address.
  2. nginx passes that address on as the only X-Forwarded-For entry. The proxy_set_header lines sit at server level in nginx.conf, so location = /metrics gets them too.
  3. The backend runs with TRUSTED_PROXIES=127.0.0.1/32 and takes X-Forwarded-For only from a peer in that set. The rate limit counts the address its existing clientIP resolves, as for reports, so clients behind nginx are limited one by one, not together as nginx's loopback address.

The new test uses up one client's allowance on wrong passwords, gets 429 with the right one, and checks that another client behind the same nginx still gets 200.

  • Judgement call: 60 a minute. The minute slides, so a scraper polling every 2 seconds or less often is never refused; a guesser gets about 60 tries a minute per address.
  • The limit counts every request to /metrics, right password or not, as the issue asks.
  • httprate was already a dependency, so go.mod does not change.

Model: opus-5-5

Closes https://git.eeqj.de/sneak/netwatch/issues/104. `/metrics` now goes through the same `httprate` middleware as `POST /api/v1/reports`, with its own allowance of 60 requests a minute per client address, a constant in `backend/internal/server/routes.go`. The limit runs before the basic auth, so past it the answer is 429 and the password is not checked; under it nothing changes. `backend/README.md` says so. How the client address reaches the backend, unchanged by this PR: 1. nginx takes the client address from `X-Forwarded-For` only on a request from one of the container's `TRUSTED_PROXIES` (the `set_real_ip_from` lines `bin/entrypoint.sh` writes); otherwise it is the connecting address. 2. nginx passes that address on as the only `X-Forwarded-For` entry. The `proxy_set_header` lines sit at server level in `nginx.conf`, so `location = /metrics` gets them too. 3. The backend runs with `TRUSTED_PROXIES=127.0.0.1/32` and takes `X-Forwarded-For` only from a peer in that set. The rate limit counts the address its existing `clientIP` resolves, as for reports, so clients behind nginx are limited one by one, not together as nginx's loopback address. The new test uses up one client's allowance on wrong passwords, gets 429 with the right one, and checks that another client behind the same nginx still gets 200. - Judgement call: 60 a minute. The minute slides, so a scraper polling every 2 seconds or less often is never refused; a guesser gets about 60 tries a minute per address. - The limit counts every request to `/metrics`, right password or not, as the issue asks. - `httprate` was already a dependency, so `go.mod` does not change. Model: opus-5-5
clawbot added the needs-review label 2026-10-04 06:01:58 +02:00
clawbot self-assigned this 2026-10-04 06:01:58 +02:00
clawbot added 1 commit 2026-10-04 06:01:59 +02:00
Each client address may make 60 requests to /metrics a minute,
through the same httprate middleware and TRUSTED_PROXIES
resolution the report route uses, with an allowance of its own.
The limit runs before the basic auth, so past it the answer is 429
and the password is not checked. backend/README.md says so; a test
uses up one client's allowance on wrong passwords, gets 429 with
the right one, and checks that another client behind the same
nginx still gets in.

Model: opus-5-5
Author
Collaborator

PASS: /metrics is rate limited per client address with httprate ahead of the basic auth, 60 a minute as a plain constant, so past the limit the answer is 429 and the password is not checked; the address counted is the one nginx resolves through TRUSTED_PROXIES, which a client outside them cannot forge; the test, backend/README.md and TODO.md cover it as #104 asks.

Judgement call: the limit counts each IPv6 address on its own, as the report limit already does, so a client holding a block of IPv6 addresses gets 60 guesses a minute per address; left for a follow-up, outside this issue.
Judgement call: not a finding that the root README.md operator section does not mention the /metrics limit; with TRUSTED_PROXIES unset behind a proxy, every client shares one /metrics allowance, so one guesser can keep the scraper refused.

Model: opus-5-5

PASS: `/metrics` is rate limited per client address with `httprate` ahead of the basic auth, 60 a minute as a plain constant, so past the limit the answer is 429 and the password is not checked; the address counted is the one nginx resolves through `TRUSTED_PROXIES`, which a client outside them cannot forge; the test, `backend/README.md` and `TODO.md` cover it as https://git.eeqj.de/sneak/netwatch/issues/104 asks. Judgement call: the limit counts each IPv6 address on its own, as the report limit already does, so a client holding a block of IPv6 addresses gets 60 guesses a minute per address; left for a follow-up, outside this issue. Judgement call: not a finding that the root `README.md` operator section does not mention the `/metrics` limit; with `TRUSTED_PROXIES` unset behind a proxy, every client shares one `/metrics` allowance, so one guesser can keep the scraper refused. Model: opus-5-5
clawbot added needs-checks and removed needs-review labels 2026-10-04 06:15:24 +02:00
clawbot merged commit d40e67d4ab into next 2026-10-04 06:19:07 +02:00
clawbot deleted branch issue-104-metrics-rate-limit 2026-10-04 06:19:08 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/netwatch#109