@@ -30,6 +30,7 @@ const ChallengeHeader = "WWW-Authenticate"
30
30
const (
31
31
DockerRegistryDomain = "docker.io"
32
32
DockerRegistryHost = "index.docker.io"
33
+ LSCRRegistry = "lscr.io"
33
34
)
34
35
35
36
// Constants for HTTP client configuration.
@@ -229,20 +230,23 @@ func extractChallengeHost(realm string, fields logrus.Fields) string {
229
230
// - container: Container with image info.
230
231
// - registryAuth: Base64-encoded auth string.
231
232
// - client: Client for HTTP requests.
233
+ // - redirected: True if the challenge request was redirected.
232
234
// - fields: Logging fields for context.
233
235
//
234
236
// Returns:
235
237
// - string: Bearer token header (e.g., "Bearer ...").
236
238
// - string: Challenge host (e.g., "ghcr.io").
239
+ // - bool: Redirect flag (passed through).
237
240
// - error: Non-nil if processing fails, nil on success.
238
241
func handleBearerAuth (
239
242
ctx context.Context ,
240
243
wwwAuthHeader string ,
241
244
container types.Container ,
242
245
registryAuth string ,
243
246
client Client ,
247
+ redirected bool ,
244
248
fields logrus.Fields ,
245
- ) (string , string , error ) {
249
+ ) (string , string , bool , error ) {
246
250
logrus .WithFields (fields ).Debug ("Entering Bearer auth path" )
247
251
248
252
var challengeHost string
@@ -276,7 +280,7 @@ func handleBearerAuth(
276
280
if err != nil {
277
281
logrus .WithError (err ).WithFields (fields ).Debug ("Failed to parse image name" )
278
282
279
- return "" , "" , fmt .Errorf ("%w: %w" , errFailedParseImageName , err )
283
+ return "" , "" , redirected , fmt .Errorf ("%w: %w" , errFailedParseImageName , err )
280
284
}
281
285
282
286
token , err := GetBearerHeader (
@@ -289,21 +293,24 @@ func handleBearerAuth(
289
293
if err != nil {
290
294
logrus .WithError (err ).WithFields (fields ).Debug ("Failed to get bearer token" )
291
295
292
- return "" , "" , fmt .Errorf ("%w: %w" , errFailedDecodeResponse , err )
296
+ return "" , "" , redirected , fmt .Errorf ("%w: %w" , errFailedDecodeResponse , err )
293
297
}
294
298
295
299
if token == "" {
296
300
logrus .WithFields (fields ).Debug ("Empty bearer token received" )
297
301
298
- return "" , "" , fmt .Errorf ("%w: empty token in response" , errFailedDecodeResponse )
302
+ return "" , "" , redirected , fmt .Errorf (
303
+ "%w: empty token in response" ,
304
+ errFailedDecodeResponse ,
305
+ )
299
306
}
300
307
301
308
logrus .WithFields (fields ).
302
309
WithField ("token_present" , token != "" ).
303
310
WithField ("challenge_host" , challengeHost ).
304
311
Debug ("Returning Bearer token and challenge host" )
305
312
306
- return token , challengeHost , nil
313
+ return token , challengeHost , redirected , nil
307
314
}
308
315
309
316
// GetToken fetches a token and the challenge host for the registry hosting the provided image.
@@ -317,13 +324,14 @@ func handleBearerAuth(
317
324
// Returns:
318
325
// - string: Authentication token (e.g., "Basic ..." or "Bearer ...").
319
326
// - string: Challenge host (e.g., "ghcr.io"), empty if not applicable.
327
+ // - bool: True if the challenge request was redirected, false otherwise.
320
328
// - error: Non-nil if operation fails, nil on success.
321
329
func GetToken (
322
330
ctx context.Context ,
323
331
container types.Container ,
324
332
registryAuth string ,
325
333
client Client ,
326
- ) (string , string , error ) {
334
+ ) (string , string , bool , error ) {
327
335
fields := logrus.Fields {
328
336
"image" : container .ImageName (),
329
337
}
@@ -333,7 +341,7 @@ func GetToken(
333
341
if err != nil {
334
342
logrus .WithError (err ).WithFields (fields ).Debug ("Failed to parse image name" )
335
343
336
- return "" , "" , fmt .Errorf ("%w: %w" , errFailedParseImageName , err )
344
+ return "" , "" , false , fmt .Errorf ("%w: %w" , errFailedParseImageName , err )
337
345
}
338
346
339
347
// Generate the challenge URL.
@@ -347,7 +355,7 @@ func GetToken(
347
355
if err != nil {
348
356
logrus .WithError (err ).WithFields (fields ).Debug ("Failed to create challenge request" )
349
357
350
- return "" , "" , fmt .Errorf ("%w: %w" , errFailedCreateChallengeRequest , err )
358
+ return "" , "" , false , fmt .Errorf ("%w: %w" , errFailedCreateChallengeRequest , err )
351
359
}
352
360
353
361
res , err := client .Do (req )
@@ -357,17 +365,20 @@ func GetToken(
357
365
WithField ("url" , challengeURL .String ()).
358
366
Debug ("Failed to execute challenge request" )
359
367
360
- return "" , "" , fmt .Errorf ("%w: %w" , errFailedExecuteChallengeRequest , err )
368
+ return "" , "" , false , fmt .Errorf ("%w: %w" , errFailedExecuteChallengeRequest , err )
361
369
}
362
370
defer res .Body .Close ()
363
371
372
+ // Detect if the request was redirected.
373
+ redirected := res .Request .URL .Host != challengeURL .Host
374
+
364
375
// Handle 200 OK response (no auth required).
365
376
if res .StatusCode == http .StatusOK {
366
377
logrus .WithFields (fields ).
367
378
WithField ("url" , challengeURL .String ()).
368
379
Debug ("No authentication required (200 OK)" )
369
380
370
- return "" , "" , nil
381
+ return "" , "" , redirected , nil
371
382
}
372
383
373
384
// Extract the challenge header.
@@ -383,7 +394,7 @@ func GetToken(
383
394
WithField ("url" , challengeURL .String ()).
384
395
Debug ("Empty WWW-Authenticate header; assuming no authentication required" )
385
396
386
- return "" , "" , nil
397
+ return "" , "" , redirected , nil
387
398
}
388
399
389
400
// Normalize challenge for comparison.
@@ -395,25 +406,33 @@ func GetToken(
395
406
if registryAuth == "" {
396
407
logrus .WithFields (fields ).Debug ("No credentials provided for Basic auth" )
397
408
398
- return "" , "" , fmt .Errorf ("%w: basic auth required" , errNoCredentials )
409
+ return "" , "" , redirected , fmt .Errorf ("%w: basic auth required" , errNoCredentials )
399
410
}
400
411
401
412
logrus .WithFields (fields ).Debug ("Using Basic auth" )
402
413
403
- return "Basic " + registryAuth , "" , nil
414
+ return "Basic " + registryAuth , "" , redirected , nil
404
415
}
405
416
406
417
// Handle Bearer auth.
407
418
if strings .HasPrefix (challenge , "bearer" ) {
408
- return handleBearerAuth (ctx , wwwAuthHeader , container , registryAuth , client , fields )
419
+ return handleBearerAuth (
420
+ ctx ,
421
+ wwwAuthHeader ,
422
+ container ,
423
+ registryAuth ,
424
+ client ,
425
+ redirected ,
426
+ fields ,
427
+ )
409
428
}
410
429
411
430
// Handle unknown challenge types.
412
431
logrus .WithFields (fields ).
413
432
WithField ("challenge" , challenge ).
414
433
Error ("Unsupported challenge type from registry" )
415
434
416
- return "" , "" , fmt .Errorf ("%w: %s" , errUnsupportedChallenge , challenge )
435
+ return "" , "" , redirected , fmt .Errorf ("%w: %s" , errUnsupportedChallenge , challenge )
417
436
}
418
437
419
438
// processChallenge parses the WWW-Authenticate header to extract authentication details.
@@ -631,10 +650,36 @@ func GetAuthURL(challenge string, imageRef reference.Named) (*url.URL, error) {
631
650
}
632
651
}
633
652
653
+ // Get registry address for special handling.
654
+ registryAddress , err := GetRegistryAddress (imageRef .Name ())
655
+ if err != nil {
656
+ logrus .WithError (err ).
657
+ WithField ("image" , imageRef .Name ()).
658
+ Debug ("Failed to get registry address" )
659
+ }
660
+
661
+ // Special handling for lscr.io registry authentication:
662
+ // lscr.io (LinuxServer.io) images are hosted on GitHub Container Registry (ghcr.io),
663
+ // but the authentication challenge from lscr.io redirects to ghcr.io. When we receive
664
+ // the WWW-Authenticate header from ghcr.io, it contains the ghcr.io realm URL.
665
+ // However, for lscr.io images, we need to ensure the realm points to ghcr.io's token endpoint
666
+ // to get the correct authentication tokens. This prevents authentication failures
667
+ // that would occur if we tried to use lscr.io's non-existent token endpoint.
668
+ //
669
+ // Without this override, lscr.io authentication would fail because:
670
+ // 1. Challenge request to lscr.io/v2/ redirects to ghcr.io/v2/
671
+ // 2. ghcr.io returns WWW-Authenticate header with ghcr.io realm
672
+ // 3. But sometimes the realm might not be correctly set, causing token requests to fail
673
+ // 4. This ensures we always use the correct ghcr.io token endpoint for lscr.io images
674
+ if registryAddress == LSCRRegistry {
675
+ values ["realm" ] = "https://ghcr.io/token"
676
+ }
677
+
634
678
logrus .WithFields (logrus.Fields {
635
679
"image" : imageRef .Name (),
636
680
"realm" : values ["realm" ],
637
681
"service" : values ["service" ],
682
+ "scope" : values ["scope" ],
638
683
}).Debug ("Parsed challenge header" )
639
684
640
685
// Validate required fields.
@@ -748,7 +793,7 @@ func TransformAuth(registryAuth string) string {
748
793
return registryAuth // Return original if no valid credentials.
749
794
}
750
795
751
- // GetChallengeURL generates a challenge URL for accessing an image’ s registry.
796
+ // GetChallengeURL generates a challenge URL for accessing an image' s registry.
752
797
//
753
798
// Parameters:
754
799
// - imageRef: Normalized image reference.
@@ -759,6 +804,12 @@ func GetChallengeURL(imageRef reference.Named) url.URL {
759
804
// Extract registry host from the image reference.
760
805
host , _ := GetRegistryAddress (imageRef .Name ())
761
806
807
+ // Special handling for lscr.io registry: use ghcr.io for challenge URL
808
+ // to get the correct WWW-Authenticate header with proper scope.
809
+ if host == "lscr.io" {
810
+ host = "ghcr.io"
811
+ }
812
+
762
813
scheme := "https"
763
814
if viper .GetBool ("WATCHTOWER_REGISTRY_TLS_SKIP" ) {
764
815
scheme = "http"
0 commit comments