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::ContainerControllerChallenge
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:
- Allows us to distinguish between Dependency Proxy and Container Virtual Registry requests
- Removes the unnecessary coupling between these two features
- Maintains compatibility with Docker client authentication flows
- 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
-
Auth::DependencyProxyAuthenticationService#authorized_token— Detectservice_typefrom thescopesparam 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_typeclaim
- Scope containing
-
DependencyProxy::AuthTokenService.user_or_token_from_jwt— Expose theservice_typefrom the decoded token payload so controllers can access it. -
VirtualRegistries::ContainerController— Overrideauthenticate_user_from_jwt_token!to reject tokens that don't haveservice_type: 'virtual_registry'. -
DP controllers (optional, for symmetry) — In
Groups::DependencyProxy::ApplicationControllerorDependencyProxy::GroupAccess, reject tokens with a wrongservice_type(while still accepting tokens with noservice_typefor backward compatibility with cached login tokens). -
Specs for all of the above.
-
Rename
Auth::DependencyProxyAuthenticationServicetoAuth::ContainerProxyAuthenticationServiceto reflect that it now serves both Dependency Proxy and Container Virtual Registry. Update all references.