Container Registry Routing Service
| Status | Authors | Coach | DRIs | Owning Stage | Created |
|---|---|---|---|---|---|
| accepted |
ayufan
|
2025-07-17 |
This document outlines the design goals and architecture of Container Registry Routing Service.
Overview
This document describes the implementation of Container Registry routing within the GitLab Cells architecture using the HTTP Router. The Container Registry requires routing to the correct Cell based on project ownership using path-based routing.
Current Docker Push Flow
Before diving into Cells implementation, let’s understand how Docker push works today:
sequenceDiagram
participant Docker as Docker Client
participant Registry as registry.gitlab.com
participant GitLab as gitlab.com<br/>(GitLab Rails)
participant Storage as Object Storage
Note over Docker,Storage: Current Docker Push Flow (Single Instance)
Docker->>+Registry: docker push registry.gitlab.com/my-org/my-project:latest
Registry->>-Docker: 401 Unauthorized + JWT Auth URL<br/>(https://gitlab.com/jwt/auth)
Docker->>+GitLab: GET /jwt/auth?scope=repository:my-org/my-project:push<br/>(with user credentials)
GitLab->>GitLab: Validate user & project permissions
GitLab->>-Docker: JWT Token
Docker->>+Registry: POST /v2/my-org/my-project/blobs/uploads/<br/>(with JWT token, upload blobs first)
Registry->>+Storage: Store blob layers
Storage->>-Registry: Stored
Registry->>-Docker: Upload complete
Docker->>+Registry: PUT /v2/my-org/my-project/manifests/latest<br/>(with JWT token, manifest pushed last)
Registry->>+Storage: Store manifest
Storage->>-Registry: Stored
Registry->>-Docker: 201 Created
Architecture Overview
In the Cells architecture, the /jwt/auth endpoint is part of GitLab Rails and must be processed by the HTTP Router to decode the scope and route to the correct Cell. Container Registry routing uses path-based routing exclusively, extracting project information from URL paths. All endpoints not explicitly classified will route to the legacy Cell.
Key Components
- HTTP Router: Deployed as Cloudflare Worker, handles request routing
- Container Registry: Run as local service within each Cell
- Topology Service: Provides Cell discovery and classification
- JWT Authentication:
/jwt/authendpoint within GitLab Rails for Docker authentication - Legacy Cell: Default target for all non-classified endpoints and global operations
HTTP Router Rules Configuration
Session Token Rules (ruleset/session_token.json)
Add the following rule to the existing session token ruleset for JWT authentication:
{
"comment": "Container Registry JWT Authentication - with scope (path-based routing)",
"match": [
{
"type": "path",
"regex_name": "jwt_auth",
"regex_value": "^/jwt/auth$"
},
{
"type": "query_string",
"regex_name": "scope",
"regex_value": "repository:(?<route>[^:]+):(?<actions>.*)"
}
],
"action": "classify",
"classify": [
{
"type": "route",
"value": "${route}",
"target": "web"
}
]
}
Note: /jwt/auth will be routed based on path only if scope repository is present, otherwise it will fallback to secret-based routing (handled by other rules in the session token ruleset). The route is sent in full to the Topology Service, which will try to find the longest matching prefix.
JWT Authentication Routing Limitations
The current implementation depends on the first registry scope in the /jwt/auth request for routing decisions. This creates a potential issue since the Docker Registry specification does not define the order of scopes in the request.
Current Assumption: Clients make the first scope the repository where they want to push, and other scopes are repositories they intend to link from (for blob mounting operations).
Potential Issue: If this assumption is incorrect and clients provide scopes in different orders, requests might be routed to the wrong Cell.
Alternative Routing Solutions
If the first-scope assumption proves problematic, several alternatives exist:
-
Current Implementation:
/jwt/authwith secret-based routing (as described above) -
Option 1 (Preferred):
/jwt/auth?cell_id=X- Configure Registry to include
cell_idparameter - Minimal changes required
- Risk: Some clients might not support cell_id parameters and drop them
- Configure Registry to include
-
Option 2:
/c/cell-id/jwt/auth- Requires GitLab Rails changes to support
/c/cell-idprefix - More reliable than query parameters
- Moderate implementation complexity
- Requires GitLab Rails changes to support
-
Option 3:
/jwt/auth/cell/cell_id- Requires GitLab Rails changes to support different paths
- Higher implementation complexity
Recommendation: Option 1 is preferred due to minimal required changes, with Option 2 as fallback if query parameter support proves insufficient.
Container Registry Rules (ruleset/container_registry.json)
Minimal ruleset for Container Registry with four rules:
{
"rules": [
{
"comment": "Container Registry v2 API - All project-related operations",
"match": [
{
"type": "path",
"regex_name": "v2_api",
"regex_value": "^/v2/(?<route>[^/]+(?:/[^/]+)*)/.*$"
}
],
"action": "classify",
"classify": [
{
"type": "route",
"value": "${route}",
"target": "registry"
}
]
},
{
"comment": "Container Registry v2 base endpoint - JWT token validation",
"match": [
{
"type": "path",
"regex_name": "v2_base",
"regex_value": "^/v2/$"
}
],
"action": "classify",
"classify": [
{
"type": "org_id",
"value": "${org_id}",
"target": "registry"
}
]
},
{
"comment": "GitLab Container Registry HTTP API V1",
"match": [
{
"type": "path",
"regex_name": "gitlab_v1_api",
"regex_value": "^/gitlab/v1/repositories/(?<route>[^/]+(?:/[^/]+)*)/.*$"
}
],
"action": "classify",
"classify": [
{
"type": "route",
"value": "${route}",
"target": "registry"
}
]
},
{
"comment": "All other registry endpoints route to first cell",
"match": [],
"action": "classify",
"classify": {
"type": "FIRST_CELL",
"target": "registry"
}
}
]
}
Note: The /v2/ endpoint is used for JWT token validation by Docker client. The JWT token contains organization_id which enables routing to the correct Cell. This requires organization_id to be added in the GitLab Rails generated JWT.
Topology Service Configuration Extension
Current Topology Service Configuration
The Topology Service configuration needs to be extended to include Container Registry URL information for each Cell:
# Legacy Cell configuration
[[cells]]
address = "gitlab.com"
registry_address = "registry.gitlab.com"
[[cells]]
address = "cell-us-1.gitlab.com"
registry_address = "registry-cell-us-1.gitlab.com"
[[cells]]
address = "cell-eu-1.gitlab.com"
registry_address = "registry-cell-eu-1.gitlab.com"
Docker Login Authentication Flow
Docker Login Process
The docker login command is scopeless and will be routed using secret-based routing:
sequenceDiagram
participant Docker as Docker Client
participant GitLabRouter as HTTP Router<br/>(gitlab.com)
participant RegistryRouter as HTTP Router<br/>(registry.gitlab.com)
participant TS as Topology Service
participant Cell2 as Cell 2<br/>(GitLab Rails)
participant Legacy as Legacy Cell<br/>(gitlab.com)
Note over Docker,Legacy: Docker Login: docker login registry.gitlab.com
Docker->>+GitLabRouter: GET /jwt/auth<br/>(no scope parameter, authentication token in headers)
Note over GitLabRouter: Scopeless request - fallback to secret-based routing
GitLabRouter->>+TS: Classify based on authentication token
TS->>-GitLabRouter: Proxy(target=web, address=cell-2.gitlab.com)
GitLabRouter->>+Cell2: GET /jwt/auth<br/>(authentication token in headers)
Cell2->>Cell2: Validate token & generate JWT
Cell2->>-GitLabRouter: JWT Token
GitLabRouter->>-Docker: JWT Token
Note over Docker,Legacy: Docker client validates JWT with registry
Docker->>+RegistryRouter: GET /v2/<br/>(with Bearer JWT token)
RegistryRouter->>+TS: Classify based on JWT org_id
TS->>-RegistryRouter: Proxy(target=registry, address=registry-cell-2.gitlab.com)
RegistryRouter->>+Cell2: GET /v2/<br/>(with Bearer JWT token)
Cell2->>-RegistryRouter: 200 OK
RegistryRouter->>-Docker: 200 OK
Repository Linking Limitation
Repository linking functionality will not work in this architecture:
- Cross-Cell Mounting: Users cannot mount blobs from repositories in different Cells
- Public Resource Access: Users can only access public resources from other Organizations or Cells
- Single Repository Routing: Each JWT request is routed based on the first repository in the scope only
Public Repository Access Across Cells
The /jwt/auth endpoint routing based on project path is essential for accessing public container repositories from other Cells. When a user attempts to pull a public image from a different Cell:
- Path-Based Routing: The
/jwt/authrequest includes the repository scope, allowing the HTTP Router to route the authentication request to the Cell containing the repository - Cross-Cell Public Access: This enables users to authenticate and pull public repositories that exist in different Cells than their home Cell
- Public Project Resolution: Even though user credentials may not be valid on another Cell, the system will be able to resolve public project access by routing the authentication request to the correct Cell where the public repository exists
Without path-based routing for /jwt/auth, users would only be able to access repositories within their own Cell.
Container Registry Request Flow
Docker Push Flow with Separate HTTP Routers
sequenceDiagram
participant Docker as Docker Client
participant GitLabRouter as HTTP Router<br/>(gitlab.com)
participant RegistryRouter as HTTP Router<br/>(registry.gitlab.com)
participant Cache as Cloudflare Cache
participant TS as Topology Service
participant Cell2 as Cell 2<br/>(GitLab Rails + Registry)
participant Legacy as Legacy Cell<br/>(gitlab.com)
Note over Docker,Legacy: Docker Push: registry.gitlab.com/my-org/my-project:latest
Docker->>+RegistryRouter: docker push registry.gitlab.com/my-org/my-project:latest
RegistryRouter->>+RegistryRouter: Match /v2/my-org/my-project/manifests/latest
RegistryRouter->>+Cache: GetClassify(type=route, value=my-org/my-project, target=registry)
Cache->>-RegistryRouter: NotFound
Note over TS: Find longest matching prefix for my-org/my-project
RegistryRouter->>+TS: Classify(type=route, value=my-org/my-project, target=registry)
TS->>-RegistryRouter: Proxy(address=registry-cell-2.gitlab.com)
RegistryRouter->>Cache: Cache classification result
RegistryRouter->>+Cell2: docker push registry-cell-2.gitlab.com/my-org/my-project:latest
Cell2->>-RegistryRouter: 401 Unauthorized + JWT Auth URL
RegistryRouter->>-Docker: 401 Unauthorized + JWT Auth URL
Docker->>+GitLabRouter: GET /jwt/auth?scope=repository:my-org/my-project:push
GitLabRouter->>+Cache: GetClassify(type=route, value=my-org/my-project, target=web)
Cache->>-GitLabRouter: NotFound
GitLabRouter->>+TS: Classify(type=route, value=my-org/my-project, target=web)
Note over TS: Find longest matching prefix for my-org/my-project
TS->>-GitLabRouter: Proxy(address=cell-2.gitlab.com)
GitLabRouter->>+Cell2: GET /jwt/auth?scope=repository:my-org/my-project:push
Cell2->>Cell2: Validate user & project permissions
Cell2->>-GitLabRouter: JWT Token
GitLabRouter->>-Docker: JWT Token
Docker->>+RegistryRouter: POST /v2/my-org/my-project/blobs/uploads/<br/>(upload blobs first)
RegistryRouter->>+Cache: GetClassify(type=route, value=my-org/my-project, target=registry)
Cache->>-RegistryRouter: Proxy(address=registry-cell-2.gitlab.com)
RegistryRouter->>+Cell2: POST /v2/my-org/my-project/blobs/uploads/
Cell2->>-RegistryRouter: Blob upload complete
RegistryRouter->>-Docker: Blob upload complete
Docker->>+RegistryRouter: PUT /v2/my-org/my-project/manifests/latest<br/>(manifest pushed last)
RegistryRouter->>+Cache: GetClassify(type=route, value=my-org/my-project)
Cache->>-RegistryRouter: Proxy(address=registry-cell-2.gitlab.com)
RegistryRouter->>+Cell2: PUT /v2/my-org/my-project/manifests/latest
Cell2->>-RegistryRouter: 201 Created
RegistryRouter->>-Docker: 201 Created
Legacy Cell Fallback Flow
sequenceDiagram
participant Docker as Docker Client
participant RegistryRouter as HTTP Router<br/>(registry.gitlab.com)
participant TS as Topology Service
participant FirstCell as First Cell<br/>(registry.gitlab.com)
Note over Docker,FirstCell: Global operations route to first cell
Docker->>+RegistryRouter: GET /v2/_catalog
RegistryRouter->>+TS: Classify(type=FIRST_CELL, target=registry)
TS->>-RegistryRouter: Proxy(address=registry.gitlab.com)
RegistryRouter->>+FirstCell: GET /v2/_catalog
FirstCell->>-RegistryRouter: Catalog response (all repositories)
RegistryRouter->>-Docker: Catalog response
Docker->>+RegistryRouter: GET /v2/
RegistryRouter->>+TS: Classify(type=FIRST_CELL, target=registry)
TS->>-RegistryRouter: Proxy(address=registry.gitlab.com)
RegistryRouter->>+FirstCell: GET /v2/
FirstCell->>-RegistryRouter: Registry info
RegistryRouter->>-Docker: Registry info
Implementation Details
HTTP Router Configuration
Separate HTTP Routers are deployed for different domains:
// wrangler.toml configuration for gitlab.com
[env.gprd.vars]
vars = {
GITLAB_SESSION_RULES = "session_token",
TOPOLOGY_SERVICE_URL = "https://topology-service.gitlab.com"
}
// wrangler.toml configuration for registry.gitlab.com
[env.reg_gprd.vars]
vars = {
GITLAB_SESSION_RULES = "container_registry",
TOPOLOGY_SERVICE_URL = "https://topology-service.gitlab.com"
}
Separate HTTP Router Architecture
The Container Registry uses a separate HTTP Router deployment from GitLab Rails for the following architectural reasons:
Service Separation Benefits
-
Different Rule Complexity: Registry rules are lightweight (3-4 rules) and unlikely to change after initial deployment, while GitLab Rails rules are more complex and frequently updated
-
Different Token Handling: Registry uses JWT Bearer tokens with different validation requirements than GitLab Rails session tokens
-
Performance Optimization: Avoids CPU cost of processing GitLab Rails rules that will never match registry requests
-
Reduced Complexity: Eliminates need for:
- Host header processing when not required
- Conditional logic for different rulesets based on hostname
- Environment-specific ruleset management
-
Service Scalability: Follows the pattern where additional services get their own rulesets rather than joining existing ones
-
Configuration Management: Each service can maintain its own ruleset without cross-service dependencies
Configuration Approach
Rather than using magic conventions replicated across multiple places, explicit configuration is required. While conventions are acceptable when implemented in a single place, having the same convention in multiple locations creates maintenance complexity. Services implementing conventions can generate appropriate configurations, but the configuration itself must be explicit and centralized.
HTTP Router Changes Required
The HTTP Router requires the following enhancements to support Container Registry routing:
1. Target-Based Routing Support
Add support for the target parameter in routing rules:
export interface ClassifyRequest {
type: string;
value?: string;
target: string; // default to "web"
}
2. Topology Service API Updates
Update the ClassifyRequest interface:
enum TargetType {
WEB = 0;
REGISTRY = 1;
}
message ClassifyRequest {
ClassifyType type = 2;
string value = 3;
TargetType target = 4;
}
3. Topology Service Config Updates
[[cells]]
id = 1
address = "my.cell-1.example.com"
registry_address = "registry.my.cell-1.example.com"
4. Multiple Classification Matches Support
The HTTP Router must support multiple classification matches processed in sequence. Example rule structure:
{
"match": [
{
"type": "path",
"regex_name": "jwt_auth",
"regex_value": "^/jwt/auth$"
},
{
"type": "query_string",
"regex_name": "scope",
"regex_value": "repository:(?<project_path>[^:]+):(?<actions>.*)"
}
]
}
Matches are processed in sequence until a successful classification is found. The route is sent in full to the Topology Service, which will try to find the longest matching prefix.
5. Caching with Target Parameter
The target parameter will be automatically cached as part of the classify request body hash. The existing caching mechanism in the HTTP Router caches based on the request body hash, which will include the target parameter, ensuring proper cache isolation between different target types.
Topology Service Target-Based Routing
The HTTP Router uses the target parameter when calling the Topology Service. The Topology Service, based on the target, will return the proper URL:
// Example Topology Service call
const response = await topologyService.classify({
type: "project_full_path",
value: "my-org/my-project",
target: "registry" // or "web"
});
// Response will contain the appropriate URL based on target
// For target="registry": returns registry-cell-2.gitlab.com
// For target="web": returns cell-2.gitlab.com
Cell Configuration Requirements
No changes are required in GitLab Rails or Container Registry components. All connectivity is handled by the HTTP Router. Each Cell requires minimal configuration:
Container Registry Configuration
Each Cell’s Container Registry must be configured with:
- Public Endpoint: All Cells use
registry.gitlab.comas the public host - Internal Communication: Each Cell uses internal URLs (
api_url) for Cell-to-Registry communication - Cell-Specific Keys: Each Cell has its own signing keys for JWT token validation
- Internal Notifications: Registry pushes notifications to Cell Rails using internal networking
GitLab Rails Configuration
GitLab Rails requires minimal configuration changes:
# GitLab Rails configuration for Cell
gitlab:
registry:
enabled: true
host: registry.gitlab.com # Public endpoint
port: 443
api_url: http://registry-cell-1.internal:5000/ # Internal registry URL
key: /path/to/cell-1-registry.key # Same key as registry for JWT validation
issuer: cell-1-gitlab-issuer # Cell-specific issuer
Network Architecture
The HTTP Router handles all external routing, while Cells use internal networking:
graph TB
subgraph "External Traffic"
User[Docker Client]
Router[HTTP Router<br/>registry.gitlab.com]
end
subgraph "Cell 1 Internal Network"
Rails1[GitLab Rails<br/>cell-1-rails.internal]
Registry1[Container Registry<br/>registry-cell-1.internal]
Registry1 -->|Notifications| Rails1
Rails1 -->|JWT Validation| Registry1
end
subgraph "Cell 2 Internal Network"
Rails2[GitLab Rails<br/>cell-2-rails.internal]
Registry2[Container Registry<br/>registry-cell-2.internal]
Registry2 -->|Notifications| Rails2
Rails2 -->|JWT Validation| Registry2
end
User --> Router
Router -->|Routed Traffic| Registry1
Router -->|Routed Traffic| Registry2
style Router fill:#ff9900
style Registry1 fill:#6699ff
style Registry2 fill:#6699ff
Related Documents
- GitLab Cells Infrastructure Architecture
- HTTP Router Configuration
- HTTP Router Rulesets
- Topology Service Implementation
- Container Registry Auth Request Flow - Most accurate Docker flows documentation
3643eb9e)
