How peryx scopes and serves manifest reads
peryx stores every manifest once, addressed by the sha256 of its bytes, in a global content pool the OCI indexes share. That is what makes deduplication work: the same image pulled through two repositories keeps one copy. It also raises a question the read path has to answer on every by-digest pull, which repository is allowed to read those shared bytes, and a compatibility question a tag pull has to answer, which of a multi-platform image's parts to hand a client that cannot read the index.
Why a by-digest read is scoped to the repository
A GET or HEAD /v2/<name>/manifests/<digest> names a manifest by its content address. peryx used to answer it from
the shared pool under any repository the caller could address, checking only that repository's policy, never whether the
repository held the digest. So the bytes came back as long as the digest existed anywhere in the store.
Digests are not secret. They appear in tags, referrers, the catalog, image indexes, CI logs, and every docker pull
output. A caller who learned one, from a colleague's build log or a public base image, could pull or probe a private
image's manifest by digest under any repository name it was allowed to read, across index and tenant boundaries. The
by-digest read was the leak; #103 had already closed the same gap on
DELETE and left the read open, so this (#136) is its read half.
peryx now authorizes a by-digest read against per-repository membership rather than the digest's presence in the pool. A repository reads a digest by digest only when one of its serving members recorded serving that digest under the repository:
- a manifest pushed, pulled, tagged, or mirrored under that member and repository,
- a child of an image index or manifest list the member stores, or
- a referrer pushed there.
Blobs already gate this way; manifests now match. A proxy member still pulls an unauthorized miss through its upstream,
scoped to the requested repository, so a legitimate pull-through of an image the repository serves stays intact, as do
referrer and image-index child pulls. A digest that no member records and no proxy can fetch returns the ordinary
404 MANIFEST_UNKNOWN, the same answer as a digest that does not exist, so the response never reveals that the digest
is stored elsewhere.
The membership record is written wherever peryx stores a manifest: its own digest, plus each child an image index or manifest list names. A by-digest delete drops the record unless an index the repository still serves names the digest as a child, so deleting an image and re-pushing it under another repository cannot revive a stale grant.
Why an index tag can serve a single-platform image
A tag often points at an image index (an OCI index or a Docker manifest list), the small document that maps each
platform, linux/amd64, linux/arm64, to the per-platform image manifest for it. A modern client pulls the index,
picks the entry for its platform, and pulls that child.
Docker below 17.06 predates the manifest list. It sends an Accept naming only the schema-2 image manifest and cannot
parse an index, so a registry that hands it the index body on a tag pull gives it something it cannot read. peryx now
negotiates the manifest read against Accept the way an upstream registry does: when a tag resolves to an index or
manifest list and the client's Accept names neither list media type, peryx serves the index's linux/amd64 child
image manifest, reading it from the store or fetching it by digest through a proxy member, with the child's
Content-Type and Docker-Content-Digest. A HEAD returns the same headers with an empty body.
Nothing else changes. An Accept that names a list type, or is absent, still gets the index, as does an index with no
linux/amd64 child; only the serve path negotiates, and a push stores what it is given. Modern docker, podman,
containerd, and oras all send Accept lists that name the index types, so they receive the index and never see the
substitution (#114).
In practice
- The manifest routes and their status codes: HTTP endpoints
- How a repository shadows an upstream image: the index model
- What a digest addresses and how peryx verifies it: OCI