-
Notifications
You must be signed in to change notification settings - Fork 0
fix(middleware): support RepoAccess token format in registry auth #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3da1a2a
cd55df6
84afce5
bfdb3d5
63d5fb3
122280e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,13 +22,28 @@ const userIDKey contextKey = "user_id" | |
| // It properly parses repository names (which can contain slashes) from /v2/ paths. | ||
| var registryRouter = v2.Router() | ||
|
|
||
| // RepoPermission defines access permissions for a specific repository. | ||
| // This mirrors the type in lib/builds/registry_token.go to avoid circular imports. | ||
| type RepoPermission struct { | ||
| Repo string `json:"repo"` | ||
| Scope string `json:"scope"` | ||
| } | ||
|
|
||
| // RegistryTokenClaims contains the claims for a scoped registry access token. | ||
| // This mirrors the type in lib/builds/registry_token.go to avoid circular imports. | ||
| type RegistryTokenClaims struct { | ||
| jwt.RegisteredClaims | ||
| BuildID string `json:"build_id"` | ||
| Repositories []string `json:"repos"` | ||
| Scope string `json:"scope"` | ||
| BuildID string `json:"build_id"` | ||
|
|
||
| // RepoAccess defines per-repository access permissions (new two-tier format) | ||
| // If present, this takes precedence over the legacy Repositories/Scope fields | ||
| RepoAccess []RepoPermission `json:"repo_access,omitempty"` | ||
|
|
||
| // Repositories is the list of allowed repository paths (legacy format) | ||
| Repositories []string `json:"repos,omitempty"` | ||
|
|
||
| // Scope is the access scope (legacy format) | ||
| Scope string `json:"scope,omitempty"` | ||
| } | ||
|
|
||
| // OapiAuthenticationFunc creates an AuthenticationFunc compatible with nethttp-middleware | ||
|
|
@@ -116,6 +131,13 @@ func OapiAuthenticationFunc(jwtSecret string) openapi3filter.AuthenticationFunc | |
| // that returns consistent error responses. | ||
| func OapiErrorHandler(w http.ResponseWriter, message string, statusCode int) { | ||
| w.Header().Set("Content-Type", "application/json") | ||
|
|
||
| // For 401 responses, include WWW-Authenticate header so Docker/BuildKit | ||
| // knows to send credentials from the Docker config | ||
| if statusCode == http.StatusUnauthorized { | ||
| w.Header().Set("WWW-Authenticate", `Basic realm="registry"`) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WWW-Authenticate header incorrectly added to all API 401sLow Severity The |
||
|
|
||
| w.WriteHeader(statusCode) | ||
|
|
||
| // Return a simple JSON error response matching our Error schema | ||
|
|
@@ -197,8 +219,11 @@ func isInternalVMRequest(r *http.Request) bool { | |
| ip = ip[:idx] | ||
| } | ||
|
|
||
| // Check if it's from the VM network (10.100.x.x or 10.102.x.x) | ||
| return strings.HasPrefix(ip, "10.100.") || strings.HasPrefix(ip, "10.102.") | ||
| // Check if it's from the VM network | ||
| // BuildKit with registry.insecure=true doesn't do WWW-Authenticate challenge-response, | ||
| // so we need IP fallback for all internal subnets until we find a way to make | ||
| // BuildKit send auth proactively | ||
| return strings.HasPrefix(ip, "10.100.") || strings.HasPrefix(ip, "10.102.") || strings.HasPrefix(ip, "172.30.") | ||
| } | ||
|
|
||
| // extractRepoFromPath extracts the repository name from a registry path. | ||
|
|
@@ -229,6 +254,7 @@ func isWriteOperation(method string) bool { | |
|
|
||
| // validateRegistryToken validates a registry-scoped JWT token and checks repository access. | ||
| // Returns the claims if valid, nil otherwise. | ||
| // Supports both new RepoAccess format and legacy Repositories/Scope format. | ||
| func validateRegistryToken(tokenString, jwtSecret, requestPath, method string) (*RegistryTokenClaims, error) { | ||
| token, err := jwt.ParseWithClaims(tokenString, &RegistryTokenClaims{}, func(token *jwt.Token) (interface{}, error) { | ||
| if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||
|
|
@@ -246,8 +272,8 @@ func validateRegistryToken(tokenString, jwtSecret, requestPath, method string) ( | |
| return nil, fmt.Errorf("invalid token") | ||
| } | ||
|
|
||
| // Check if this is a registry token (has repos claim) | ||
| if len(claims.Repositories) == 0 { | ||
| // Check if this is a registry token (has either RepoAccess or legacy repos claim) | ||
| if len(claims.RepoAccess) == 0 && len(claims.Repositories) == 0 { | ||
| return nil, fmt.Errorf("not a registry token") | ||
| } | ||
|
|
||
|
|
@@ -261,21 +287,34 @@ func validateRegistryToken(tokenString, jwtSecret, requestPath, method string) ( | |
| return nil, fmt.Errorf("could not extract repository from path") | ||
| } | ||
|
|
||
| // Check if the repository is allowed by the token | ||
| allowed := false | ||
| for _, allowedRepo := range claims.Repositories { | ||
| if allowedRepo == repo { | ||
| allowed = true | ||
| break | ||
| // Check if the repository is allowed by the token and get its scope | ||
| var repoScope string | ||
|
|
||
| // Check new RepoAccess format first | ||
| if len(claims.RepoAccess) > 0 { | ||
| for _, perm := range claims.RepoAccess { | ||
| if perm.Repo == repo { | ||
| repoScope = perm.Scope | ||
| break | ||
| } | ||
| } | ||
| } else { | ||
| // Fall back to legacy format | ||
| for _, allowedRepo := range claims.Repositories { | ||
| if allowedRepo == repo { | ||
| repoScope = claims.Scope | ||
| break | ||
| } | ||
| } | ||
| } | ||
| if !allowed { | ||
|
|
||
| if repoScope == "" { | ||
| return nil, fmt.Errorf("repository %s not allowed by token", repo) | ||
| } | ||
|
|
||
| // Check scope for write operations | ||
| if isWriteOperation(method) && claims.Scope != "push" { | ||
| return nil, fmt.Errorf("token does not allow write operations") | ||
| if isWriteOperation(method) && repoScope != "push" { | ||
| return nil, fmt.Errorf("token does not allow write operations for %s", repo) | ||
| } | ||
|
|
||
| return claims, nil | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Registry auth config may silently fail completely
Medium Severity
The
setupRegistryAuthfunction returnsnilunconditionally even if all config writes fail. The loop logs warnings and continues on each failure, but doesn't track whether at least one write succeeded. If both/home/builder/.dockerand/root/.dockerwrites fail, the function reports success despite no authentication being configured. The build then fails later with a cryptic 401 error instead of a clear "failed to write docker config" message.