Investigate authentication handling for Container Virtual Registry to decouple from Dependency Proxy

Problem

Currently, Container Virtual Registry reuses the Dependency Proxy authentication service (Auth::DependencyProxyAuthenticationService) because both services live on the same host and use the same URL structure (/v2/...). This creates an unnecessary coupling between the two features.

As noted in !210622 (merged)#note_2870902483:

From #549131 (closed):

Authentication. Docker clients will use a specific login flow. For this part, we don't have any choice given that the virtual registry lives in the same host than the dependency proxy for container images. Thus, we have to reuse the dependency proxy authentication service.

The current authentication flow is:

JwtController -> DependencyProxyAuthenticationService -> VirtualRegistries::ContainerController

Challenge

We need to figure out how to distinguish between requests for:

  • Dependency Proxy (/v2/...)
  • Container Virtual Registry (/v2/virtual_registries/container/:id/...)

Both use the same authentication mechanism, but they serve different purposes and should ideally be decoupled.

However, full decoupling at the authentication layer is not possible because both services are constrained by Docker's v2 registry protocol: the Docker CLI requires a /v2/ base path, implements a fixed auth flow (GET /v2/ → 401 challenge → token exchange → retry with Bearer), and caches credentials per host. Since both services live on the same GitLab host, they necessarily share the same token endpoint (/jwt/auth). The only way to have truly separate auth services would be to serve them from different hosts or ports, which is not practical for a single GitLab instance.

Goal

Investigate and propose a solution for handling authentication for Container Virtual Registry that:

  1. Allows us to distinguish between Dependency Proxy and Container Virtual Registry requests
  2. Removes the unnecessary coupling between these two features
  3. Maintains compatibility with Docker client authentication flows
  4. Doesn't break existing Dependency Proxy functionality

Acceptance Criteria

  • Document the current authentication flow for both Dependency Proxy and Container Virtual Registry
  • Identify the specific points where requests need to be distinguished
  • Propose one or more solutions for decoupling the authentication
  • Evaluate trade-offs of each proposed solution
  • Recommend a preferred approach with implementation plan

Proposed Solution

See detailed investigation here and here.

Both Dependency Proxy and Container Virtual Registry share the same JWT authentication infrastructure — same HMAC secret, same DependencyProxyAuthenticationService, same token format. The key distinguishing signal is the scope parameter that the Docker client sends during token exchange: DP scopes contain dependency_proxy/containers/... while VR scopes contain virtual_registries/container/.... We will embed a service_type claim in the JWT based on this scope, and have each controller validate that the token was minted for its service. This is a defense-in-depth measure — controller-level authorization checks already prevent cross-service access, but scoping tokens at the authentication layer adds an extra boundary.

Planned Changes

  1. Auth::DependencyProxyAuthenticationService#authorized_token — Detect service_type from the scopes param and embed it in the JWT:

    • Scope containing virtual_registries/container/token['service_type'] = 'virtual_registry'
    • Scope containing dependency_proxy/containers/token['service_type'] = 'dependency_proxy'
    • No scope (login flow) → no service_type claim
  2. DependencyProxy::AuthTokenService.user_or_token_from_jwt — Expose the service_type from the decoded token payload so controllers can access it.

  3. VirtualRegistries::ContainerController — Override authenticate_user_from_jwt_token! to reject tokens that don't have service_type: 'virtual_registry'.

  4. DP controllers (optional, for symmetry) — In Groups::DependencyProxy::ApplicationController or DependencyProxy::GroupAccess, reject tokens with a wrong service_type (while still accepting tokens with no service_type for backward compatibility with cached login tokens).

  5. Specs for all of the above.

  6. Rename Auth::DependencyProxyAuthenticationService to Auth::ContainerProxyAuthenticationService to reflect that it now serves both Dependency Proxy and Container Virtual Registry. Update all references.

Edited by Radamanthus Batnag