cli/command: use the Docker Desktop session for Docker Hub

When Docker Desktop is running and signed in, read the Docker Hub session from the Docker Desktop secrets engine, and send it as the registry credentials for Docker Hub (docker.io) and Docker Hardened Images (dhi.io) when pulling or pushing images through the daemon: docker pull, push, run, create, plugin install and push, service create and update, and stack deploy.

Credentials in DOCKER_AUTH_CONFIG take precedence for the registries they cover. Without a usable Docker Desktop session (no socket, nothing listening, signed out, or no answer within 5 seconds), the CLI uses its credential store, credential helpers, and configuration file as before. docker login and stored credentials are not changed.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
This commit is contained in:
Alano Terblanche
2026-10-22 11:21:50 +02:00
parent 0c2d0f700c
commit a46376f216
3 changed files with 248 additions and 3 deletions
+21 -3
View File
@@ -15,6 +15,7 @@ import (
"github.com/docker/cli/cli/hints"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/internal/secretsengine"
"github.com/docker/cli/internal/tui"
"github.com/moby/moby/api/pkg/authconfig"
registrytypes "github.com/moby/moby/api/types/registry"
@@ -35,6 +36,16 @@ const (
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer
const authConfigKey = "https://index.docker.io/v1/"
var desktopAuthConfig = secretsengine.AuthConfig
func envAuthConfig(registryHostname string) (configtypes.AuthConfig, bool) {
if os.Getenv(configfile.DockerEnvConfigKey) == "" {
return configtypes.AuthConfig{}, false
}
authConfig, err := configfile.New("").GetAuthConfig(registryHostname)
return authConfig, err == nil && authConfig.Username != ""
}
// ResolveAuthConfig returns auth-config for the given registry from the
// credential-store. It returns an empty AuthConfig if no credentials were
// found.
@@ -199,9 +210,16 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin
if err != nil {
return "", err
}
authConfig, err := cfg.GetAuthConfig(reference.Domain(registryRef))
if err != nil {
return "", err
domain := reference.Domain(registryRef)
authConfig, ok := envAuthConfig(domain)
if !ok {
authConfig, ok = desktopAuthConfig(domain)
}
if !ok {
authConfig, err = cfg.GetAuthConfig(domain)
if err != nil {
return "", err
}
}
return authconfig.Encode(registrytypes.AuthConfig{
+12
View File
@@ -70,6 +70,18 @@ With Docker Desktop, the credential store is already installed and configured
for you. Unless you want to change the credential store used by Docker Desktop,
you can skip the following steps.
#### Docker Desktop sign-in
When Docker Desktop is running and you are signed in, commands that pull or
push images through the Docker Engine, such as `docker pull`, `docker push`,
`docker run`, and `docker service create`, use the Docker Desktop session for
Docker Hub (`docker.io`) and Docker Hardened Images (`dhi.io`). The session is
used before the credential store, credential helpers, and the configuration
file. Credentials set in the `DOCKER_AUTH_CONFIG` environment variable take
precedence over the Docker Desktop session. If Docker Desktop is not installed
or not running, or you are not signed in, the Docker CLI uses its own
credentials.
#### Configure the credential store
You need to specify the credential store in `$HOME/.docker/config.json`
+215
View File
@@ -0,0 +1,215 @@
//go:build go1.26
package secretsengine
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"sync"
"sync/atomic"
"time"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types"
seclient "github.com/docker/secrets-engine/client"
"github.com/docker/secrets-engine/client/dockerhub"
"github.com/docker/secrets-engine/x/api"
"github.com/sirupsen/logrus"
)
const (
lookupTimeout = 5 * time.Second
sessionExpirySkew = 30 * time.Second
sessionTTL = time.Minute
retryInterval = time.Minute
indexServer = "https://index.docker.io/v1/"
dhiRegistryHost = "dhi.io"
stagingRegistryHost = "registry-1-stage.docker.io"
)
var (
errNoUsername = errors.New("no username for the session")
errSessionExpired = errors.New("the session has expired (not signed in to Docker Desktop?)")
)
type hubSessions interface {
GetDefaultSession(ctx context.Context) (dockerhub.UserSession, error)
GetDefaultProfile(ctx context.Context) (dockerhub.Profile, error)
}
var _ hubSessions = dockerhub.ClientAuth(nil)
type provider struct {
hub *session
staging *session
}
var shared = &provider{
hub: newSession("Docker Hub"),
staging: newSession("Docker Hub staging", dockerhub.Staging()),
}
var disabled atomic.Bool
func Disable() {
disabled.Store(true)
}
func AuthConfig(registryHostname string) (types.AuthConfig, bool) {
if disabled.Load() {
return types.AuthConfig{}, false
}
return shared.authConfig(registryHostname)
}
func (p *provider) authConfig(registryHostname string) (types.AuthConfig, bool) {
s, serverAddress := p.sessionFor(registryHostname)
if s == nil {
return types.AuthConfig{}, false
}
auth, ok := s.auth()
if !ok {
return types.AuthConfig{}, false
}
auth.ServerAddress = serverAddress
return auth, true
}
func (p *provider) sessionFor(registryHostname string) (*session, string) {
switch credentials.ConvertToHostname(registryHostname) {
case "docker.io", "index.docker.io", "registry-1.docker.io":
return p.hub, indexServer
case dhiRegistryHost:
return p.hub, dhiRegistryHost
case stagingRegistryHost:
return p.staging, stagingRegistryHost
default:
return nil, ""
}
}
type session struct {
name string
connect func() (hubSessions, error)
timeout time.Duration
now func() time.Time
mu sync.Mutex
hub hubSessions
retryAt time.Time
cached *types.AuthConfig
validUntil time.Time
}
func newSession(name string, opts ...dockerhub.Option) *session {
return &session{
name: name,
connect: func() (hubSessions, error) {
return connect(api.DesktopSocketPath(), opts...)
},
timeout: lookupTimeout,
now: time.Now,
}
}
func connect(socketPath string, opts ...dockerhub.Option) (hubSessions, error) {
if _, err := os.Lstat(socketPath); errors.Is(err, fs.ErrNotExist) {
return nil, err
}
c, err := seclient.New(
seclient.WithSocketPath(socketPath),
seclient.WithTimeout(lookupTimeout),
)
if err != nil {
return nil, err
}
return c.HubAuth(opts...), nil
}
func (s *session) auth() (types.AuthConfig, bool) {
s.mu.Lock()
defer s.mu.Unlock()
now := s.now()
if now.Before(s.retryAt) {
return types.AuthConfig{}, false
}
if s.cached != nil && now.Before(s.validUntil) {
return *s.cached, true
}
auth, validUntil, err := s.fetch()
if err != nil {
s.retryAt = s.now().Add(retryInterval)
s.cached = nil
logFallback(s.name, err)
return types.AuthConfig{}, false
}
logrus.Debugf("using the %s session of %q from Docker Desktop", s.name, auth.Username)
s.cached, s.validUntil = &auth, validUntil
return auth, true
}
func (s *session) fetch() (types.AuthConfig, time.Time, error) {
if s.hub == nil {
hub, err := s.connect()
if err != nil {
return types.AuthConfig{}, time.Time{}, err
}
s.hub = hub
}
ctx, cancel := context.WithTimeout(context.Background(), s.timeout)
defer cancel()
userSession, err := s.hub.GetDefaultSession(ctx)
if err != nil {
return types.AuthConfig{}, time.Time{}, err
}
username := userSession.Claims.Username
if username == "" {
profile, err := s.hub.GetDefaultProfile(ctx)
if err != nil {
return types.AuthConfig{}, time.Time{}, fmt.Errorf("resolving the Docker Hub username: %w", err)
}
username = profile.Username
}
if username == "" {
return types.AuthConfig{}, time.Time{}, errNoUsername
}
now := s.now()
validUntil := now.Add(sessionTTL)
if exp := userSession.Claims.ExpiresAt; exp != nil {
validUntil = exp.Add(-sessionExpirySkew)
if !now.Before(validUntil) {
return types.AuthConfig{}, time.Time{}, errSessionExpired
}
}
return types.AuthConfig{
Username: username,
Password: userSession.AccessToken,
}, validUntil, nil
}
func logFallback(name string, err error) {
if isSessionUnavailable(err) {
logrus.Debugf("no %s session available from Docker Desktop, using Docker CLI credentials: %v", name, err)
return
}
logrus.Warnf("could not use the %s session from Docker Desktop, using Docker CLI credentials instead: %v", name, err)
}
func isSessionUnavailable(err error) bool {
if _, ok := errors.AsType[*seclient.ConnectError](err); ok {
return true
}
return errors.Is(err, fs.ErrNotExist) ||
errors.Is(err, dockerhub.ErrNoSession) ||
errors.Is(err, errSessionExpired)
}