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

  1. The Labkit registry entry must drop claims: true. The cherry-pick applied cleanly here, which was the trap: on 19.2, ThrottleRegistry::Entry is a Struct with keyword_init: true whose members do not include :claims, and .all splats the metadata into keyword arguments via **attrs.except(:throttle). A silently-merged claims: entry would have raised ArgumentError: unknown keywords: :claims the 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.
  2. 19.4 moved the rate-limit path predicates out of lib/gitlab/rack_attack/request.rb into lib/gitlab/rate_limit/request_classification.rb; neither file exists on 19.2. dependency_proxy_path? was re-homed into lib/gitlab/rack_attack/request.rb, with the EE virtual-registry exclusion in ee/lib/ee/gitlab/rack_attack/request.rb. ClassifiedRequest includes Gitlab::RackAttack::Request, which carries prepend_mod_with, so one CE helper plus one EE override still reach both stacks.
  3. Master's lib/gitlab/rack_attack/labkit_rate_limit/classified_request.rb diff also adds a runner_jobs: classification fact. 19.2 expresses runner-jobs as a synthetic terminating skip rule in Limiters instead, so only the dependency_proxy: fact was taken.
  4. 19.2's throttle_registry_spec.rb has 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.
  5. spec/requests/labkit_rack_rate_limit_spec.rb doesn't exist on 19.2; its new claim-before-web and fall-through cases went into spec/requests/labkit_rack_rate_limit_shadow_spec.rb. The #dependency_proxy_path? unit coverage went into spec/lib/gitlab/rack_attack/request_spec.rb.
  6. lib/api/settings.rb needs no change: master's diff only adds parameter descriptions to a block introduced by later unrelated work, and 19.2 already accepts the settings via optional(*Helpers::SettingsHelpers.optional_attributes), fed by the included visible_attributes change. doc/administration/instance_limits.md keeps 19.2's "Pipeline creation rate limit" section and adds the new section in master's relative position. doc/api/openapi/openapi_v3.yaml was regenerated with bin/rake gitlab:openapi:v3:generate rather than hand-edited, so its RequestBody_ digest is whatever the generator produces for 19.2.
  7. doc/administration/settings/user_and_ip_rate_limits.md and doc/api/settings.md carried master's GitLab 19.4 references verbatim, which doesn't apply on a stable branch; commit 136de6b61d7b repoints 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.all builds without raising, confirming the claims: 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 Labkit dependency_proxy fact 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 with Ratelimit-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_limits keys that 19.2's application_setting_diff_limits.json rejects under additionalProperties: 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
  • This MR has been approved by a maintainer (only one approval is required).
  • Ensure the e2e:test-on-omnibus-ee job 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:

Edited by David Fernandez

Merge request reports

Loading
Loading