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_prefixand 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.