From 591fdee72b420149c89bc5894e1bfd6fc601b33d Mon Sep 17 00:00:00 2001 From: whit3rabbit Date: Fri, 27 Mar 2026 15:30:12 -0500 Subject: [PATCH] security: configurable rate limit fail policy via RATE_LIMIT_FAIL_POLICY Add "deny" as alias for "closed" policy. Change fail-closed retry-after from 1s to 60s to give Redis time to recover. Add from_env default test. Document env var in CLAUDE.md. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 1 + crates/proxy/src/ratelimit.rs | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d0cba58..7f14577 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,6 +91,7 @@ OPENAI_API_KEY=sk-... cargo run -p anyllm_proxy - `IP_ALLOWLIST`: Comma-separated CIDR ranges for IP allowlisting (e.g., `192.168.1.0/24,10.0.0.0/8`). Bare IPs also accepted. When set, only matching IPs can access the proxy. - `TRUST_PROXY_HEADERS`: Set to `true` or `1` to use `X-Forwarded-For` header for client IP when behind a reverse proxy. Only effective when `IP_ALLOWLIST` is set. - `WEBHOOK_URLS`: Comma-separated webhook URLs for request completion notifications. Fire-and-forget HTTP POST with `RequestLogEntry` JSON payload. +- `RATE_LIMIT_FAIL_POLICY`: Behavior when Redis rate limiter is unavailable: `open` (default, allow requests) or `closed`/`deny` (reject with 503 and retry-after 60s). ### LiteLLM env var aliases diff --git a/crates/proxy/src/ratelimit.rs b/crates/proxy/src/ratelimit.rs index fb5234a..4556a2b 100644 --- a/crates/proxy/src/ratelimit.rs +++ b/crates/proxy/src/ratelimit.rs @@ -17,7 +17,7 @@ pub enum RateLimitFailPolicy { impl RateLimitFailPolicy { pub fn from_env_str(s: &str) -> Self { match s.to_lowercase().as_str() { - "closed" => Self::Closed, + "closed" | "deny" => Self::Closed, _ => Self::Open, } } @@ -97,7 +97,7 @@ impl RedisRateLimiter { } RateLimitFailPolicy::Closed => { tracing::error!(error = %e, "Redis RPM check failed, rejecting request (fail-closed)"); - Err(1) + Err(60) } }, } @@ -164,7 +164,7 @@ impl RedisRateLimiter { } RateLimitFailPolicy::Closed => { tracing::error!(error = %e, "Redis TPM check failed, rejecting request (fail-closed)"); - Err(1) + Err(60) } }, } @@ -265,9 +265,25 @@ mod tests { RateLimitFailPolicy::from_env_str("CLOSED"), RateLimitFailPolicy::Closed )); + assert!(matches!( + RateLimitFailPolicy::from_env_str("deny"), + RateLimitFailPolicy::Closed + )); + assert!(matches!( + RateLimitFailPolicy::from_env_str("DENY"), + RateLimitFailPolicy::Closed + )); assert!(matches!( RateLimitFailPolicy::from_env_str("unknown"), RateLimitFailPolicy::Open )); } + + #[test] + fn fail_policy_defaults_to_open() { + // When RATE_LIMIT_FAIL_POLICY is unset, from_env should return Open. + std::env::remove_var("RATE_LIMIT_FAIL_POLICY"); + let policy = RateLimitFailPolicy::from_env(); + assert_eq!(policy, RateLimitFailPolicy::Open); + } }