Rate Limiting and Failure Policies¶
Security controls must survive real-world pressure. Rate limiting prevents a single caller from exhausting shared resources, and explicit failure policies decide whether the system protects availability by degrading gracefully or by refusing to operate without a trusted signal.
Security objective¶
Bound resource consumption per client and IP, and make the fail-open or fail-closed policy explicit and observable so operators can choose availability or safety under load and during downstream outages.
Core concepts¶
| Term | Meaning |
|---|---|
| Token bucket | A caller can burst up to a capacity, then refills at a steady rate. |
| Client isolation | Each client or IP has its own bucket so one abuser does not block legitimate traffic. |
| Retry-After | A 429 response tells a client when to reattempt, reducing retry storms. |
| Fail closed | A missing or failed security signal causes the request to be denied. Favours safety over availability. |
| Fail open | A missing or failed security signal causes a degraded but allowed path. Favours availability over strict enforcement. |
| Policy as code | The failure mode is a configuration choice, not an accidental side effect of an exception handler. |
Trust boundaries¶
- The rate limiter runs before the expensive work, including authentication if the caller is unauthenticated, to reject abuse cheaply.
- Client identity for rate limiting is derived from
X-Forwarded-Forfirst, thenRemoteAddr, because a reverse proxy is the trust boundary for IP. - Fail-open and fail-closed paths are explicit endpoints so the policy is not hidden in a catch block.
- Security-related decisions continue to fail closed; only non-critical reads may be allowed to fail open.
Spring Security mapping¶
| Concept | Spring Security / Spring API |
|---|---|
| Per-client rate limit | OncePerRequestFilter with an in-memory RateLimitingService bucket per key |
| IP extraction | HttpServletRequest.getHeader("X-Forwarded-For") and getRemoteAddr() |
| 429 with retry | HttpServletResponse.setStatus(429) and Retry-After header |
| Failure policy | DependencyOutageSimulator plus explicit fail/{policy} endpoint |
| Stateless policy endpoints | SecurityFilterChain with permitAll for the rate-limit and failure-policy paths |
Failure and attack patterns¶
- Shared bucket lets one attacker consume the quota for all users.
- No
Retry-Aftercauses clients to retry immediately, amplifying the load. - IP spoofing without proxy trust lets an attacker cycle headers and evade limits.
- Implicit fail open in an exception handler can silently authorize requests during outages.
- Implicit fail closed can take down a whole service because one dependency is slow.
- Rate limiter as the only defense does not stop distributed abuse; combine with WAF and CDN-level limits.
Guarantees and limitations¶
RateLimitingServicekeeps one bucket perip:method:pathkey and refills one token per second up to a capacity of five.RateLimitingFilterrejects excess calls with429and aRetry-Afterheader./rate-limit/fail/openand/rate-limit/fail/closedmake the failure policy explicit and observable.- The lab uses in-memory storage. Production needs distributed rate limiting and centralized policy configuration.