Skip to content

URL agent sources bypass Docker Desktop's proxy: SSRF-safe transport uses ProxyFromEnvironment, breaking PAC-only environments #3998

Description

@aheritier

Description

When docker-agent serve api loads an agent from an https URL source, urlSource.Read() in pkg/config/sources.go constructs the Desktop proxy-aware client via httpclient.NewHTTPClient(ctx) but then (unless the source is marked unsafe or is localhost) replaces it with httpclient.NewSSRFSafeTransport(), whose Proxy is http.ProxyFromEnvironment (pkg/httpclient/ssrf.go). Identified by source inspection at v1.115.0 (sources.go:329-344), v1.124.0, v1.125.0, and current HEAD; not yet independently reproduced in a live rig. In environments where outbound HTTPS is only possible via a PAC-file proxy (no static HTTPS_PROXY env var — common in corporate networks), the fetch connects direct and fails at DNS/connect. Because Docker Desktop's managed Gordon agent is loaded from a URL source, the server starts with zero loaded agents: source_loader.go loads synchronously once, getAgents() silently skips the failed source and returns [], the Desktop UI falls back to requesting docker_gordon.yaml, and resolveSource() (pkg/server/session_manager.go) cannot match it → HTTP 500 "agent not found". The OCI source path does not have this bug (pkg/remote/pull.go uses the Desktop-aware transport via crane.WithTransport). Additionally, the URL cache key is the sha256 of the full raw URL (hashURL); since the shipped URL embeds desktopVersion and gordonTag, every Desktop upgrade cold-starts the cache and the failure recurs.

Expected Behavior

URL sources for Docker-hosted endpoints load successfully behind a PAC-only proxy, using the same Desktop proxy-aware transport as OCI pulls; failures should surface as a meaningful error, not a 500 "agent not found".

Actual Behavior

Behind a PAC-only proxy, the initial source fetch fails (direct connect), the agent list is empty, and Desktop's Gordon UI receives HTTP 500 on POST /api/sessions/<id>/agent/docker_gordon.yaml. Because of the version-bearing cache key, the failure recurs after every Desktop upgrade even if a previous cache existed.

Steps to Reproduce

Expected reproduction, derived from source inspection — untested:

  1. On a host whose only outbound path is a PAC-file proxy (or simulate: block direct egress to the source host with a firewall rule; provide a working proxy configured only via PAC/Desktop proxy settings, with no HTTP(S)_PROXY env vars).
  2. Configure Docker Desktop's proxy settings with the PAC (Desktop's own httpproxy will CONNECT to the source host successfully).
  3. Cold cache: remove <data-dir>/url_cache.
  4. Run docker-agent serve api --listen <sock> --data-dir <dir> https://..../proxy/gordon-agent?... (the argv Desktop uses) or any https URL source resolvable only via the proxy.
  5. Expected: the source fetch dials direct and fails; GET /api/agents returns []; any session POST for the agent returns 500.

Docker Agent version

Identified by source inspection at v1.115.0; code present unchanged at v1.124.0, v1.125.0, and HEAD (pkg/config/sources.go, urlSource.Read).

OS & terminal

Not OS-specific — network topology (PAC-only egress) is the trigger.

Error output

Generic: the source load error is swallowed — `getAgents()` returns empty; the client sees `HTTP 500` / `agent not found` from `resolveSource()`.

Additional context / Proposed remediation

  • (1) For https docker.com/*.docker.com source URLs, use the Desktop proxy-aware transport with an explicit Timeout and CheckRedirect (NewHTTPClient sets neither); keep NewSSRFSafeTransport for all other non-localhost URLs. Do NOT reuse environment.IsTrustedDockerURL as the predicate — it admits localhost/loopback over plain http and gates JWT injection, not SSRF. Alternative worth security review: teach NewSSRFSafeTransport the Desktop proxy socket while keeping its dial-time allowlist (preserves anti-DNS-rebinding hardening for all hosts).
  • (2) Bounded startup retry with backoff in source_loader.go when the initial synchronous load fails (today only the 60-min ticker retries, and only when refreshInterval > 0).
  • (3) Surface unresolvable-agent as 404/503-class with the underlying source-load failure category instead of 500.

Activity

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

Metadata

Metadata

Assignees

Labels

area/configFor configuration parsing, YAML, environment variables

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions