Follow-up to #619, which #789 closes by implementing its first point only. That pull request lets a request through when its credential is one the pool already holds, so a token sprayer behind a shared address no longer cuts off its legitimate neighbours. The two remaining points are additive hardening rather than the fix for that defect, and they were deliberately kept out so the security change stayed the smallest one that is clearly correct.
What is still owed
Count distinct rejected tokens per address over a longer window, beside the fast lockout, and escalate. Ten failures in a minute is a stuck client retrying one bad token as much as it is an attack, and a minute's block is the right answer to both. Fifty distinct invalid tokens from one address inside ten minutes is only an attack, and the answer is a block that lasts and grows: a minute, then ten, then an hour, reset by an hour of silence. The distinct count is also what keeps a legitimate neighbour from ever tripping it, because a person has one token.
Make both windows and both limits configurable, as flags with GITLAB_MCP_ environment variables, since a public deployment and a LAN one want different numbers. Record the block in telemetry as a counter keyed by the reason, never by the address.
Why this matters beyond the neighbours
Every distinct token that reaches verification is a request to GitLab from the deployment's own address. GitLab rate-limits failed authentication per source address, so a sprayer that gets past the per-address budget, for instance by rotating addresses, is not attacking this server: it is spending the deployment's standing with GitLab, and the block GitLab eventually applies lands on the server, for everyone. The distinct-token count is the earliest signal of that, earlier than the failure count, because it is the one thing a sprayer cannot avoid producing.
Where it belongs
test/e2e/http, which is where the existing budgets and the ordering in resolve are already covered, and where #789 put its two regressions. The cases are a distinct-token count that blocks while a same-token retry does not, and an escalation that lengthens on repetition and resets after silence.
docs/guides/http-server-mode.md and docs/concepts/security.md describe the current budgets and get the new ones.
What already exists to build on
- The per-address failure lockout, ten failures in one minute, keyed by client IP or by
--trusted-proxy-header when the connection came from a --trusted-proxies peer.
- The per-transport-source budget of five hundred distinct failing keys per minute,
transportFailureLimit.
oauth.RejectedTokens, so a token GitLab already refused is not verified twice.
ServerPool.Admitted, added by #789, which answers whether the next request for a credential would take the pool's fast path.
Follow-up to #619, which #789 closes by implementing its first point only. That pull request lets a request through when its credential is one the pool already holds, so a token sprayer behind a shared address no longer cuts off its legitimate neighbours. The two remaining points are additive hardening rather than the fix for that defect, and they were deliberately kept out so the security change stayed the smallest one that is clearly correct.
What is still owed
Count distinct rejected tokens per address over a longer window, beside the fast lockout, and escalate. Ten failures in a minute is a stuck client retrying one bad token as much as it is an attack, and a minute's block is the right answer to both. Fifty distinct invalid tokens from one address inside ten minutes is only an attack, and the answer is a block that lasts and grows: a minute, then ten, then an hour, reset by an hour of silence. The distinct count is also what keeps a legitimate neighbour from ever tripping it, because a person has one token.
Make both windows and both limits configurable, as flags with
GITLAB_MCP_environment variables, since a public deployment and a LAN one want different numbers. Record the block in telemetry as a counter keyed by the reason, never by the address.Why this matters beyond the neighbours
Every distinct token that reaches verification is a request to GitLab from the deployment's own address. GitLab rate-limits failed authentication per source address, so a sprayer that gets past the per-address budget, for instance by rotating addresses, is not attacking this server: it is spending the deployment's standing with GitLab, and the block GitLab eventually applies lands on the server, for everyone. The distinct-token count is the earliest signal of that, earlier than the failure count, because it is the one thing a sprayer cannot avoid producing.
Where it belongs
test/e2e/http, which is where the existing budgets and the ordering inresolveare already covered, and where #789 put its two regressions. The cases are a distinct-token count that blocks while a same-token retry does not, and an escalation that lengthens on repetition and resets after silence.docs/guides/http-server-mode.mdanddocs/concepts/security.mddescribe the current budgets and get the new ones.What already exists to build on
--trusted-proxy-headerwhen the connection came from a--trusted-proxiespeer.transportFailureLimit.oauth.RejectedTokens, so a token GitLab already refused is not verified twice.ServerPool.Admitted, added by #789, which answers whether the next request for a credential would take the pool's fast path.