Skip to content

Commit 6ef4edc

Browse files
MohanLakshjonpspri
andauthored
feat(middleware): add Redis-backed rate limiting with tier-based limits (#4423)
* feat(middleware): add Redis-backed rate limiting with tier-based limits - Add RateLimitMiddleware with Redis-backed sliding window algorithm - Implement tier-based rate limits (CRITICAL/HIGH/MEDIUM/LOW) per endpoint - Add lockout mechanism after excessive violations (5 violations = 15 min lockout) - Add multi-dimensional limiting (IP → User → Team) - Include security event logging for audit trail - Add 107 unit tests with 95% coverage Implements X-Force Red security findings for API rate limiting. Closes #4168 Signed-off-by: Mohan Lakshmaiah <mohan.economist@gmail.com> * chore: fix end of file newline Signed-off-by: Mohan Lakshmaiah <mohan.economist@gmail.com> * test: disable rate limiting in test suite Rate limiting middleware was causing 38 test failures with 429 responses. Tests make rapid sequential requests that exceed rate limits. Follow existing pattern in conftest.py to disable optional middleware during tests (admin API, UI, llmchat are similarly disabled). Tests specifically for rate limiting (test_rate_limit_middleware.py) explicitly enable the middleware via settings override. Fixes test suite CI failures after rate limiting feature merge. Signed-off-by: Mohan Lakshmaiah <mohan.economist@gmail.com> * fix(middleware): rate limiting council review fixes - Prevent infinite lockout loop by skipping violation increment during lockout - Add timestamp-based expiry to memory violation counts - Harden IP extraction against proxy spoofing via request.scope - Use atomic Lua script for Redis sliding window check+add - Reuse pre-check results to avoid double-counting on success - Update tests to properly exercise Redis Lua script path - Add regression tests for lockout expiry and no-increment behavior Signed-off-by: Jonathan Springer <jps@s390x.com> * chore: update .secrets.baseline timestamp Signed-off-by: Jonathan Springer <jps@s390x.com> * test(middleware): improve rate limiting diff coverage to 100% Add tests for previously uncovered code paths: - Redis init success and exception handling - User object email extraction from request.state.user - Redis Lua script blocked (0) response - Lockout async executor exception fallback - Redis lockout count below threshold - Memory violation expiry cleanup and initialization - Violation increment async exception fallback - Redis violation increment exception fallback - Main app middleware registration when rate limiting enabled Signed-off-by: Jonathan Springer <jps@s390x.com> --------- Signed-off-by: Mohan Lakshmaiah <mohan.economist@gmail.com> Signed-off-by: Jonathan Springer <jps@s390x.com> Co-authored-by: Jonathan Springer <jps@s390x.com>
1 parent 7a19c94 commit 6ef4edc

7 files changed

Lines changed: 1868 additions & 5 deletions

File tree

.secrets.baseline

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "(?x)( package-lock\\.json$ |Cargo\\.lock$ |uv\\.lock$ |go\\.sum$ |mcpgateway/sri_hashes\\.json$ )|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2026-05-09T22:45:24Z",
6+
"generated_at": "2026-05-10T06:09:40Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -6098,15 +6098,15 @@
60986098
"hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
60996099
"is_secret": false,
61006100
"is_verified": false,
6101-
"line_number": 153,
6101+
"line_number": 154,
61026102
"type": "Basic Auth Credentials",
61036103
"verified_result": null
61046104
},
61056105
{
61066106
"hashed_secret": "206c80413b9a96c1312cc346b7d2517b84463edd",
61076107
"is_secret": false,
61086108
"is_verified": false,
6109-
"line_number": 202,
6109+
"line_number": 203,
61106110
"type": "Secret Keyword",
61116111
"verified_result": null
61126112
}

mcpgateway/config.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3115,8 +3115,31 @@ def validate_database(self) -> None:
31153115
"application/octet-stream",
31163116
]
31173117

3118-
# Rate limiting
3119-
validation_max_requests_per_minute: int = 60
3118+
# Rate limiting - Redis-backed sliding window
3119+
rate_limiting_enabled: bool = Field(default=True, description="Enable Redis-backed rate limiting middleware")
3120+
rate_limiting_redis_enabled: bool = Field(default=True, description="Use Redis for rate limiting (fallback to in-memory if unavailable)")
3121+
3122+
# Backward compatibility (used by tests)
3123+
validation_max_requests_per_minute: int = Field(default=60, description="Backward compatibility for tests")
3124+
3125+
# Tier-based rate limits (requests per minute)
3126+
# CRITICAL: Auth endpoints (login, register, password reset)
3127+
rate_limit_critical_rpm: int = Field(default=10, description="CRITICAL tier: Auth endpoints")
3128+
rate_limit_critical_burst: int = Field(default=0, description="CRITICAL tier: No burst allowance")
3129+
# HIGH: Token management, admin, OAuth
3130+
rate_limit_high_rpm: int = Field(default=30, description="HIGH tier: Token/admin endpoints")
3131+
rate_limit_high_burst: int = Field(default=0, description="HIGH tier: No burst allowance")
3132+
# MEDIUM: MCP, tools, LLM chat (reuse tool_rate_limit)
3133+
rate_limit_medium_rpm: int = Field(default=100, description="MEDIUM tier: MCP/tools (previously tool_rate_limit)")
3134+
rate_limit_medium_burst: int = Field(default=20, description="MEDIUM tier: Burst allowance for API clients")
3135+
# LOW: Health checks, metrics, static content
3136+
rate_limit_low_rpm: int = Field(default=500, description="LOW tier: Health/metrics")
3137+
rate_limit_low_burst: int = Field(default=100, description="LOW tier: Burst allowance")
3138+
3139+
# Lockout configuration
3140+
rate_limit_lockout_enabled: bool = Field(default=True, description="Enable temporary lockout after excessive violations")
3141+
rate_limit_lockout_threshold: int = Field(default=5, description="Violations before account lockout")
3142+
rate_limit_lockout_duration_minutes: int = Field(default=15, description="Lockout duration in minutes")
31203143

31213144
# Header passthrough feature (disabled by default for security)
31223145
enable_header_passthrough: bool = Field(default=False, description="Enable HTTP header passthrough feature (WARNING: Security implications - only enable if needed)")

mcpgateway/main.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
from mcpgateway.middleware.correlation_id import CorrelationIDMiddleware
104104
from mcpgateway.middleware.http_auth_middleware import HttpAuthMiddleware, run_pre_request_hooks
105105
from mcpgateway.middleware.protocol_version import MCPProtocolVersionMiddleware
106+
from mcpgateway.middleware.rate_limit_middleware import RateLimitMiddleware
106107
from mcpgateway.middleware.rbac import _ACCESS_DENIED_MSG, get_current_user_with_permissions, PermissionChecker, require_permission
107108
from mcpgateway.middleware.request_logging_middleware import RequestLoggingMiddleware
108109
from mcpgateway.middleware.security_headers import SecurityHeadersMiddleware
@@ -2975,6 +2976,17 @@ async def _call_streamable_http(self, scope, receive, send):
29752976
# Add security headers middleware
29762977
app.add_middleware(SecurityHeadersMiddleware)
29772978

2979+
# Add rate limiting middleware (after HttpAuthMiddleware for user-aware limiting)
2980+
if settings.rate_limiting_enabled:
2981+
app.add_middleware(RateLimitMiddleware)
2982+
logger.info(
2983+
f"🚦 Rate limiting enabled: Redis={settings.rate_limiting_redis_enabled}, "
2984+
f"Tiers[CRITICAL={settings.rate_limit_critical_rpm}, "
2985+
f"HIGH={settings.rate_limit_high_rpm}, "
2986+
f"MEDIUM={settings.rate_limit_medium_rpm}, "
2987+
f"LOW={settings.rate_limit_low_rpm}]"
2988+
)
2989+
29782990
# Add validation middleware if explicitly enabled
29792991
if settings.validation_middleware_enabled:
29802992
app.add_middleware(ValidationMiddleware)

0 commit comments

Comments
 (0)