feat(rate-limiting): add per-bucket limits and startup validation

- Add per-bucket rate limit config (ban, unban, import, config, jail, filter, action)
- Add process-local warning at startup for multi-worker deployments
- Document Redis migration path for shared state across workers
- Remove Issue #42 from Tasks.md (resolved)
This commit is contained in:
2026-05-03 20:53:21 +02:00
parent c3cd1574dc
commit 1c3dff31e8
5 changed files with 82 additions and 90 deletions

View File

@@ -113,6 +113,56 @@ If fail2ban goes offline but the backend always returns 200, Docker treats the c
---
## Rate Limiting
Rate limiting is enforced at two levels:
1. **Global middleware** — Per-IP request rate limit across all endpoints (default: 200 requests/minute per IP)
2. **Per-bucket limits** — Stricter limits on specific operations:
| Bucket | Limit | Window | Purpose |
|--------|-------|--------|---------|
| `bans:ban` | 100/min | 60s | Ban operations |
| `bans:unban` | 100/min | 60s | Unban operations |
| `blocklist:import` | 10/hour | 3600s | Import operations |
| `config:update` | 50/min | 60s | Config write operations |
| `jail:*` | 100/min | 60s | Jail management |
| `filter:*` | 50/min | 60s | Filter management |
| `action:*` | 50/min | 60s | Action management |
### Process-Local Scope
**Current implementation is process-local.** Each worker maintains independent in-memory counters. In a multi-worker deployment (N workers), an attacker can send up to N × limit requests before any single worker triggers a block — effectively multiplying the allowed request rate by the number of workers.
**Short-term mitigation:** The scheduler lock enforces single-worker mode. The startup warning log (`rate_limiting_process_local_only`) documents this constraint. Deploy with one worker.
**Long-term solution:** Replace the in-process GlobalRateLimiter with a Redis-backed adapter. The `check_allowed()` and `check_allowed_for_bucket()` interfaces are designed for a drop-in replacement using atomic `INCR` + `EXPIRE` semantics — no changes needed in middleware or router code.
### Redis Migration (Future)
When migrating to Redis, replace the in-memory deque store with:
```python
# Atomic increment with expiry (pseudo-code)
count = redis.incr(f"rl:{ip}")
if count == 1: # First request, set expiry
redis.expire(f"rl:{ip}", window_seconds)
if count > max_requests:
return False, window_seconds - redis.ttl(f"rl:{ip}")
return True, 0
```
The bucket variants use `INCR` + `EXPIRE` on `rl:{bucket}:{ip}` keys. This preserves the sliding-window semantics while providing shared state across all workers.
### Monitoring
Check logs for these events:
- `global_rate_limit_exceeded` — Global middleware blocked a request (WARNING)
- `rate_limiting_process_local_only` — Startup warning about multi-worker limitation (WARNING)
- `rate_limiter_cleanup` — Periodic cleanup of expired entries (DEBUG)
---
## CORS Configuration
Cross-Origin Resource Sharing (CORS) must be explicitly configured when the frontend and backend are served from different origins.