Rate limit Service Account creation

What does this MR do and why?

Introduces a rate limit of 10 service account creations per minute per authenticated user on all service account creation endpoints:

  • POST /service_accounts (instance level)
  • POST /groups/:id/service_accounts (group level)
  • POST /projects/:id/service_accounts (project level)

This addresses a security concern where automated SA creation (e.g., through Duo Flows) could exhaust the available service accounts for a tier and cause DoS.

The rate limit:

  • Applies to both GitLab.com and Self-Managed instances
  • Uses the authenticated user as scope (each user gets their own bucket)
  • Returns HTTP 429 with Retry-After header when exceeded
  • Does not require admin override (can be adjusted globally in future if needed)

The feature will be released behind the rate_limit_service_accounts_create Feature Flag (issue). This is done to ensure we can quickly rollback if discover that RL should be adjusted.

Test fix

Added :clean_gitlab_redis_rate_limiting metadata to service account spec files to ensure rate limiting state is reset between tests. This prevents rate limit counts from accumulating across shared examples.

References

Related to #581887

How to set up and validate locally

  1. Start GDK
  2. Make sure you enabled FF: Feature.enable(:rate_limit_service_accounts_create)
  3. Create service accounts via API (adjust your GDK URL if needed):
    # As admin (instance-level) or group owner (group-level)
    for i in {1..12}; do
      curl --request POST "http://gdk.test:3443/api/v4/service_accounts" \
        --header "PRIVATE-TOKEN: <your_token>"
      echo " - Request $i"
    done
  4. First 10 requests should succeed with 201
  5. Requests 11-12 should fail with 429 and error message:
    {"message":{"error":"This endpoint has been requested too many times. Try again later."}}
  6. Wait 1 minute and try again to create a new one - request should succeed
  7. You can clean up with User.where(user_type: :service_account).where(created_at: 20.minutes.ago..).find_each(&:destroy!) - be careful, I put "created last 20 minutes" assuming that it's enough and you were not testing other features. Feel free to adjust or even remove via API.

Screenshot_2026-02-12_at_13.07.45

You can also disable FF and ensure that no rate limiting is applied.

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.

Edited by Aleksei Lipniagov

Merge request reports

Loading