diff --git a/go.mod b/go.mod index 83102d224..8ca9ab85e 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( k8s.io/client-go v0.35.1 k8s.io/klog/v2 v2.140.0 k8s.io/kubectl v0.35.1 - oras.land/oras-go/v2 v2.6.0 + oras.land/oras-go/v2 v2.6.1 sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index 136675e29..7bab59d58 100644 --- a/go.sum +++ b/go.sum @@ -523,8 +523,8 @@ k8s.io/kubectl v0.35.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg= k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= -oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs= +oras.land/oras-go/v2 v2.6.1/go.mod h1:dhtFrFOuZuDtAVeZ9FUnaa5zfzplG3ZnFX9/uH1J/Yk= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= diff --git a/pkg/registry/client.go b/pkg/registry/client.go index b14b767b2..03ff2c9a9 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -296,15 +296,22 @@ func (c *Client) Login(host string, options ...LoginOption) error { } reg.PlainHTTP = c.plainHTTP cred := auth.Credential{Username: c.username, Password: c.password} - c.authorizer.ForceAttemptOAuth2 = true reg.Client = c.authorizer ctx := context.Background() - if err := reg.Ping(ctx); err != nil { - c.authorizer.ForceAttemptOAuth2 = false - if err := reg.Ping(ctx); err != nil { - return fmt.Errorf("authenticating to %q: %w", host, err) - } + err = c.ping(ctx, reg) + if err != nil && !reg.PlainHTTP && c.forcedHTTP() { + // The registry is plain HTTP: the fallback transport downgraded the + // connection from https to http. ORAS v2.6.1+ refuses to forward + // credentials across that implicit scheme change (GHSA-vh4v-2xq2-g5cg), + // so the credentialed ping above fails. Now that the fallback has been + // detected, set PlainHTTP explicitly and retry so requests are built as + // http from the start and the scheme no longer changes mid-request. + reg.PlainHTTP = true + err = c.ping(ctx, reg) + } + if err != nil { + return fmt.Errorf("authenticating to %q: %w", host, err) } // The credentialsStore loader does not handle empty files. So, there is a workaround. @@ -341,6 +348,30 @@ func (c *Client) Login(host string, options ...LoginOption) error { return nil } +// ping authenticates against the registry, first attempting the OAuth2 token +// flow and falling back to the basic/refresh token flow on failure. +func (c *Client) ping(ctx context.Context, reg *remote.Registry) error { + c.authorizer.ForceAttemptOAuth2 = true + err := reg.Ping(ctx) + if err != nil { + c.authorizer.ForceAttemptOAuth2 = false + err = reg.Ping(ctx) + } + return err +} + +// forcedHTTP reports whether the client's transport has fallen back to plain +// HTTP after a failed HTTPS attempt, indicating the registry is plain HTTP. +func (c *Client) forcedHTTP() bool { + if c.httpClient == nil { + return false + } + if ft, ok := c.httpClient.Transport.(*fallbackTransport); ok { + return ft.forcedHTTP() + } + return false +} + // LoginOptBasicAuth returns a function that sets the username/password settings on login func LoginOptBasicAuth(username string, password string) LoginOption { return func(o *loginOperation) { diff --git a/pkg/registry/fallback.go b/pkg/registry/fallback.go index 1db729576..a5ca6f72c 100644 --- a/pkg/registry/fallback.go +++ b/pkg/registry/fallback.go @@ -38,6 +38,14 @@ func newTransport(debug bool) *fallbackTransport { } } +// forcedHTTP reports whether the transport has fallen back to plain HTTP after +// a failed HTTPS attempt. Once this is true, the registry is known to be plain +// HTTP and callers should set PlainHTTP so requests are built as http from the +// start (see Client.Login). +func (t *fallbackTransport) forcedHTTP() bool { + return t.forceHTTP.Load() +} + // RoundTrip wraps base round trip with conditional insecure retry. func (t *fallbackTransport) RoundTrip(req *http.Request) (*http.Response, error) { if ok := t.forceHTTP.Load(); ok {