Skip to content

WIP: use the Docker Desktop session for Docker Hub and dhi.io credentials - #7431

Draft
Benehiko wants to merge 6 commits into
docker:masterfrom
Benehiko:secrets-engine-desktop-auth
Draft

Benehiko wants to merge 6 commits into
docker:masterfrom
Benehiko:secrets-engine-desktop-auth

Conversation

@Benehiko

@Benehiko Benehiko commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Warning

Work in progress. This is a draft to share the approach and the measurements. See Open items for what is left.

Note

This PR was created with alki, an AI coding agent, running Claude Opus 5.5: the code, the tests, the benchmarks, and this description.

Summary

When Docker Desktop is running and signed in, use its Docker Hub session as the first source of registry credentials for Docker Hub and Docker Hardened Images, as docker/compose does.

  • A new internal/secretsengine package reads the default Docker Hub session from the Docker Desktop secrets engine, through github.com/docker/secrets-engine/client.

  • command.RetrieveAuthTokenFromImage asks for credentials in this order:

    1. DOCKER_AUTH_CONFIG, for the registries it defines.
    2. The Docker Desktop session.
    3. The credential store, credential helpers, and configuration file, as before.

    This covers docker pull, push, run, create, plugin install and push, service create and update, and stack deploy.

  • Registries:

    • docker.io, index.docker.io, and registry-1.docker.io use the Docker Hub session.
    • dhi.io uses the Docker Hub session, with ServerAddress set to dhi.io.
    • registry-1-stage.docker.io uses the staging session.
    • Every other registry never contacts Docker Desktop.
  • docker login, docker logout, and stored credentials are not changed. A Docker Desktop token is never written to the credential store.

  • Falling back to the CLI's own credentials:

    • No socket: one os.Lstat, no client is created.
    • Nothing listening on the socket, signed out, or an expired session: the CLI falls back without a warning.
    • An engine that does not answer within 5 seconds: the CLI falls back with a warning.
    • After a failed lookup, Docker Desktop is not asked again for a minute. A session is reused until 30 seconds before it expires.
  • On macOS, Docker Desktop does not prompt Docker-signed binaries. Binaries that are not signed by Docker, such as local builds and test binaries, would prompt. test.NewFakeCli turns the lookup off, so that unit tests never reach the Docker Desktop of the machine they run on.

Commits:

  1. vendor: github.com/docker/secrets-engine/client v0.2.2
  2. cli/command: use the Docker Desktop session for Docker Hub
  3. internal/secretsengine: add unit tests
  4. internal/secretsengine: test and benchmark against a secrets engine socket
  5. internal/test: keep FakeCli from contacting Docker Desktop
  6. cli/command: test credential precedence with Docker Desktop

Latency

Extra time per docker pull of a Docker Hub or dhi.io image (p50, this branch minus master), and the cost of one lookup against the engine:

Linux Windows macOS
No Docker Desktop (no socket) +0.05 to +0.14 ms +0.48 to +0.99 ms¹ +0.24 to +0.25 ms
Docker Desktop not running (nothing listening) +0.20 to +0.26 ms +1.15 to +1.24 ms¹ +0.04 to +0.67 ms
Engine running, signed out +15.2 to +15.4 ms³ +1.2 to +1.6 ms +3.8 to +4.2 ms
Engine running, signed in +15.3 to +16.6 ms³ +1.8 to +2.4 ms +3.7 to +4.5 ms
Engine adds 50 ms per request +197 to +200 ms² +153 to +177 ms² +99 to +104 ms
DOCKER_AUTH_CONFIG set for Docker Hub +0.26 ms +0.21 ms −0.13 ms
Engine accepts but never answers +5.01 s +5.04 s +5.00 s
docker --version (p50, before → after) 5.11 → 5.24 ms 28.33 → 28.60 ms 16.17 → 17.10 ms
One lookup, new connection to the engine 6.95 ms 0.68 ms 2.68 ms
One lookup, reused connection 0.19 ms 0.21 to 0.37 ms 0.12 ms

¹ ghcr.io, which never contacts the engine, differs by +0.58 to +0.79 ms in the same runs.
² A lookup makes two requests to the engine. The VMs oversleep: a fake engine that sleeps 10 ms per request takes 44.7 ms per lookup on Linux and 62.0 ms on Windows, against 22.8 ms on macOS.
³ The cost is in the engine, not the CLI. On Linux the engine verifies the binary of every connecting process, and the cost grows with the size of that binary. Signed out costs the same as signed in.

Every case sent the expected credentials: the Docker Desktop session for Docker Hub and dhi.io, DOCKER_AUTH_CONFIG over Docker Desktop, and the configuration file everywhere else.

How this was measured:

  • master and this branch were built with CGO_ENABLED=0.
  • The engine is a standalone secrets engine built on the engine runtime, with a seeded Docker Hub session. A fake Docker daemon on 127.0.0.1 recorded the X-Registry-Auth of every pull.
  • 30 measured runs per case, 3 for the engine that never answers. HOME, the Docker config, and the Docker Desktop socket path were isolated in a temporary directory.
  • Linux: Ubuntu 24.04.4 arm64 VM, 4 vCPUs. Windows: Windows 11 (26100) arm64 VM, 4 vCPUs. macOS: an Apple M4 Max host, since a macOS VM needs interactive provisioning.
  • "One lookup" rows: BenchmarkExternalEngine from this branch, against the same engine.
  • The measurements were taken before the package was renamed from internal/desktopauth to internal/secretsengine; the code is otherwise the same.

Binary size

docker built from master and from this branch with -tags grpcnotrace and CGO_ENABLED=0; stripped means -ldflags "-s -w". 1 MB = 1,000,000 bytes.

platform unstripped stripped
linux/arm64 40.14 → 41.41 MB (+1.27 MB, +3.2%) 27.59 → 28.44 MB (+0.85 MB, +3.1%)
linux/amd64 42.93 → 44.25 MB (+1.32 MB, +3.1%) 29.70 → 30.59 MB (+0.90 MB, +3.0%)
darwin/arm64 41.66 → 42.97 MB (+1.31 MB, +3.1%) 28.54 → 29.43 MB (+0.89 MB, +3.1%)
windows/amd64 43.58 → 44.92 MB (+1.34 MB, +3.1%) 30.53 → 31.45 MB (+0.92 MB, +3.0%)
  • Newly linked modules: connectrpc.com/connect v1.19.1, github.com/docker/secrets-engine/client v0.2.2, github.com/docker/secrets-engine/x v0.8.5, and golang.org/x/mod v0.41.0. Protobuf and net/http were already linked.
  • The added symbols total 0.29 MB, of which internal/secretsengine itself is 0.006 MB. Most of the rest is the secrets engine client wiring RPCs the CLI never calls, and connect.
  • client: read-only client without plugin management or connect (client v0.2.2 adds 1.3 MB to the Docker CLI) secrets-engine#709 proposes a read-only client without plugin management or connect. With prototypes of that client, the increase on linux/arm64 drops to +0.20 MB unstripped and +0.13 MB stripped (−85%).
  • Release builds also use cgo and -buildmode=pie on Linux and macOS, which these builds did not.

Open items

  • Reduce the binary size through client: read-only client without plugin management or connect (client v0.2.2 adds 1.3 MB to the Docker CLI) secrets-engine#709, then switch to the read-only client.
  • Engine-side: reduce the per-connection cost on Linux (+15 ms per docker pull).
  • Measure with a Docker-signed binary against Docker Desktop, and on macOS in a VM.
  • Windows: in one benchmark run against the engine, a lookup on a reused connection failed with wsarecv: The I/O operation has been aborted and fell back to the CLI's credentials. This was not reproduced in 10 further runs, and no docker pull failed.
  • Decide whether "access denied" from Docker Desktop should warn on every pull (as docker/compose does) or log at debug level.

Release notes (optional)

Use the Docker Desktop session for Docker Hub and Docker Hardened Images credentials when Docker Desktop is running and signed in. `DOCKER_AUTH_CONFIG` still takes precedence.

Add the Docker secrets engine client, used to read the Docker Desktop session for Docker Hub. It brings in github.com/docker/secrets-engine/x v0.8.5 and connectrpc.com/connect v1.19.1.

The client module requires go1.26.10, which raises the go directive in vendor.mod from 1.26.0.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
@codecov-commenter

codecov-commenter commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.96429% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/secretsengine/secretsengine.go 93.87% 4 Missing and 2 partials ⚠️
cli/command/registry.go 84.61% 1 Missing and 1 partial ⚠️
internal/test/cli.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

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>
Cover which registries use the Docker Desktop session, keeping the production and staging sessions apart, caching the session until it expires, the retry interval after a failed lookup, and Disable.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
…ocket

Run lookups against a fake secrets engine on a unix socket: signed in, signed out, no socket, a socket with nothing listening, and an engine that never answers. The benchmarks cover the same cases. BenchmarkExternalEngine runs against the engine at SECRETSENGINE_BENCH_SOCKET when it is set.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
Tests that use FakeCli pull, push, and create containers through RetrieveAuthTokenFromImage. Disable the Docker Desktop lookup, so that they never reach the Docker Desktop of the machine they run on.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
Check that the Docker Desktop session is used for Docker Hub and dhi.io, that other registries use the configuration file, and that DOCKER_AUTH_CONFIG takes precedence over Docker Desktop. TestRetrieveAuthTokenFromImage disables the Docker Desktop lookup.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
@Benehiko
Benehiko force-pushed the secrets-engine-desktop-auth branch from 7c99af5 to a4adc90 Compare October 22, 2026 09:23

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟢 APPROVE

s.hub = hub
}

ctx, cancel := context.WithTimeout(context.Background(), s.timeout)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Shared 5-second context split across two sequential RPC calls

fetch() creates a single context.WithTimeout(context.Background(), s.timeout) (5 s) and passes it to both GetDefaultSession and — when userSession.Claims.Username is empty — a subsequent GetDefaultProfile call. If the first call consumes the majority of the budget the second call gets only the remainder and may time out immediately, causing a spurious failure and a 1-minute retry backoff.

In practice the benchmarks show lookups take 2–15 ms (far below 5 s), so both calls complete well within the budget under normal conditions. The consequence of a timeout is also graceful: auth() sets retryAt and falls back to the CLI's own credentials. Consider using two separate timeouts (one per call) to make the intent explicit and to avoid a theoretical starvation when the engine is under load.

Confidence Score
🟡 moderate 62/100


userSession, err := s.hub.GetDefaultSession(ctx)
if err != nil {
return types.AuthConfig{}, time.Time{}, err

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Stale s.hub client is never reset after a connection failure

s.hub is set once in fetch() and never cleared when GetDefaultSession returns an error. After the 1-minute retryAt window elapses, fetch() skips the if s.hub == nil { s.connect() } branch and retries on the same client object.

For the common case of a Docker Desktop restart at the same socket path, the existing HTTP/1.1 keep-alive transport will auto-redial and the retry succeeds. However, if the socket path were to change (e.g., Docker Desktop is reinstalled at a different path), the stale transport continues dialling the old path and retries will keep failing until the process restarts.

Resetting s.hub = nil on the error path inside auth() (or inside fetch() when a *seclient.ConnectError is returned) would guarantee a clean reconnect on every retry:

Suggested change
return types.AuthConfig{}, time.Time{}, err
userSession, err := s.hub.GetDefaultSession(ctx)
if err != nil {
s.hub = nil
return types.AuthConfig{}, time.Time{}, err
}
Confidence Score
🟡 moderate 62/100

for _, item := range resp.Msg.GetEnvelopes() {
id, err := secrets.ParseID(item.GetId())
if err != nil {
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] ParseID errors are silently swallowed in resolverClient.GetSecrets

When the server returns envelopes whose IDs cannot be parsed by secrets.ParseID, the loop continues without logging or propagating the error. If every envelope in the response has an unparseable ID, the function returns (nil, nil) — an empty slice with no error — instead of surfacing the failure.

Callers in desktopauth treat an empty/nil result as "no session found" and fall through to the next credential source, hiding a potential protocol mismatch between the client library and the secrets engine.

In normal operation the server serializes IDs that were previously validated, so this path should not trigger. However, a format change or server/client version mismatch would silently degrade rather than report an actionable error. Consider logging at debug level when an ID cannot be parsed, or returning a partial-result error.

Confidence Score
🟡 moderate 62/100

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants