Documentation

Docker Hub names and upstream auth

Docker Hub expands short official-image names and returns 401 when its token realm rejects a repository scope.

Official-image namespace

Docker Hub namespaces every repository by its owner: grafana/grafana belongs to the grafana organization. The curated set Docker maintains (official images) belongs to an organization too, named library, so ubuntu on the registry is library/ubuntu.

The short form is a client-side convenience. When you type docker pull ubuntu, the Docker daemon expands the reference before it touches the network: no registry host means Docker Hub, no namespace means library, no tag means latest. The registry protocol has no short names, only library/ubuntu.

Routed-proxy expansion

peryx serves the container protocol under a route: /v2/hub/ubuntu/manifests/latest. The name that arrives is whatever the client put after the route, and a client that would have expanded ubuntu against Docker Hub does not expand it against peryx.internal:4433, because that host is a separate registry with a repository called hub/ubuntu. peryx strips the route and would pass ubuntu upstream.

Hub answers 401 to a request for a repository named ubuntu, not 404, because its auth layer runs before its lookup: the token realm will not issue a pull token for a scope it does not recognize. So the failure of a routed pull of a short name therefore looks like an authorization failure.

The library_prefix setting makes peryx do the expansion the client skipped, on the upstream request alone. auto, the default, recognizes a Hub upstream by its host and prefixes a single-segment name.

Registry-mirror behavior

When the Docker daemon uses peryx through registry-mirrors, short names need no peryx setting. The daemon resolves ubuntu to library/ubuntu as part of its own reference parsing, then sends that full name to the mirror, which serves an empty route. peryx receives library/ubuntu and passes it upstream verbatim, because a name with a namespace is never rewritten.

The two modes differ in who expands the name. In registry-mirror mode the daemon does it and peryx sees the result. In routed mode nothing expands it, so peryx does. Both end up asking Hub for library/ubuntu.

Upstream 401 diagnosis

Earlier versions folded an upstream 401 into "this member does not have it", which reached the client as MANIFEST_UNKNOWN: a pull of an official image by its short name reported a missing manifest, when the real cause was Hub refusing the request. Since #108, an upstream 401 surfaces as itself:

{
  "errors": [
    {
      "code": "UNAUTHORIZED",
      "message": "upstream registry refused authentication for this manifest"
    }
  ]
}

The status is 401. A cached index asks no credentials of its own clients, so this response reports an upstream rejection. Check these causes:

  • The repository name reaching the upstream is not one it will serve. On a Hub proxy, check library_prefix and the spelling of the name.
  • The index's upstream credentials (username, password, token) are wrong or expired.
  • The account behind those credentials cannot see that repository.

A 404 still means absent, and still reaches the client as MANIFEST_UNKNOWN or BLOB_UNKNOWN. A 403 also counts as absent, since a registry answers it for a repository it will not show anonymously, and a virtual index walks on to its next member.

Cached-image fallback

A tag is mutable, so a cached index revalidates it upstream once its freshness window (cache_ttl_secs) elapses. If that revalidation returns 401, peryx has failed to confirm whether the tag changed. It serves the cached manifest and blobs within max_stale_secs past the freshness window, the same bound used when an upstream is unreachable (see configuration).

With an expired upstream credential, cached content remains available within max_stale_secs; uncached content returns 401, and logs include the upstream status.

Trusted token realms

The upstream 401 names its token realm in the WWW-Authenticate header, and peryx follows it to trade the index's credentials for a bearer token. When those credentials are a username/password pair, peryx sends them as Basic authentication. The registry picks the realm, so a hostile or compromised upstream that answers with realm="https://attacker.example/token" would be handing itself the mirror's credentials, over a TLS-valid connection to a registry the operator trusts for everything else.

peryx presents those credentials only to the upstream's own origin or to an origin the operator named in [index.settings].token_realms. Docker Hub authenticates at auth.docker.io, a different host from registry-1.docker.io, so an authenticated Hub proxy lists it:

[index.settings]
token_realms = ["https://auth.docker.io"]

Any other realm is still contacted, because that is how a public registry issues an anonymous pull token, but the request carries no credentials and each redirect hop is checked again before the next one can receive them. An anonymous Hub proxy needs no entry at all.

A realm that then answers 401 names the origin the credentials were withheld from, so a missing token_realms entry reads as itself rather than as a wrong password:

bearer realm https://auth.docker.io is not a trusted token realm for this upstream, so the token request
carried no credentials; add it to `token_realms` to authenticate there

Reaching a realm on a private address is a separate permission that belongs to the upstream's trusted_hosts.

On this page