IP block
Source: src/security/ip-block.service.ts
Tracks bursts of authentication failures per source IP. After 10 failures in a 1-hour rolling window, that IP is blocked for 1 hour. While blocked, all /mcp/* requests from that IP get 401 + an auth.blocked_ip audit row, even if the request carries a valid API key.
Why?
Stops a credential-stuffing attacker from cycling through stolen keys at full request rate. Works in concert with the global per-IP rate limiter — that one caps request volume, this one caps failed-auth volume.
State
Two in-memory maps:
| Map | Key | Value |
|---|---|---|
failures | IP | { timestamps: number[] } — sorted oldest-first, trimmed when entries fall outside the 1h window |
blocks | IP | epoch ms when the block expires |
When the 10th failure lands, the IP moves from failures to blocks. While in blocks, failures is irrelevant — the block is the active state.
API
isBlocked(ip): boolean // cheap O(1) check, used by tenantAuthPlugin
recordFailure(ip): boolean // returns true on the call that tripped the threshold
startIpBlockCleanup(): void // call once at startup; trims expired state every 5 min
_resetIpBlockState(): void // test-onlySingle-process assumption
State is in-process memory. Phase 5’s deploy uses a single Node process per host (systemd unit, not PM2 cluster mode). If we ever scale horizontally, this map must move to Redis with INCR + TTL, or to a blocked_ips DB table.
Tests
tests/integration/ip-block.test.ts:
- under threshold → not blocked
- on 10th failure → tripped + blocked
- IP isolation (other IPs unaffected)
The end-to-end “valid key from blocked IP also fails” test lives in the auth integration tests (Phase 1 §E5).