Skip to content
All writing
  • oci
  • nexus
  • artifact-spec

OCI 1.1 and the subject field that finally made referrers click

OCI 1.0 made it painful to attach signatures and SBOMs to an existing image. The 1.1 subject field and referrers API quietly fix that, and implementing it taught me things the spec does not say out loud.

6 min readBy Sraavan Chevireddy

For a long time, attaching something to an OCI image — a signature, an SBOM, a provenance attestation — meant inventing a naming convention on top of a tag. You would push the image as myapp:1.2.3, and then push a separate artifact as myapp:sha256-abc123.sig. The client had to know the convention. The registry had no idea the two were related. If you garbage-collected one, you could orphan the other. It worked, in the way that a rubber band works.

OCI 1.1 is the version of the spec that makes this stop being a hack. The two things that matter are the subject field on a manifest and the referrers API on the registry.

What the subject field actually does

Before 1.1, a manifest described a blob graph: one config, some layers, done. There was no first-class way for a manifest to say "I exist because of that other manifest over there."

The subject field is that declaration. When I push an SBOM as an OCI artifact, I set the SBOM manifest's subject to a descriptor pointing at the image it describes — digest, media type, size. The registry stores that link. When a client later asks "what refers to sha256:...?", the registry walks its index and returns every manifest that named that digest in its subject.

The important thing is that the referring artifact does not need a tag. It does not need a predictable name. It gets stored and addressed entirely by its content hash, and discovered through the graph. Tag namespaces stop being overloaded with metadata they were never meant to hold.

The referrers API

The spec defines a new endpoint, roughly:

GET /v2/<name>/referrers/<digest>

The response is an image index — a list of descriptors for manifests whose subject pointed at <digest>. You can filter by artifactType query parameter, so a cosign verify only pulls signature manifests and a syft client only pulls SBOMs.

That description hides how much of the work is storage work, not protocol work. A referrers response has to be cheap. You cannot afford to walk every manifest in a repository on every query. You need an index. And the index has to stay honest across pushes, deletes, and the long tail of weird edge cases that have been legal since 1.0.

What I shipped, and the one thing that bit me

The piece my team and I shipped into Nexus Repository was the referrers endpoint across our blob-store backends, plus the fallback path for tenants still sitting on 1.0-era data. The fallback matters more than the happy path, because most customers do not get to cut over cleanly. They have ten million existing images, and they need every one of them to keep working while the new plumbing goes in.

Here is the gotcha nobody wrote down in the PRs I read.

The 1.1 spec allows registries to either maintain a persistent index of referrers or, if they cannot, return a synthesized response at query time. A "fallback" response is marked with the header OCI-Filters-Applied: artifactType, and clients are supposed to handle either path. That all sounds clean.

What actually happens is this: in the fallback path, you are walking the manifest store under read load to compute an answer. If you are not careful about bounding that walk, a single referrers query for a popular image digest will hit every manifest in the repository on every call. We watched a large customer's p99 on that endpoint go to four seconds under their normal build traffic — not because the index was slow, but because there was no index yet for images pushed before we enabled 1.1. The write path was correct. The read path was paying the migration cost, in real time, on every request.

The fix was not clever. We built a background migrator that scanned the manifest store and emitted referrer index entries for older pushes. Throttled, resumable, with a per-repository progress marker that the serving layer could consult. Once the migrator caught up to a repository, the read path flipped to indexed mode for that repository and never looked back. During the catch-up window, we returned a correct synthesized answer with a higher latency budget, and we logged the tail so support could actually see what was slow and why.

The lesson that stuck: when you add an index to an existing system, the hard part is not maintaining it going forward. The hard part is the migration from a world where the index did not exist to a world where it does, without dropping reads on the floor or lying to clients about completeness.

A small reflection on specs

Reading the 1.1 PRs, you would think the addition was tidy. subject field, new endpoint, two media-type constants, done. And at the spec layer it genuinely is tidy. The people who wrote 1.1 did a careful job.

But specs have a seam where they stop describing protocol and start describing implementation, and that seam is where the work lives. The spec does not tell you how to keep the referrers index in sync with a distributed blob store under concurrent pushes. It does not tell you what to do when a manifest with a subject field is pushed before the manifest it references, which happens more often than you would expect in CI pipelines that push artifacts in parallel. It does not tell you how to garbage-collect a referring artifact when its subject is deleted, or whether you even should.

Those answers are not in the spec because the spec is not the right place for them. They are in the implementation, and each registry gets to make its own choices, and those choices are part of what makes one registry feel different from another once you have been using it for a year.

I have come out of this work more sympathetic to spec authors, not less. You can see the shape of a decision from inside an implementation that you cannot see from the PR that introduced it. And the ones who do this well — the ones who leave enough room in the text for implementers to make reasonable, different choices — are doing work that is genuinely hard and almost entirely invisible.

If you are integrating with OCI 1.1, the short version is: push a manifest with a subject field, look up referrers by digest, filter by artifactType, and trust the fallback header when the registry tells you it synthesized the answer. It finally works the way most of us thought OCI worked the first time we read about it.

The rubber band is gone. That is a nice feeling, on a Friday.