Skip to content

SSRF fix in PR #1038 blocks all private/loopback/link-local realm addresses, breaking internal/corporate registries #1050

Description

@okhowang

Summary

PR #1038 fixes an SSRF where a malicious registry redirects the token endpoint (realm in a 401 WWW-Authenticate: Bearer challenge) to an internal address such as the cloud metadata service 169.254.169.254. The fix lives in pkg/distribution/oci/remote/transport.go (resolveAndValidateHost / isDisallowedIP, plus the guarded dialer newGuardedAuthClient).

While it stops the metadata SSRF, the chosen approach is a blocklist of private / loopback / link-local IP ranges and internal hostnames. This rejects any token endpoint that resolves to a private IP — including fully legitimate internal/corporate registries — so pulling models from an internal registry now fails.

Current behavior

resolveAndValidateHost (called both at resolve time and again in newGuardedAuthClient.DialContext) rejects the realm when:

  • the hostname is in internalHostnames (localhost, host.docker.internal, …), or
  • the literal IP, or any IP returned by net.LookupHost, falls in private (10/8, 172.16/12, 192.168/16), loopback (127/8, ::1), or link-local (169.254/16, fe80::/64) ranges.

Impact

Internal model registries are a very common enterprise deployment (internal Harbor / Artifactory / HuggingFace mirror, or registry mirrors on RFC1918 networks). For such a registry, the realm in the 401 challenge is typically https://auth.corp.internal/token — or even the registry's own hostname — which resolves to a private IP. After PR #1038 those pulls fail with:

realm URL resolves to a disallowed address 10.x.x.x

…even though the registry is trusted and in the same trust domain. Public clouds (Docker Hub auth.docker.io, ECR, GCR, ACR) resolve to public IPs, so they are unaffected — which is likely why this breakage does not surface in public-registry testing.

Why an IP-range blocklist is the wrong primitive

The real threat is a cross-trust-domain pivot: a registry at host A returns a realm pointing at host B that the client can reach but the registry cannot (cloud metadata, internal admin panels). Whether B's IP is "private" is incidental. A legitimate internal registry at registry.corp.internal returning a realm also on registry.corp.internal (or auth.corp.internal) is same-trust-domain and is not the threat — yet it is blocked purely because the IP is private.

Proposed fix: trust-domain validation instead of an IP-range blocklist

Validate that the realm host stays within the trust domain of the registry being pulled, rather than banning private IPs:

  • The realm host must equal the registry host being pulled, or be in that registry's configured set of allowed auth hosts (mirror / hosts config). This preserves internal registries and still blocks pivots to other internal services.
  • Keep a narrow safety net that additionally blocks only the cloud-metadata link-local addresses (169.254.169.254, 169.254.170.2) and the IPv6 metadata range, since those are the high-value SSRF targets and should never be a token endpoint.
  • Retain the existing DNS-rebinding protection (resolveAndValidateHost re-checked at dial time, dialing the already-resolved IP).

This keeps the SSRF protection — a public evil.com registry returning realm=169.254.169.254 is still rejected, and a registry cannot pivot to an arbitrary other internal host — while allowing internal/corporate registries whose token endpoint is on a private network.

Reproduction (conceptual)

  1. Deploy an internal registry whose token auth endpoint resolves to a private IP (e.g. https://registry.corp.internal returns WWW-Authenticate: Bearer realm=https://registry.corp.internal/auth/token).
  2. Pull a model from that registry via model-runner.
  3. With PR fix(distribution): validate token realm on pull and re-challenge paths #1038, the pull fails at token fetch with realm URL resolves to a disallowed address 10.x.x.x.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions