detached 06011c0

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
06011c002f5fe1b5d5e096db6b57dae065f725a8
author
brice · · 2026-01-14 06:23 UTC
parent
a340e3d
signature
unsigned
2 files · +50 -15
M internal/adapters/in/http/registry/token.go +45 -14
@@ -19,17 +19,25 @@ type TokenResponse struct {
1919 IssuedAt string `json:"issued_at,omitempty"`
2020 }
2121
22+// InternalAuth holds credentials for internal loopback registry access.
23+type InternalAuth struct {
24+ Username string
25+ Password string
26+}
27+
2228 // TokenHandler handles token server requests for Docker Registry authentication.
2329 type TokenHandler struct {
24- authSvc in.AuthService
25- log zerowrap.Logger
30+ authSvc in.AuthService
31+ internalAuth InternalAuth
32+ log zerowrap.Logger
2633 }
2734
2835 // 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 {
3037 return &TokenHandler{
31- authSvc: authSvc,
32- log: log,
38+ authSvc: authSvc,
39+ internalAuth: internalAuth,
40+ log: log,
3341 }
3442 }
3543
@@ -70,16 +78,22 @@ func (h *TokenHandler) handleToken(w http.ResponseWriter, r *http.Request) {
7078 return
7179 }
7280
73- // Validate credentials based on auth type
81+ // Check for internal registry auth (localhost requests with internal creds)
7482 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+ }
8397 }
8498 }
8599
@@ -137,3 +151,20 @@ func (h *TokenHandler) sendAnonymousToken(w http.ResponseWriter, log zerowrap.Lo
137151
138152 log.Debug().Msg("anonymous token issued")
139153 }
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.AuthService30+ authSvc in.AuthService
25- log zerowrap.Logger31+ 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 return78 return
71 }79 }
72 80
73- // Validate credentials based on auth type81+ // Check for internal registry auth (localhost requests with internal creds)
74 var authenticated bool82 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 JWT87+ // 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 = true90+ 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
260260
261261 // Create token handler for registry token endpoint
262262 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)
264268 }
265269
266270 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 endpoint261 // 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, nil270 return svc, nil

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close