From 5e0fdb3f6ecd1b763a7e2a43cd0169213638d58e Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Fri, 28 Aug 2026 14:53:19 +0500 Subject: [PATCH 1/2] registry: treat basic-auth 401 as unauthorized Client-side login (daemon down) was wrapping a 401 as a plain error, so Auth() moved on to the next endpoint. That can print Login Succeeded if the HTTP fallback happens to answer 200. Fixes #7237 Signed-off-by: Dean Chen <862469039@qq.com> --- internal/registry/auth.go | 6 ++++- internal/registry/auth_test.go | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 internal/registry/auth_test.go diff --git a/internal/registry/auth.go b/internal/registry/auth.go index e15a139cb471..1c0ccb675968 100644 --- a/internal/registry/auth.go +++ b/internal/registry/auth.go @@ -67,7 +67,11 @@ func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIE if resp.StatusCode != http.StatusOK { // TODO(dmcgowan): Attempt to further interpret result, status code and error code string - return "", fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) + err := fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) + if resp.StatusCode == http.StatusUnauthorized { + return "", unauthorizedErr{err} + } + return "", err } return credentialAuthConfig.IdentityToken, nil diff --git a/internal/registry/auth_test.go b/internal/registry/auth_test.go new file mode 100644 index 000000000000..4b7f1a8d5bf3 --- /dev/null +++ b/internal/registry/auth_test.go @@ -0,0 +1,40 @@ +package registry + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/containerd/errdefs" + "github.com/moby/moby/api/types/registry" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestLoginV2BasicAuthUnauthorized(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || user != "alice" || pass != "secret" { + w.Header().Set("WWW-Authenticate", `Basic realm="test"`) + http.Error(w, "401 Unauthorized", http.StatusUnauthorized) + return + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + + u, err := url.Parse(srv.URL) + assert.NilError(t, err) + endpoint := APIEndpoint{URL: u} + ctx := context.Background() + + _, err = loginV2(ctx, ®istry.AuthConfig{Username: "alice", Password: "wrong"}, endpoint, "docker-test") + assert.ErrorContains(t, err, "401") + assert.Check(t, errdefs.IsUnauthorized(err)) + + token, err := loginV2(ctx, ®istry.AuthConfig{Username: "alice", Password: "secret"}, endpoint, "docker-test") + assert.NilError(t, err) + assert.Check(t, is.Equal("", token)) +} From bd8d076e5eb5375237410741a6c9397e261b56db Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Sat, 29 Aug 2026 02:40:37 +0500 Subject: [PATCH 2/2] registry: map login HTTP status through errhttp.ToNative translateV2AuthError only unwraps a url.Error from Do(); a 401 status never went through it. Use errhttp.ToNative so Auth() treats that 401 as unauthorized and stops. Signed-off-by: Dean Chen <862469039@qq.com> --- internal/registry/auth.go | 7 ++----- internal/registry/auth_test.go | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/registry/auth.go b/internal/registry/auth.go index 1c0ccb675968..4844710e6646 100644 --- a/internal/registry/auth.go +++ b/internal/registry/auth.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/containerd/errdefs/pkg/errhttp" "github.com/containerd/log" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth/challenge" @@ -67,11 +68,7 @@ func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIE if resp.StatusCode != http.StatusOK { // TODO(dmcgowan): Attempt to further interpret result, status code and error code string - err := fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) - if resp.StatusCode == http.StatusUnauthorized { - return "", unauthorizedErr{err} - } - return "", err + return "", fmt.Errorf("login attempt to %s failed with status: %d %s: %w", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode), errhttp.ToNative(resp.StatusCode)) } return credentialAuthConfig.IdentityToken, nil diff --git a/internal/registry/auth_test.go b/internal/registry/auth_test.go index 4b7f1a8d5bf3..44632df42e39 100644 --- a/internal/registry/auth_test.go +++ b/internal/registry/auth_test.go @@ -33,6 +33,7 @@ func TestLoginV2BasicAuthUnauthorized(t *testing.T) { _, err = loginV2(ctx, ®istry.AuthConfig{Username: "alice", Password: "wrong"}, endpoint, "docker-test") assert.ErrorContains(t, err, "401") assert.Check(t, errdefs.IsUnauthorized(err)) + assert.Check(t, is.ErrorType(err, errdefs.IsUnauthorized)) token, err := loginV2(ctx, ®istry.AuthConfig{Username: "alice", Password: "secret"}, endpoint, "docker-test") assert.NilError(t, err)