Backport of 'Stop dependency proxy requests counting against the web rate limit'
🤔 What does this MR do and why?
Backports !253039 (merged) (merged into master 2026-09-14, deployed to GitLab.com the same day) to 19-2-stable-ee for the next 19.2 patch release. The 19.3 sibling is !255600 (merged).
Fixes #627044 (closed) (severity::2). Authenticated dependency proxy container image pulls counted against the authenticated web rate limit, so image pulls shared one budget with a user's ordinary browsing. CI jobs running several parallel docker pulls exhausted that budget and failed with HTTP 429. A GitLab Dedicated customer running 19.2.4 hit this in production, which is why 19.2 is a target.
The fix adds a dedicated throttle, throttle_authenticated_dependency_proxy, registered in both enforcement stacks: the legacy Rack::Attack predicates and the newer Labkit throttle registry. It adds three keys to the existing rate_limits JSONB column, so there is no schema change. Defaults: disabled, 1000 requests, 15 seconds. A post-deployment migration enables the throttle on any instance where the authenticated web throttle is already enabled, copying that throttle's requests-per-period and period-in-seconds.
This is a split of an existing rate limit, not a new one. It can only loosen limits, never tighten them: before, web browsing and dependency proxy pulls shared one budget; after, each gets its own budget of the same size. While the setting is off, dependency proxy requests keep counting against the web throttle, so nothing escapes rate limiting.
🔀 Backport adaptations
- The Labkit registry entry must drop
claims: true. The cherry-pick applied cleanly here, which was the trap: on 19.2,ThrottleRegistry::Entryis aStructwithkeyword_init: truewhose members do not include:claims, and.allsplats the metadata into keyword arguments via**attrs.except(:throttle). A silently-mergedclaims:entry would have raisedArgumentError: unknown keywords: :claimsthe first time the registry was built. On 19.2 the exclusion is expressed by insertion order alone: the entry sits in the cohort 1 block ahead of the cohort 2 web rules, with no claim. - 19.4 moved the rate-limit path predicates out of
lib/gitlab/rack_attack/request.rbintolib/gitlab/rate_limit/request_classification.rb; neither file exists on 19.2.dependency_proxy_path?was re-homed intolib/gitlab/rack_attack/request.rb, with the EE virtual-registry exclusion inee/lib/ee/gitlab/rack_attack/request.rb.ClassifiedRequestincludesGitlab::RackAttack::Request, which carriesprepend_mod_with, so one CE helper plus one EE override still reach both stacks. - Master's
lib/gitlab/rack_attack/labkit_rate_limit/classified_request.rbdiff also adds arunner_jobs:classification fact. 19.2 expresses runner-jobs as a synthetic terminating skip rule inLimitersinstead, so only thedependency_proxy:fact was taken. - 19.2's
throttle_registry_spec.rbhas no rule-ordering test — that's a 19.3-era addition. Rather than import master's unrelated ordering assertions, a focused example was added asserting only that the dependency proxy rule precedes both web rules, the invariant this change depends on. spec/requests/labkit_rack_rate_limit_spec.rbdoesn't exist on 19.2; its new claim-before-web and fall-through cases went intospec/requests/labkit_rack_rate_limit_shadow_spec.rb. The#dependency_proxy_path?unit coverage went intospec/lib/gitlab/rack_attack/request_spec.rb.lib/api/settings.rbneeds no change: master's diff only adds parameter descriptions to a block introduced by later unrelated work, and 19.2 already accepts the settings viaoptional(*Helpers::SettingsHelpers.optional_attributes), fed by the includedvisible_attributeschange.doc/administration/instance_limits.mdkeeps 19.2's "Pipeline creation rate limit" section and adds the new section in master's relative position.doc/api/openapi/openapi_v3.yamlwas regenerated withbin/rake gitlab:openapi:v3:generaterather than hand-edited, so itsRequestBody_digest is whatever the generator produces for 19.2.doc/administration/settings/user_and_ip_rate_limits.mdanddoc/api/settings.mdcarried master's GitLab 19.4 references verbatim, which doesn't apply on a stable branch; commit136de6b61d7brepoints both to GitLab 19.2.7, the next patch after the already-released 19.2.6. This pin only holds if the change still ships in 19.2.7, so a slip to a later patch means both lines need bumping again.
✅ Local verification
22 checks, all passing. Which stack enforces differs from 19.3: all six rate_limiter_use_labkit_rack_cohort_{1,2,3}{,_enforce} flags default to false on 19.2, and 19.2 has no fully_enforced? Rack::Attack safelist at all. Rack::Attack is the enforcing stack here and the Labkit rules are inert at runtime — but the registry entry is still required, since the registry meta-spec asserts it covers every Rack::Attack throttle.
ThrottleRegistry.allbuilds without raising, confirming theclaims:fix.- The predicate fires for manifest and blob paths and not for
referrers; a dependency proxy request is excluded from the authenticated web throttle when the setting is on and still counted by it when off; the EE virtual-registry path is excluded; the Labkitdependency_proxyfact tracks the same override; the rule is ordered at index 3, ahead of the web rules at 11 and 13; the registry covers every Rack::Attack throttle. - End to end over HTTP with the limit at 1 request per 60 seconds, exercising Rack::Attack: with the throttle enabled, the second pull returned 429 with
Ratelimit-Name: throttle_authenticated_dependency_proxy. With the throttle disabled and the web throttle at 1 per 60 seconds, the request returned 429 withRatelimit-Name: throttle_authenticated_web, confirming the fall-through. - Worth noting for reviewers, unrelated to this change: on a GDK whose database was written by master, ActiveRecord cannot save application settings at all on 19.2, because master added three
diff_limitskeys that 19.2'sapplication_setting_diff_limits.jsonrejects underadditionalProperties: false. Settings had to be written with SQL for the verification — a neat live demonstration of why the throttle's activation ships as a post-deployment migration rather than a settings write.
☑️ MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
- This MR is backporting a bug fix, documentation update, or spec fix, previously merged in the default branch.
- The MR that fixed the bug on the default branch has been deployed to GitLab.com (not applicable for documentation or spec changes).
- The MR title is descriptive (e.g. "Backport of 'title of default branch MR'"). This is important, since the title will be copied to the patch blog post.
- Required labels have been applied to this merge request
- severity label and bug subtype labels (if applicable)
- If this MR fixes a bug that affects customers, the customer label has been applied.
- This MR has been approved by a maintainer (only one approval is required).
- Ensure the
e2e:test-on-omnibus-eejob has succeeded, or if it has failed, investigate the failures. If you determine the failures are unrelated, you may proceed. If you need assistance investigating, request help in the #s_developer_experience Slack channel to confirm the failures are unrelated to the merge request.
Note to the merge request author and maintainer
If you have questions about the patch release process, please:
- Refer to the patch release runbook for engineers and maintainers for guidance.
- Ask questions on the
#releasesSlack channel (internal only). - Once the backport has been merged, the commit changes will be automatically deployed to a release environment that can be used for manual validation. See after merging runbook for details.