Description
docker login reports Login Succeeded for a wrong password against a private registry secured with plain HTTP Basic Auth (WWW-Authenticate: Basic, not a Bearer-token challenge) — but only when the Docker daemon is not reachable. In that state, docker login falls back to a client-side (daemon-less) login path. That path performs a real HTTP request against the registry and correctly propagates transport-level errors (DNS failure, proxy unreachable), but does not appear to correctly evaluate a 401 Unauthorized response from a Basic-Auth-only registry as a login failure.
Once the daemon is running, the exact same command against the exact same registry with the exact same wrong password correctly returns:
Error response from daemon: login attempt to https://<registry>/v2/ failed with status: 401 Unauthorized
So the bug is isolated to the daemon-less client-side login path, specifically for registries using plain Basic Auth rather than the Bearer-token flow.
The registry itself behaves correctly throughout — cross-checked with curl using a Docker-matching User-Agent against the same host/credentials, bypassing Docker entirely: it consistently and correctly returns 401 / WWW-Authenticate: Basic realm="..." for missing, empty, and wrong credentials alike. The discrepancy is purely in how the CLI's daemon-less path evaluates the response.
Reproduce
Environment: private registry behind nginx, auth_basic enabled, WWW-Authenticate: Basic realm="..." (no Bearer/token server involved).
- Ensure Docker Desktop / the daemon is not running (
docker info returns Cannot connect to the Docker daemon at unix:///.../docker.sock).
- Run:
echo "definitely-wrong-password" | docker -D login -u someuser --password-stdin registry.example.com
- Observe debug output:
time="..." level=debug msg="hostDir: /etc/docker/certs.d/registry.example.com"
time="..." level=debug msg="attempting v2 login to registry endpoint" endpoint="https://registry.example.com/v2/"
Login Succeeded
No error is logged between "attempting v2 login" and "Login Succeeded", and the exit code is 0.
For comparison, two genuine failure cases do correctly log an error and exit non-zero on the same daemon-less path:
- Against a non-existent domain:
level=info msg="Error logging in to endpoint, trying next endpoint" endpoint="{https://nonexistent.invalid.example 0x...}" error="Get \"https://nonexistent.invalid.example/v2/\": dial tcp: lookup nonexistent.invalid.example: no such host"
- Routed through a deliberately unreachable local proxy (
HTTPS_PROXY=http://127.0.0.1:1):
level=info msg="Error logging in to endpoint, trying next endpoint" endpoint="{https://registry.example.com 0x...}" error="Get \"https://registry.example.com/v2/\": proxyconnect tcp: dial tcp 127.0.0.1:1: connect: connection refused"
Only the real registry's 401 (Basic-Auth challenge) response is missing that same error-logging step and is instead treated as success.
Expected behavior
docker login should report a failure (non-zero exit, no "Login Succeeded") when the registry responds with 401 Unauthorized to the Basic Auth credentials supplied, regardless of whether the request went through the daemon or the client-side fallback path — matching the behavior already seen when the daemon is running (Error response from daemon: login attempt to https://registry.example.com/v2/ failed with status: 401 Unauthorized).
docker version
Client:
Version: 28.4.0
API version: 1.51
Go version: go1.24.7
Git commit: d8eb465
Built: Wed Sep 3 20:56:26 2025
OS/Arch: darwin/arm64
Context: desktop-linux
Server: Docker Desktop 4.46.0 (204649)
Engine:
Version: 28.4.0
API version: 1.51 (minimum version 1.24)
Go version: go1.24.7
Git commit: 249d679
Built: Wed Sep 3 20:58:53 2025
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.7.27
GitCommit: 05044ec0a9a75232cad458027ca83437aae3f4da
runc:
Version: 1.2.5
GitCommit: v1.2.5-0-g59923ef
docker-init:
Version: 0.19.0
GitCommit: de40ad0
docker info
Client:
Version: 28.4.0
Context: desktop-linux
Debug Mode: false
Server:
Containers: 1
Running: 0
Paused: 0
Stopped: 1
Images: 35
Server Version: 28.4.0
Storage Driver: overlayfs
driver-type: io.containerd.snapshotter.v1
Logging Driver: json-file
Cgroup Driver: cgroupfs
Cgroup Version: 2
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
Swarm: inactive
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 05044ec0a9a75232cad458027ca83437aae3f4da
runc version: v1.2.5-0-g59923ef
init version: de40ad0
Security Options:
seccomp
Profile: builtin
cgroupns
Kernel Version: 6.10.14-linuxkit
Operating System: Docker Desktop
OSType: linux
Architecture: aarch64
CPUs: 10
Total Memory: 19.5GiB
Name: docker-desktop
Docker Root Dir: /var/lib/docker
Debug Mode: false
HTTP Proxy: http.docker.internal:3128
HTTPS Proxy: http.docker.internal:3128
No Proxy: hubproxy.docker.internal
Experimental: false
Insecure Registries:
hubproxy.docker.internal:5555
::1/128
127.0.0.0/8
Live Restore Enabled: false
(Captured while the daemon was running, i.e. the "working" case, for reference — the bug only reproduces while the daemon is *not* running, at which point `docker info` itself just returns "Cannot connect to the Docker daemon".)
Additional Info
- The registry uses plain HTTP Basic Auth directly (
WWW-Authenticate: Basic realm="..."), not the more common Bearer-token flow (Docker Hub / Harbor / ECR style). This appears to be what triggers the mis-evaluation in the daemon-less path.
- The daemon-less client-side login fallback itself is a known, intentional feature (see the design discussion in #41603), but that discussion doesn't cover this correctness gap.
- Practical security impact seems limited:
docker pull/docker push require the daemon and fail immediately with "Cannot connect to the Docker daemon" when it's down, before any registry auth would be attempted. The false "Login Succeeded" is nonetheless actively misleading when diagnosing credential problems while Docker Desktop happens to be stopped — it can send someone chasing a network/registry-config red herring instead of noticing the daemon simply isn't running.
- Reproduced on macOS (Apple Silicon), Docker Desktop, client
darwin/arm64.
Description
docker loginreportsLogin Succeededfor a wrong password against a private registry secured with plain HTTP Basic Auth (WWW-Authenticate: Basic, not a Bearer-token challenge) — but only when the Docker daemon is not reachable. In that state,docker loginfalls back to a client-side (daemon-less) login path. That path performs a real HTTP request against the registry and correctly propagates transport-level errors (DNS failure, proxy unreachable), but does not appear to correctly evaluate a401 Unauthorizedresponse from a Basic-Auth-only registry as a login failure.Once the daemon is running, the exact same command against the exact same registry with the exact same wrong password correctly returns:
So the bug is isolated to the daemon-less client-side login path, specifically for registries using plain Basic Auth rather than the Bearer-token flow.
The registry itself behaves correctly throughout — cross-checked with
curlusing a Docker-matching User-Agent against the same host/credentials, bypassing Docker entirely: it consistently and correctly returns401/WWW-Authenticate: Basic realm="..."for missing, empty, and wrong credentials alike. The discrepancy is purely in how the CLI's daemon-less path evaluates the response.Reproduce
Environment: private registry behind nginx,
auth_basicenabled,WWW-Authenticate: Basic realm="..."(no Bearer/token server involved).docker inforeturnsCannot connect to the Docker daemon at unix:///.../docker.sock).0.For comparison, two genuine failure cases do correctly log an error and exit non-zero on the same daemon-less path:
HTTPS_PROXY=http://127.0.0.1:1):Only the real registry's
401(Basic-Auth challenge) response is missing that same error-logging step and is instead treated as success.Expected behavior
docker loginshould report a failure (non-zero exit, no "Login Succeeded") when the registry responds with401 Unauthorizedto the Basic Auth credentials supplied, regardless of whether the request went through the daemon or the client-side fallback path — matching the behavior already seen when the daemon is running (Error response from daemon: login attempt to https://registry.example.com/v2/ failed with status: 401 Unauthorized).docker version
Client: Version: 28.4.0 API version: 1.51 Go version: go1.24.7 Git commit: d8eb465 Built: Wed Sep 3 20:56:26 2025 OS/Arch: darwin/arm64 Context: desktop-linux Server: Docker Desktop 4.46.0 (204649) Engine: Version: 28.4.0 API version: 1.51 (minimum version 1.24) Go version: go1.24.7 Git commit: 249d679 Built: Wed Sep 3 20:58:53 2025 OS/Arch: linux/arm64 Experimental: false containerd: Version: 1.7.27 GitCommit: 05044ec0a9a75232cad458027ca83437aae3f4da runc: Version: 1.2.5 GitCommit: v1.2.5-0-g59923ef docker-init: Version: 0.19.0 GitCommit: de40ad0docker info
Additional Info
WWW-Authenticate: Basic realm="..."), not the more common Bearer-token flow (Docker Hub / Harbor / ECR style). This appears to be what triggers the mis-evaluation in the daemon-less path.docker pull/docker pushrequire the daemon and fail immediately with "Cannot connect to the Docker daemon" when it's down, before any registry auth would be attempted. The false "Login Succeeded" is nonetheless actively misleading when diagnosing credential problems while Docker Desktop happens to be stopped — it can send someone chasing a network/registry-config red herring instead of noticing the daemon simply isn't running.darwin/arm64.