Skip to content

Commit 1e41ae9

Browse files
fix(registry): digest retrieval failed, falling back to full pull (#763)
- add redirect detection to GetToken and handleBearerAuth functions - add special handling for lscr.io registry authentication and manifest requests - add LSCRRegistry constant for lscr.io identification - refactor buildManifestURL to accept explicit scheme parameter - add handleManifestResponse and makeManifestRequest utility functions - update tests to handle new function signatures and add redirect test cases - remove viper dependency from BuildManifestURL function Fixes #761
1 parent c7c7760 commit 1e41ae9

File tree

6 files changed

+1285
-459
lines changed

6 files changed

+1285
-459
lines changed

pkg/registry/auth/auth.go

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const ChallengeHeader = "WWW-Authenticate"
3030
const (
3131
DockerRegistryDomain = "docker.io"
3232
DockerRegistryHost = "index.docker.io"
33+
LSCRRegistry = "lscr.io"
3334
)
3435

3536
// Constants for HTTP client configuration.
@@ -229,20 +230,23 @@ func extractChallengeHost(realm string, fields logrus.Fields) string {
229230
// - container: Container with image info.
230231
// - registryAuth: Base64-encoded auth string.
231232
// - client: Client for HTTP requests.
233+
// - redirected: True if the challenge request was redirected.
232234
// - fields: Logging fields for context.
233235
//
234236
// Returns:
235237
// - string: Bearer token header (e.g., "Bearer ...").
236238
// - string: Challenge host (e.g., "ghcr.io").
239+
// - bool: Redirect flag (passed through).
237240
// - error: Non-nil if processing fails, nil on success.
238241
func handleBearerAuth(
239242
ctx context.Context,
240243
wwwAuthHeader string,
241244
container types.Container,
242245
registryAuth string,
243246
client Client,
247+
redirected bool,
244248
fields logrus.Fields,
245-
) (string, string, error) {
249+
) (string, string, bool, error) {
246250
logrus.WithFields(fields).Debug("Entering Bearer auth path")
247251

248252
var challengeHost string
@@ -276,7 +280,7 @@ func handleBearerAuth(
276280
if err != nil {
277281
logrus.WithError(err).WithFields(fields).Debug("Failed to parse image name")
278282

279-
return "", "", fmt.Errorf("%w: %w", errFailedParseImageName, err)
283+
return "", "", redirected, fmt.Errorf("%w: %w", errFailedParseImageName, err)
280284
}
281285

282286
token, err := GetBearerHeader(
@@ -289,21 +293,24 @@ func handleBearerAuth(
289293
if err != nil {
290294
logrus.WithError(err).WithFields(fields).Debug("Failed to get bearer token")
291295

292-
return "", "", fmt.Errorf("%w: %w", errFailedDecodeResponse, err)
296+
return "", "", redirected, fmt.Errorf("%w: %w", errFailedDecodeResponse, err)
293297
}
294298

295299
if token == "" {
296300
logrus.WithFields(fields).Debug("Empty bearer token received")
297301

298-
return "", "", fmt.Errorf("%w: empty token in response", errFailedDecodeResponse)
302+
return "", "", redirected, fmt.Errorf(
303+
"%w: empty token in response",
304+
errFailedDecodeResponse,
305+
)
299306
}
300307

301308
logrus.WithFields(fields).
302309
WithField("token_present", token != "").
303310
WithField("challenge_host", challengeHost).
304311
Debug("Returning Bearer token and challenge host")
305312

306-
return token, challengeHost, nil
313+
return token, challengeHost, redirected, nil
307314
}
308315

309316
// GetToken fetches a token and the challenge host for the registry hosting the provided image.
@@ -317,13 +324,14 @@ func handleBearerAuth(
317324
// Returns:
318325
// - string: Authentication token (e.g., "Basic ..." or "Bearer ...").
319326
// - string: Challenge host (e.g., "ghcr.io"), empty if not applicable.
327+
// - bool: True if the challenge request was redirected, false otherwise.
320328
// - error: Non-nil if operation fails, nil on success.
321329
func GetToken(
322330
ctx context.Context,
323331
container types.Container,
324332
registryAuth string,
325333
client Client,
326-
) (string, string, error) {
334+
) (string, string, bool, error) {
327335
fields := logrus.Fields{
328336
"image": container.ImageName(),
329337
}
@@ -333,7 +341,7 @@ func GetToken(
333341
if err != nil {
334342
logrus.WithError(err).WithFields(fields).Debug("Failed to parse image name")
335343

336-
return "", "", fmt.Errorf("%w: %w", errFailedParseImageName, err)
344+
return "", "", false, fmt.Errorf("%w: %w", errFailedParseImageName, err)
337345
}
338346

339347
// Generate the challenge URL.
@@ -347,7 +355,7 @@ func GetToken(
347355
if err != nil {
348356
logrus.WithError(err).WithFields(fields).Debug("Failed to create challenge request")
349357

350-
return "", "", fmt.Errorf("%w: %w", errFailedCreateChallengeRequest, err)
358+
return "", "", false, fmt.Errorf("%w: %w", errFailedCreateChallengeRequest, err)
351359
}
352360

353361
res, err := client.Do(req)
@@ -357,17 +365,20 @@ func GetToken(
357365
WithField("url", challengeURL.String()).
358366
Debug("Failed to execute challenge request")
359367

360-
return "", "", fmt.Errorf("%w: %w", errFailedExecuteChallengeRequest, err)
368+
return "", "", false, fmt.Errorf("%w: %w", errFailedExecuteChallengeRequest, err)
361369
}
362370
defer res.Body.Close()
363371

372+
// Detect if the request was redirected.
373+
redirected := res.Request.URL.Host != challengeURL.Host
374+
364375
// Handle 200 OK response (no auth required).
365376
if res.StatusCode == http.StatusOK {
366377
logrus.WithFields(fields).
367378
WithField("url", challengeURL.String()).
368379
Debug("No authentication required (200 OK)")
369380

370-
return "", "", nil
381+
return "", "", redirected, nil
371382
}
372383

373384
// Extract the challenge header.
@@ -383,7 +394,7 @@ func GetToken(
383394
WithField("url", challengeURL.String()).
384395
Debug("Empty WWW-Authenticate header; assuming no authentication required")
385396

386-
return "", "", nil
397+
return "", "", redirected, nil
387398
}
388399

389400
// Normalize challenge for comparison.
@@ -395,25 +406,33 @@ func GetToken(
395406
if registryAuth == "" {
396407
logrus.WithFields(fields).Debug("No credentials provided for Basic auth")
397408

398-
return "", "", fmt.Errorf("%w: basic auth required", errNoCredentials)
409+
return "", "", redirected, fmt.Errorf("%w: basic auth required", errNoCredentials)
399410
}
400411

401412
logrus.WithFields(fields).Debug("Using Basic auth")
402413

403-
return "Basic " + registryAuth, "", nil
414+
return "Basic " + registryAuth, "", redirected, nil
404415
}
405416

406417
// Handle Bearer auth.
407418
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+
)
409428
}
410429

411430
// Handle unknown challenge types.
412431
logrus.WithFields(fields).
413432
WithField("challenge", challenge).
414433
Error("Unsupported challenge type from registry")
415434

416-
return "", "", fmt.Errorf("%w: %s", errUnsupportedChallenge, challenge)
435+
return "", "", redirected, fmt.Errorf("%w: %s", errUnsupportedChallenge, challenge)
417436
}
418437

419438
// processChallenge parses the WWW-Authenticate header to extract authentication details.
@@ -631,10 +650,36 @@ func GetAuthURL(challenge string, imageRef reference.Named) (*url.URL, error) {
631650
}
632651
}
633652

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+
634678
logrus.WithFields(logrus.Fields{
635679
"image": imageRef.Name(),
636680
"realm": values["realm"],
637681
"service": values["service"],
682+
"scope": values["scope"],
638683
}).Debug("Parsed challenge header")
639684

640685
// Validate required fields.
@@ -748,7 +793,7 @@ func TransformAuth(registryAuth string) string {
748793
return registryAuth // Return original if no valid credentials.
749794
}
750795

751-
// GetChallengeURL generates a challenge URL for accessing an images registry.
796+
// GetChallengeURL generates a challenge URL for accessing an image's registry.
752797
//
753798
// Parameters:
754799
// - imageRef: Normalized image reference.
@@ -759,6 +804,12 @@ func GetChallengeURL(imageRef reference.Named) url.URL {
759804
// Extract registry host from the image reference.
760805
host, _ := GetRegistryAddress(imageRef.Name())
761806

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+
762813
scheme := "https"
763814
if viper.GetBool("WATCHTOWER_REGISTRY_TLS_SKIP") {
764815
scheme = "http"

0 commit comments

Comments
 (0)