fix(SEC-31,SEC-32): re-partition the API-key failure limiter on (peer, key id) with probe admission
The gRPC auth failure limiter partitioned on the key id parsed out of the *unauthenticated* token and rejected with ResourceExhausted before VerifyAsync ran. Key ids are not secret — they ride in every token and are listed on the dashboard — so any network peer could send 10 garbage-secret requests per minute and deny that key indefinitely: the legitimate holder's correct secret was refused before it was ever checked, and the success-path Reset that would clear the block sat behind the verification the block prevented (SEC-31). The tracked map was also flushable — any `a_b_c`-shaped junk minted a fresh partition (the `mxgw` literal was never compared), so ~4096 throwaway tokens evicted a blocked entry and reset the window (SEC-32). ApiKeyFailureLimiter moves from IsBlocked/RecordFailure/Reset(string peer) to a partition-pair API: Check/RecordFailure/Reset(ApiKeyThrottlePartition) with an ApiKeyThrottleDecision result. Two layers share one sliding window — a composite (transport peer, key id) partition at ApiKeyFailureLimit, and a per-key-id aggregate across all peers at the new ApiKeyFailureAggregateLimit (default 30) that bounds a source-rotating sprayer. An over-limit state is now a valve rather than a wall: one request per the new ApiKeyFailureProbeIntervalSeconds (default 5) is admitted through to the real verifier, so the correct secret always reaches the constant-time compare and resets both layers. Guarantees preserved: guessing stays bounded per window, and the failure path still spends no store read per attempt. SEC-32 rides the same change set: the interceptor validates token shape (literal `mxgw` prefix, >= 3 non-empty `_` segments, key id <= 64 chars) before minting a key-id partition, each transport peer may mint at most 32 of them before the overflow collapses onto its fallback partition, and eviction prefers fully expired windows and never drops an over-limit partition below a 2x transient overshoot ceiling. Throttled attempts increment mxgateway.auth.throttled, tagged stage=peer|aggregate only — /metrics is unauthenticated (open SEC-14), so no key material may appear there. Docs in the same commit: GatewayConfiguration limiter rows plus the two new keys, the Authentication hot-path paragraph, the Authorization SEC-11 section, and the limiter / SecurityOptions XML remarks (the old NAT rationale described the defective keying). Tracking rows flipped to Done with a change-log entry. Tests: new ApiKeyFailureLimiterTests (11) covering window pruning, composite vs aggregate trip points, probe cadence, absolute-block mode, reset across both layers, junk-spray eviction resistance, the per-peer cap, and expired-window eviction preference; GatewayGrpcAuthorizationInterceptorTests gains the four SEC-31 contract tests plus NonMxgwToken_FallsBackToTransportPeerPartition (20 total); GatewayOptionsValidatorTests covers both new keys including 0 as a supported disable value (66 total).
This commit is contained in:
@@ -116,6 +116,26 @@ a dictionary lookup. Both windows are configurable and may be set to `0` to disa
|
||||
the respective mechanism; see
|
||||
[GatewayConfiguration](./GatewayConfiguration.md).
|
||||
|
||||
Failures are never cached — a wrong secret always reaches the store — so the
|
||||
failure path is shielded by `ApiKeyFailureLimiter` instead, consulted before
|
||||
`VerifyAsync` runs. It counts failures over one sliding
|
||||
`MxGateway:Security:ApiKeyFailureWindowSeconds` window in two layers: a composite
|
||||
`(transport peer, key id)` partition capped at `ApiKeyFailureLimit`, and a
|
||||
per-key-id aggregate across all peers capped at `ApiKeyFailureAggregateLimit`. The
|
||||
key id never partitions on its own — it is public, so an attacker-supplied one
|
||||
would otherwise let any peer throttle a key it does not hold — and it joins the
|
||||
partition only when the presented token is validly shaped
|
||||
(`mxgw_<keyId>_<secret>`, key id at most 64 characters), with at most 32 key-id
|
||||
partitions per address before the overflow collapses onto that address's fallback
|
||||
partition. An over-limit state admits one probe per
|
||||
`ApiKeyFailureProbeIntervalSeconds` through to the real verifier and refuses
|
||||
everything else with `ResourceExhausted` before the store read, so a legitimate
|
||||
holder presenting the correct secret always reaches the constant-time compare and
|
||||
resets both layers; the counter's LRU eviction (`ApiKeyFailureTrackedPeers`)
|
||||
prefers expired windows and will not drop an over-limit partition below a 2x
|
||||
overshoot ceiling, so the memory bound cannot be turned into a way to clear an
|
||||
active block. See [Authorization](./Authorization.md) for the enforcement path.
|
||||
|
||||
## Storage
|
||||
|
||||
API-key state lives in a dedicated SQLite database owned by the shared library.
|
||||
|
||||
Reference in New Issue
Block a user