feat(auth): rate-limit dashboard sign-in against brute-force attempts - #437
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (6)
WalkthroughChangesThe dashboard session endpoint now applies configurable per-IP throttling to failed master-key attempts. Application wiring, settings display, documentation, and tests cover enabled, successful-login, isolated-IP, and disabled-limit behavior. Dashboard login throttling
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/unit/test_dashboard_session.py (1)
247-257: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winCover isolation between client IPs.
All requests use one
TestClient, so these assertions do not prove that separate IPs receive separate buckets. Add a test using two distinct client addresses; otherwise a regression to a global limiter could pass unnoticed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_dashboard_session.py` around lines 247 - 257, Extend test_repeated_failed_sign_ins_get_throttled to issue failed sign-in requests from two distinct client IP addresses, verifying each address has its own rate-limit bucket. Preserve the existing per-IP throttling assertions while ensuring a global limiter regression would fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/gateway/api/routes/auth_session.py`:
- Around line 86-88: Update the authentication flow around is_valid_master_key
to run a non-consuming rate-limit admission check before credential
verification, rejecting over-limit requests before the database/hash lookup.
After verification fails, record the failed attempt and retain the existing
rate-limit handling; successful sign-ins must not consume rate-limit quota.
---
Nitpick comments:
In `@tests/unit/test_dashboard_session.py`:
- Around line 247-257: Extend test_repeated_failed_sign_ins_get_throttled to
issue failed sign-in requests from two distinct client IP addresses, verifying
each address has its own rate-limit bucket. Preserve the existing per-IP
throttling assertions while ensuring a global limiter regression would fail.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: b181941f-5bb5-4780-8bfd-5de05ab8e490
📒 Files selected for processing (5)
src/gateway/api/routes/auth_session.pysrc/gateway/api/routes/settings.pysrc/gateway/core/config.pysrc/gateway/main.pytests/unit/test_dashboard_session.py
CodeRabbit's review on mozilla-ai#437 suggested gating the DB/hash lookup in is_valid_master_key behind a non-consuming rate-limit check before verification, to stop an already-throttled IP from forcing that work on every request. Tried it, and a test caught a real regression: a pre-verification gate can't know whether a given attempt would have succeeded, so once an IP hits its failure quota, it also blocks that IP's legitimate owner from signing in with the *correct* key, exactly what the issue explicitly said must never happen. Reverted to checking only after a failed verification (functionally unchanged from the original commit), and documented why: the DB/hash lookup this exposes to repeated attempts only runs on the auto-generated bootstrap-key path; with a configured master_key, verification is a constant-time string compare, not a DB round trip.
Two things from mozilla-ai#437: - The docstring added in 5cffd02 changed create_session's OpenAPI description, which wasn't regenerated at the time, failing the openapi-spec CI check. Regenerated both artifacts. - CodeRabbit nitpick: all prior rate-limit tests used one TestClient, so none of them actually proved isolation between client IPs, only that throttling happens at all. A regression to a single global bucket would have passed unnoticed. Added a test using two TestClients on the same app with distinct client addresses, confirming one IP's throttling doesn't affect the other.
|
also fixed the openapi-spec CI failure (forgot to regenerate after adding docstrings in the last commit) and addressed the isolation nitpick (there wasn't a discrete thread for it, so replying here): added test_rate_limit_buckets_are_isolated_per_client_ip using two TestClients with distinct client addresses on the same app, confirms one IP's throttling doesn't bleed into another's bucket. both in f9b5c79 |
Adds a per-IP throttle to POST /v1/auth/session, which had no rate limiting despite being a purpose-built credential-checking endpoint (each attempt touches the DB for a hash lookup). Not a new exposure - header auth on every management endpoint was already unthrottled - but this endpoint deserves its own purpose-built limiter. Reuses the existing RateLimiter (sliding window, 429 + Retry-After) keyed by client IP instead of user_id, as a separate instance from the general rate_limit_rpm (which is keyed to authenticated users and never sees this pre-auth path). Only counts failed attempts, so a correct master key is never throttled. Client IP comes from request.client.host, not a hand-parsed X-Forwarded-For: uvicorn's ProxyHeadersMiddleware already rewrites it from that header, but only when the immediate peer is trusted (loopback by default), the same trust boundary request_is_https already relies on for X-Forwarded-Proto. Parsing the header directly in application code would let anyone bypass the throttle by sending their own X-Forwarded-For. New config: dashboard_login_rate_limit_per_minute (default 10, None disables). Surfaced read-only in the settings dashboard alongside rate_limit_rpm, not hot-settable, matching that field's own precedent.
CodeRabbit's review on mozilla-ai#437 suggested gating the DB/hash lookup in is_valid_master_key behind a non-consuming rate-limit check before verification, to stop an already-throttled IP from forcing that work on every request. Tried it, and a test caught a real regression: a pre-verification gate can't know whether a given attempt would have succeeded, so once an IP hits its failure quota, it also blocks that IP's legitimate owner from signing in with the *correct* key, exactly what the issue explicitly said must never happen. Reverted to checking only after a failed verification (functionally unchanged from the original commit), and documented why: the DB/hash lookup this exposes to repeated attempts only runs on the auto-generated bootstrap-key path; with a configured master_key, verification is a constant-time string compare, not a DB round trip.
Two things from mozilla-ai#437: - The docstring added in 5cffd02 changed create_session's OpenAPI description, which wasn't regenerated at the time, failing the openapi-spec CI check. Regenerated both artifacts. - CodeRabbit nitpick: all prior rate-limit tests used one TestClient, so none of them actually proved isolation between client IPs, only that throttling happens at all. A regression to a single global bucket would have passed unnoticed. Added a test using two TestClients on the same app with distinct client addresses, confirming one IP's throttling doesn't affect the other.
f9b5c79 to
3c2a467
Compare
|
This looks good! Thanks for the contribution @champ18ion 🎉 |
Description
Adds a per-IP throttle to
POST /v1/auth/session, which had no rate limiting despite being a purpose-built credential-checking endpoint (each attempt touches the DB for a hash lookup). Not a new exposure, header auth on every management endpoint was already unthrottled, but this endpoint deserves its own purpose-built limiter.Reuses the existing
RateLimiter(sliding window, 429 +Retry-After) keyed by client IP instead of user_id, as a separate instance from the generalrate_limit_rpm(which is keyed to authenticated users and never sees this pre-auth path). Only counts failed attempts, so a correct master key is never throttled.Client IP comes from
request.client.host, not a hand-parsedX-Forwarded-For: uvicorn'sProxyHeadersMiddlewarealready rewrites it from that header, but only when the immediate peer is trusted (loopback by default), the same trust boundaryrequest_is_httpsalready relies on forX-Forwarded-Proto. Parsing the header directly in application code would let anyone bypass the throttle by sending their ownX-Forwarded-For.New config:
dashboard_login_rate_limit_per_minute(default 10,Nonedisables). Surfaced read-only in the settings dashboard alongsiderate_limit_rpm, not hot-settable, matching that field's own precedent.PR Type
Relevant issues
Fixes #390
Checklist
tests/unit).make lint,make typecheck,make test) — note: ran via a manual pip venv, notuv, since Windows is excluded from your lockfile's supported platforms; integration tests (tests/integration) couldn't run locally without Docker, but this change is fully covered bytests/unit/test_dashboard_session.py(TestClient + SQLite), which passes.AI Usage
AI Model/Tool used: Claude Code (Sonnet 5)
Any additional AI details you'd like to share: Approach followed the plan you laid out in the issue; code written by Claude Code under my direction and review.
Summary
Added an optional, per-IP “failed login” rate limit for
POST /v1/auth/sessionto help slow down brute-force attempts against the dashboard sign-in flow.dashboard_login_rate_limit_per_minute(default 10; set to None to disable).429with aRetry-Afterheader and logs the throttled source IP.rate_limit_rpm.Technical notes
request.client.host(from trusted proxy handling) as the per-IP key.login_rate_limiter.