fix(registry): add internal auth for loopback registry pulls
- Add InternalAuth struct for internal registry credentials - Check for internal auth on localhost requests before external auth - Pass internal credentials to token handler from app startup
- commit
- b59f5466d7d8d7dd14951ae52ff7b0ecd45b4786
- author
- brice · · 2026-01-14 06:23 UTC
- parent
- 9bf7d6c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +50 -15
M internal/adapters/in/http/registry/token.go +45 -14
| @@ -19,17 +19,25 @@ type TokenResponse struct { | |||
| 19 | 19 | IssuedAt string `json:"issued_at,omitempty"` | |
| 20 | 20 | } | |
| 21 | 21 | ||
| 22 | + | // InternalAuth holds credentials for internal loopback registry access. | |
| 23 | + | type InternalAuth struct { | |
| 24 | + | Username string | |
| 25 | + | Password string | |
| 26 | + | } | |
| 27 | + | ||
| 22 | 28 | // TokenHandler handles token server requests for Docker Registry authentication. | |
| 23 | 29 | type TokenHandler struct { | |
| 24 | - | authSvc in.AuthService | |
| 25 | - | log zerowrap.Logger | |
| 30 | + | authSvc in.AuthService | |
| 31 | + | internalAuth InternalAuth | |
| 32 | + | log zerowrap.Logger | |
| 26 | 33 | } | |
| 27 | 34 | ||
| 28 | 35 | // NewTokenHandler creates a new token handler. | |
| 29 | - | func NewTokenHandler(authSvc in.AuthService, log zerowrap.Logger) *TokenHandler { | |
| 36 | + | func NewTokenHandler(authSvc in.AuthService, internalAuth InternalAuth, log zerowrap.Logger) *TokenHandler { | |
| 30 | 37 | return &TokenHandler{ | |
| 31 | - | authSvc: authSvc, | |
| 32 | - | log: log, | |
| 38 | + | authSvc: authSvc, | |
| 39 | + | internalAuth: internalAuth, | |
| 40 | + | log: log, | |
| 33 | 41 | } | |
| 34 | 42 | } | |
| 35 | 43 | ||
| @@ -70,16 +78,22 @@ func (h *TokenHandler) handleToken(w http.ResponseWriter, r *http.Request) { | |||
| 70 | 78 | return | |
| 71 | 79 | } | |
| 72 | 80 | ||
| 73 | - | // Validate credentials based on auth type | |
| 81 | + | // Check for internal registry auth (localhost requests with internal creds) | |
| 74 | 82 | var authenticated bool | |
| 75 | - | switch h.authSvc.GetAuthType() { | |
| 76 | - | case domain.AuthTypePassword: | |
| 77 | - | authenticated = h.authSvc.ValidatePassword(ctx, username, password) | |
| 78 | - | case domain.AuthTypeToken: | |
| 79 | - | // For token auth, the password might be an existing JWT | |
| 80 | - | claims, err := h.authSvc.ValidateToken(ctx, password) | |
| 81 | - | if err == nil && claims.Subject == username { | |
| 82 | - | authenticated = true | |
| 83 | + | if isLocalhostRequest(r) && h.isInternalAuth(username, password) { | |
| 84 | + | authenticated = true | |
| 85 | + | log.Debug().Str("username", username).Msg("internal registry auth accepted") | |
| 86 | + | } else { | |
| 87 | + | // Validate credentials based on auth type | |
| 88 | + | switch h.authSvc.GetAuthType() { | |
| 89 | + | case domain.AuthTypePassword: | |
| 90 | + | authenticated = h.authSvc.ValidatePassword(ctx, username, password) | |
| 91 | + | case domain.AuthTypeToken: | |
| 92 | + | // For token auth, the password might be an existing JWT | |
| 93 | + | claims, err := h.authSvc.ValidateToken(ctx, password) | |
| 94 | + | if err == nil && claims.Subject == username { | |
| 95 | + | authenticated = true | |
| 96 | + | } | |
| 83 | 97 | } | |
| 84 | 98 | } | |
| 85 | 99 | ||
| @@ -137,3 +151,20 @@ func (h *TokenHandler) sendAnonymousToken(w http.ResponseWriter, log zerowrap.Lo | |||
| 137 | 151 | ||
| 138 | 152 | log.Debug().Msg("anonymous token issued") | |
| 139 | 153 | } | |
| 154 | + | ||
| 155 | + | // isLocalhostRequest checks if the request is from localhost. | |
| 156 | + | func isLocalhostRequest(r *http.Request) bool { | |
| 157 | + | host := r.Host | |
| 158 | + | if host == "" { | |
| 159 | + | host = r.URL.Host | |
| 160 | + | } | |
| 161 | + | return host == "localhost:5000" || host == "127.0.0.1:5000" || host == "[::1]:5000" | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // isInternalAuth checks if the credentials match internal registry auth. | |
| 165 | + | func (h *TokenHandler) isInternalAuth(username, password string) bool { | |
| 166 | + | if h.internalAuth.Username == "" || h.internalAuth.Password == "" { | |
| 167 | + | return false | |
| 168 | + | } | |
| 169 | + | return username == h.internalAuth.Username && password == h.internalAuth.Password | |
| 170 | + | } | |
| @@ -19,17 +19,25 @@ type TokenResponse struct { | |||||
| 19 | IssuedAt string `json:"issued_at,omitempty"` | 19 | IssuedAt string `json:"issued_at,omitempty"` | ||
| 20 | } | 20 | } | ||
| 21 | 21 | ||||
| 22 | + | // InternalAuth holds credentials for internal loopback registry access. | |||
| 23 | + | type InternalAuth struct { | |||
| 24 | + | Username string | |||
| 25 | + | Password string | |||
| 26 | + | } | |||
| 27 | + | ||||
| 22 | // TokenHandler handles token server requests for Docker Registry authentication. | 28 | // TokenHandler handles token server requests for Docker Registry authentication. | ||
| 23 | type TokenHandler struct { | 29 | type TokenHandler struct { | ||
| 24 | - | authSvc in.AuthService | 30 | + | authSvc in.AuthService |
| 25 | - | log zerowrap.Logger | 31 | + | internalAuth InternalAuth |
| 32 | + | log zerowrap.Logger | |||
| 26 | } | 33 | } | ||
| 27 | 34 | ||||
| 28 | // NewTokenHandler creates a new token handler. | 35 | // NewTokenHandler creates a new token handler. | ||
| 29 | - | func NewTokenHandler(authSvc in.AuthService, log zerowrap.Logger) *TokenHandler { | 36 | + | func NewTokenHandler(authSvc in.AuthService, internalAuth InternalAuth, log zerowrap.Logger) *TokenHandler { |
| 30 | return &TokenHandler{ | 37 | return &TokenHandler{ | ||
| 31 | - | authSvc: authSvc, | 38 | + | authSvc: authSvc, |
| 32 | - | log: log, | 39 | + | internalAuth: internalAuth, |
| 40 | + | log: log, | |||
| 33 | } | 41 | } | ||
| 34 | } | 42 | } | ||
| 35 | 43 | ||||
| @@ -70,16 +78,22 @@ func (h *TokenHandler) handleToken(w http.ResponseWriter, r *http.Request) { | |||||
| 70 | return | 78 | return | ||
| 71 | } | 79 | } | ||
| 72 | 80 | ||||
| 73 | - | // Validate credentials based on auth type | 81 | + | // Check for internal registry auth (localhost requests with internal creds) |
| 74 | var authenticated bool | 82 | var authenticated bool | ||
| 75 | - | switch h.authSvc.GetAuthType() { | 83 | + | if isLocalhostRequest(r) && h.isInternalAuth(username, password) { |
| 76 | - | case domain.AuthTypePassword: | 84 | + | authenticated = true |
| 77 | - | authenticated = h.authSvc.ValidatePassword(ctx, username, password) | 85 | + | log.Debug().Str("username", username).Msg("internal registry auth accepted") |
| 78 | - | case domain.AuthTypeToken: | 86 | + | } else { |
| 79 | - | // For token auth, the password might be an existing JWT | 87 | + | // Validate credentials based on auth type |
| 80 | - | claims, err := h.authSvc.ValidateToken(ctx, password) | 88 | + | switch h.authSvc.GetAuthType() { |
| 81 | - | if err == nil && claims.Subject == username { | 89 | + | case domain.AuthTypePassword: |
| 82 | - | authenticated = true | 90 | + | authenticated = h.authSvc.ValidatePassword(ctx, username, password) |
| 91 | + | case domain.AuthTypeToken: | |||
| 92 | + | // For token auth, the password might be an existing JWT | |||
| 93 | + | claims, err := h.authSvc.ValidateToken(ctx, password) | |||
| 94 | + | if err == nil && claims.Subject == username { | |||
| 95 | + | authenticated = true | |||
| 96 | + | } | |||
| 83 | } | 97 | } | ||
| 84 | } | 98 | } | ||
| 85 | 99 | ||||
| @@ -137,3 +151,20 @@ func (h *TokenHandler) sendAnonymousToken(w http.ResponseWriter, log zerowrap.Lo | |||||
| 137 | 151 | ||||
| 138 | log.Debug().Msg("anonymous token issued") | 152 | log.Debug().Msg("anonymous token issued") | ||
| 139 | } | 153 | } | ||
| 154 | + | ||||
| 155 | + | // isLocalhostRequest checks if the request is from localhost. | |||
| 156 | + | func isLocalhostRequest(r *http.Request) bool { | |||
| 157 | + | host := r.Host | |||
| 158 | + | if host == "" { | |||
| 159 | + | host = r.URL.Host | |||
| 160 | + | } | |||
| 161 | + | return host == "localhost:5000" || host == "127.0.0.1:5000" || host == "[::1]:5000" | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | // isInternalAuth checks if the credentials match internal registry auth. | |||
| 165 | + | func (h *TokenHandler) isInternalAuth(username, password string) bool { | |||
| 166 | + | if h.internalAuth.Username == "" || h.internalAuth.Password == "" { | |||
| 167 | + | return false | |||
| 168 | + | } | |||
| 169 | + | return username == h.internalAuth.Username && password == h.internalAuth.Password | |||
| 170 | + | } | |||
M internal/app/run.go +5 -1
| @@ -260,7 +260,11 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||
| 260 | 260 | ||
| 261 | 261 | // Create token handler for registry token endpoint | |
| 262 | 262 | if svc.authSvc != nil { | |
| 263 | - | svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, log) | |
| 263 | + | internalAuth := registry.InternalAuth{ | |
| 264 | + | Username: svc.internalRegUser, | |
| 265 | + | Password: svc.internalRegPass, | |
| 266 | + | } | |
| 267 | + | svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, internalAuth, log) | |
| 264 | 268 | } | |
| 265 | 269 | ||
| 266 | 270 | return svc, nil | |
| @@ -260,7 +260,11 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||||
| 260 | 260 | ||||
| 261 | // Create token handler for registry token endpoint | 261 | // Create token handler for registry token endpoint | ||
| 262 | if svc.authSvc != nil { | 262 | if svc.authSvc != nil { | ||
| 263 | - | svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, log) | 263 | + | internalAuth := registry.InternalAuth{ |
| 264 | + | Username: svc.internalRegUser, | |||
| 265 | + | Password: svc.internalRegPass, | |||
| 266 | + | } | |||
| 267 | + | svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, internalAuth, log) | |||
| 264 | } | 268 | } | ||
| 265 | 269 | ||||
| 266 | return svc, nil | 270 | return svc, nil | ||