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:
@@ -8,8 +8,18 @@ Rate limits can be customized per endpoint or use a global default.
|
||||
IP addresses are extracted using the same trusted-proxy-aware logic as
|
||||
authentication to ensure consistent behavior across all rate limiting.
|
||||
|
||||
Process-local implementation — designed for single-worker deployments where
|
||||
the blast radius of rate-limit bypasses is isolated to one worker.
|
||||
**Process-local implementation** — Each worker process maintains its own
|
||||
independent counter store. In multi-worker deployments (N workers), an
|
||||
attacker can send up to N × limit requests before any single worker triggers
|
||||
the limit. This is a fundamental limitation of in-process stores.
|
||||
|
||||
**Short-term mitigation:** Deploy with a single worker (enforced by the
|
||||
scheduler lock). The startup warning log documents this constraint.
|
||||
|
||||
**Long-term solution:** Replace the in-process GlobalRateLimiter with a
|
||||
Redis-backed adapter that uses atomic INCR + EXPIRE semantics. The
|
||||
check_allowed() and check_allowed_for_bucket() interfaces are designed
|
||||
to make this swap-in without touching middleware or router code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -302,6 +302,16 @@ async def _stage_check_worker_mode_and_acquire_lock(startup_db: Any) -> None:
|
||||
"See Docs/Architekture.md § Deployment Constraints for details."
|
||||
)
|
||||
|
||||
log.warning(
|
||||
"rate_limiting_process_local_only",
|
||||
message=(
|
||||
"Rate limiting is process-local. With multiple workers, each worker enforces "
|
||||
"its own independent limit — an attacker can send N × limit requests before "
|
||||
"any worker triggers a block. Deploy with a single worker, or replace the "
|
||||
"in-process store with a shared backend (e.g., Redis) for multi-worker setups."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def _stage_init_database(app: FastAPI, settings: Settings) -> Any:
|
||||
"""Initialize database schema and load setup state.
|
||||
|
||||
@@ -219,9 +219,16 @@ class GlobalRateLimiter:
|
||||
request counting: when an IP exceeds the limit, the next request is blocked
|
||||
until the oldest request in the window expires.
|
||||
|
||||
Process-local implementation — each worker maintains independent counters.
|
||||
Designed for single-worker deployments where the blast radius is isolated
|
||||
to one worker.
|
||||
**Process-local implementation** — Each worker maintains independent counters.
|
||||
In multi-worker deployments (N workers), an attacker can send up to N × limit
|
||||
requests before any single worker triggers a block. The single-worker scheduler
|
||||
lock provides partial protection, but deployments requiring horizontal scaling
|
||||
should replace this with a Redis-backed store using atomic INCR + EXPIRE.
|
||||
|
||||
**Long-term migration path:** The check_allowed() and check_allowed_for_bucket()
|
||||
interfaces map directly to Redis INCR + EXPIRE. A drop-in RedisRateLimiter
|
||||
adapter would only need to replace the deque-based in-memory store with Redis
|
||||
calls, without touching any caller code.
|
||||
|
||||
**How It Works:**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user