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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user