Feat: Rate-limit login endpoint to block brute-force attacks
After LOGIN_MAX_FAILURES consecutive failed attempts from the same source IP within LOGIN_WINDOW_SECONDS, POST /api/v1/auth/token returns HTTP 429 with a Retry-After header for LOGIN_COOLDOWN_SECONDS. A successful login resets the counter. Trusted upstream proxy IPs/CIDRs can be declared via LOGIN_TRUSTED_PROXY_IPS so X-Forwarded-For is honoured correctly behind nginx ingress or similar reverse proxies. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
53
specs/009-login-rate-limiting/data-model.md
Normal file
53
specs/009-login-rate-limiting/data-model.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Data Model: Login Brute-Force Protection
|
||||
|
||||
## Overview
|
||||
|
||||
This feature introduces no new database tables. The only data entity is a transient,
|
||||
in-memory rate-limit record that does not survive process restarts. This is intentional
|
||||
(see research.md Decision 3).
|
||||
|
||||
---
|
||||
|
||||
## Entity: Rate-Limit Record (in-memory only)
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------------|---------|-----------------------------------------------------------------------------|
|
||||
| `failures` | int | Count of consecutive failed login attempts in the current window |
|
||||
| `window_start` | float | Unix timestamp marking when the current counting window began |
|
||||
| `blocked_until`| float | Unix timestamp after which the source is no longer blocked; 0.0 if not blocked |
|
||||
|
||||
**Keyed by**: resolved client IP address string (e.g., `"192.168.1.1"`); see `get_client_ip()` in `rate_limiter.py` for resolution logic
|
||||
|
||||
**Lifecycle**:
|
||||
1. Record is created on the first failed login from a source.
|
||||
2. `failures` increments on each subsequent failure within the window.
|
||||
3. When `failures >= LOGIN_MAX_FAILURES`, `blocked_until` is set to `now + LOGIN_COOLDOWN_SECONDS`.
|
||||
4. When `blocked_until` has passed, the record is deleted on the next request from that source.
|
||||
5. A successful login deletes the record immediately (failure counter reset).
|
||||
6. If `now - window_start > LOGIN_WINDOW_SECONDS` without triggering lockout, the counter resets within the existing record.
|
||||
|
||||
**State machine**:
|
||||
|
||||
```
|
||||
[no record]
|
||||
│ first failure
|
||||
▼
|
||||
[tracking] ──── failure N ≥ max ────► [blocked]
|
||||
│ │
|
||||
│ success / window expires │ cooldown expires
|
||||
▼ ▼
|
||||
[no record] ◄─────────────────────── [no record]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Entity: Rate-Limit Settings
|
||||
|
||||
Stored as environment variables; loaded via `app.config.Settings`:
|
||||
|
||||
| Env Var | Default | Description |
|
||||
|----------------------------|---------|----------------------------------------------------------|
|
||||
| `LOGIN_MAX_FAILURES` | `5` | Failures within window before lockout |
|
||||
| `LOGIN_WINDOW_SECONDS` | `300` | Rolling window duration in seconds (5 minutes) |
|
||||
| `LOGIN_COOLDOWN_SECONDS` | `900` | Lockout duration in seconds after threshold exceeded (15 minutes) |
|
||||
| `LOGIN_TRUSTED_PROXY_IPS` | `""` | Comma-separated IPs/CIDRs of trusted upstream proxies (e.g., `10.0.0.0/8`); empty = disabled |
|
||||
Reference in New Issue
Block a user