Exclude dependency proxy requests from unauthenticated IP throttle
What does this MR do and why?
Dependency proxy requests for container images no longer count against the unauthenticated per-IP rate limit (60 an hour) on GitLab.com. Authenticated pulls now count against the right requester.
Three causes:
- Every pull starts with an anonymous request. Docker and containerd send it without credentials, get a 401 with a
WWW-Authenticateheader, and only then fetch a token from/jwt/auth. That request counted against the IP. Once an IP is over 60 an hour, enforcement returns a 429 instead of the 401. The client never learns where to authenticate, and the pull fails before it starts. - Deploy-token JWTs were never resolved at the rack layer.
find_user_from_dependency_proxy_tokendiscards aDeployTokenon purpose (#342481 (closed)). So CI pulls with a deploy token counted per IP, and runners share IPs. - The rack-side JWT read only covered
GET .../(manifests|blobs)/<ref>. A valid user JWT onGET /v2(the Docker auth ping) and on the four cache-write upload routes counted as anonymous.
Data from 24 hours:
- About 372k anonymous 401 challenges a day would be blocked. They come from about 1,033 IPs.
- About 83.6k successful authenticated requests a day would also be blocked. 30,973 of them are on
GET /v2. - Combined, that is about 456k requests a day.
The full tables are in https://gitlab.com/gitlab-com/gl-infra/production-engineering/-/work_items/29995.
Changes:
- Add
dependency_proxy_containers_route_regexinlib/gitlab/path_regex.rb. It matchesGET /v2exactly and every/v2/<namespace>/dependency_proxy/containers/...route, including the cache-write uploads. It doesn't match virtual registry image paths, which have no/dependency_proxy/containers/segment.GET /v2is also the virtual registry auth ping, so it matches for those clients too. - Exclude it from
unauthenticated_traffic_per_ipand its log-mode twin inee/lib/ee/gitlab/rate_limit/plan_rules.rb. This alone removes the per-IP impact, including the 401 challenges. Those requests stay underunauthenticated_web. - In
lib/gitlab/auth/auth_finders.rb,dependency_proxy_request?now uses the new regex, so the JWT is read on/v2and the upload routes too. The decode is memoized in a newdependency_proxy_requester, so the deploy-token check and the user finder share one decode. Before, a JWT that resolved to no user, such as a deploy token, was decoded once for each of the[:api, :rss, :ics]request formats. - In
lib/gitlab/auth/request_authenticator.rb,find_authenticated_requesterreturns a dependency proxy deploy token as the requester.find_sessionless_userstill returns only users, so #342481 (closed) stays fixed. - Correct the reason in the Jira Connect route comment in
ee/lib/ee/gitlab/path_regex.rb. Lifecycle events stay counted because verifying them fetches a public key over the network. The app descriptor and public keys are fetched once per install, and the other routes are browser requests from users' own IPs. This follows a review comment on !259902 (merged). - Document the exclusion in
doc/user/gitlab_com/rate_limits.md.
Behaviour change outside GitLab.com
find_authenticated_requester also feeds the Rack::Attack throttles on every instance.
- Deploy-token dependency proxy pulls now count as authenticated, per deploy token.
- User JWTs on
/v2and the upload routes move from the unauthenticated web throttle (per IP) to the authenticated web throttle (per user). They do not move intothrottle_authenticated_dependency_proxy, which still keys on the unchangeddependency_proxy_route_regex.
Who is allowed to pull is unchanged. The dependency proxy controllers authenticate with JwtAuthenticatable#authenticate_user_from_jwt_token! and never call find_sessionless_user. On /v2 paths, only the rate limiters reach the widened finder:
- Workhorse
authorize_uploadis under/api/v4/internal/. - The controllers that call
authenticate_sessionless_user!do not serve/v2. API::APIGuarddoes not use this finder.
Known tradeoffs
dependency_proxy_containers_route_regexlives in CE, not inEE::Gitlab::PathRegexlike the other exclusion patterns. The CE auth finder also uses it, and one definition cannot drift.dependency_proxy_route_regexis not widened. It also decides thedependency_proxyclassification fact andthrottle_authenticated_dependency_proxy. Widening it would move traffic between throttles as a side effect.- The pattern matches by prefix under
/containers/. It also covers the OCIreferrerscatch-all route, which returns 404. - After this change, anonymous dependency proxy requests are limited only by
unauthenticated_web(about 793k 401s a day). Checking that its GitLab.com limit is acceptable is a follow-up on the issue. So is deciding whether the Workhorse cache writes should count against the user at all. - Cost: anonymous requests carry no token, so they return before any decode. Requests with a JWT do one HMAC decode and one primary-key lookup.
References
- Issue: https://gitlab.com/gitlab-com/gl-infra/production-engineering/-/work_items/29995
- Deploy token decision: #342481 (closed)
- Same problem for CI job tokens: https://gitlab.com/gitlab-com/gl-infra/production-engineering/-/work_items/29990
- Brownout analysis: https://gitlab.com/gitlab-com/gl-infra/production/-/work_items/22991#note_3945857418
- Epic: https://gitlab.com/groups/gitlab-com/gl-infra/-/work_items/2122
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Related to https://gitlab.com/gitlab-com/gl-infra/production-engineering/-/work_items/29995