OCI Registry — Store more than Container Images

Stéphane Este-Gracias
9 min readApr 26, 2024
Photo by Guillaume Bolduc on Unsplash

The swift progress of container technologies necessitates standardized practices, notably guided by the Open Container Initiative (OCI).

This article examines the new OCI specifications crucial for standardizing container images and artifacts’ operation, structure, and distribution. The practical application of these standards in a registry to store artifacts is presented.

Introduction to OCI Specifications

The Open Container Initiative (OCI) specifications are defined for standardizing container technologies. There are 3 specifications.

First, the Runtime Specification defines the environment where containers operate. Next, the Image Specification defines how container images are organized. Finally, the Distribution Specs defines how to serve these images.

💡 OCI Specifications
- Runtime Specifications — latest release v1.2.0 — Feb 13th, 2024
-
Image Specifications — latest release v1.1.0 — Feb 15th, 2024
-
Distribution Specifications — latest release v1.1.0 — Feb 15th, 2024

Together, the Image and Distribution Specs have been updated to support more than container images. Before going to the new features, let’s focus first on image format and storage.

First, the registry is a service that implements the required HTTP API endpoints to push and pull images.

Then, a repository is a collection of related images, as different versions of the same application, stored within the registry. It represents a namespace for grouping images.

Then, the reference is a generic pointer to an image within a repository. It can be a tag, a human readable string, or a digest providing a unique hash for the image content, ensuring immutability and verification.

Usually, the registry clients support both references simultaneously: tag and digest for human readability and unicity, but, in that case, only the digest is sent to registry API.

Registry / Repository URL

The subsequent paragraphs provide explanations of the diagram presented below.

OCI Registry components

Tags Management

The registry implements a tags list endpoint /v2/<name>/tags/list to retrieve the related tags from the <name> repository. The response returns the list of stored tags on this repository with the following format.

$ curl https://registry.my.labs/v2/kube-apiserver-amd64/tags/list
{
"name": "kube-apiserver-amd64",
"tags": [
"v1.28.0",
"v1.28.1",
"v1.28.2",
"v1.28.3",
"v1.28.4",
"v1.28.5",
"v1.28.6",
"v1.28.7",
"v1.29.1",
"v1.29.2"
]
}

So, the tags are named-pointers to a manifest in a repository. They are designed to be human-readable.

It’s possible to assign multiple tags, each pointing to distinct manifests, allowing the management of different image versions. Multiple tags can be assigned to the same manifest: for instance, the latest tag. And a tag can be reassigned to a different manifest.

Manifests Management

Image Manifest

To access the manifest pointed by a tag, the endpoint is the following for a given <name> repository and <tag> tag: /v2/<name>/manifests/<tag> .

A manifest is a JSON document that describes an object in the given repository.

$ curl https://registry.my.labs/v2/kube-apiserver-amd64/manifests/v1.29.2
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha26:786295faae…",
"size": 2564
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:aba5379b9c…",
"size": 84572
},

]
}

In this case, the manifest is an image manifest as described in mediaType key,

Then, the config part describes the image configuration. It references a blob by its digest within this repository.

Blobs are the binary form of the content that is stored by a registry, addressable by its digest.

So, the config blob defines the image’s runtime configuration, including environment variables, default command to run, and other settings coming from the tool used to build the image.

The next part are the layers of the image. Each layer references a blob by its digest.

The layers blobs are the file system layers. Each layer is built upon the previous one, creating a full filesystem of the image when combined.

Image Index

$ curl https://registry.my.labs/v2/opentelemetry-operator/manifests/latest
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:7db3ebbdd5480eb18035d1ef8f8dd5604923bc0796d6ff80ac6025365fed214e",
"size": 673,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:97bcecfaefc88fa27b1a81f9ec3bc18ee2fd50bd7163bf8bb8448ba0f7e6f307",
"size": 673,
"platform": {
"architecture": "arm64",
"os": "linux"
}
}
]
}

Another type of manifest is image index that points to multiple image manifests as denoted by the mediaType key and the manifests list. Here, both manifests point to the same image version but with different architecture.

The pointed image manifests are the same as explained above, except that the image manifest is stored as a blob.

So, the usual use of OCI Registry is for image storage, let’s see now its expanded use to store artifacts.

OCI Artifacts

OCI artifacts storage uses the same principles explained before using an image manifest metadata to identify the artifacts and using the layers blobs to store their contents.

For several years, some projects have been coming up with different ways to specify artifacts in the registry.

First by storing content on the same repository as the image, such as signatures and software bill of materials. The usual implementation uses a well-known tag to identify the artifact.

Another method relied on using a dedicated repository to store content.
This is the method used by tools such as Helm, FluxCD, Open Policy Agent bundles…

In the new OCI release, the use of image manifest metadata and layers to store contents
has been finally codified as a valid way with some adjustments.

For new clients pushing artifacts, there is a new top-level key called artifactType which can be used to denote a custom artifact.

A new key called subject can now be included on manifests which points to another object in the registry. This feature can establish relationships for linking objects in the registry (image and/or artifacts). To discover these relationships, a new endpoint called referrers has been implemented.

ORAS implements these new specifications. ORAS means OCI Registry As Storage.

To demonstrate these changes, ORAS CLI tool is used in the following paragraphs to achieve the process represented in the following diagram.

Hierarchy of artifacts on OCI Registry

Push Content

Push Content

First, let’s create a resource file, basically, push a test namespace.

$ cat namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: test

$ oras push registry.my.labs/oras/test:v1 \
--artifact-type application/vnd.example.manifests.v1 \
namespace.yaml:application/yaml

Uploading d6f2a1f4c156 namespace.yaml
Uploaded d6f2a1f4c156 namespace.yaml
Pushed [registry] registry.my.labs/oras/test:v1
Digest: sha256:ba4a52f4c16afeb6af9e5fa7c833ea4d9e806bf032e9f10e4ea337ca6809469e

Let’s use oras push command to push this YAML resource to the local registry: oras/test:v1. Define the artifact type: example vendor of Kubernetes manifests. For each pushed file, define the filename with its type: namespace.yaml and application/yaml. After pushing the artifact, you get the digests of all uploaded files and the related image manifest.

Let’s see the created manifest.

$ curl https://registry.my.labs/v2/oras/test/manifests/v1 | jq .
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.example.manifests.v1",
"config": {
"mediaType": "application/vnd.oci.empty.v1+json",
"digest": "sha256:44136fa355b3…",
"size": 2
},
"layers": [
{
"mediaType": "application/yaml",
"digest": "sha256:d6f2a1f4c156…",
"size": 54,
"annotations": {
"org.opencontainers.image.title": "namespace.yaml"
}
}...

It’s still a JSON file representing an image manifest:

  • the same mediaType as for an image
  • the new artifactType and the referenced layer are those passed to the ORAS CLI just before

To pull the YAML resource from the registry, use oras pull command using the same URL as the push command.

$ oras pull registry.my.labs/oras/test:v1
Downloading d6f2a1f4c156 namespace.yaml
Downloaded d6f2a1f4c156 namespace.yaml
Pulled [registry] registry.my.labs/oras/test:v1
Digest: sha256:ba4a52f4c16afeb6af9e5fa7c833ea4d9e806bf032e9f10e4ea337ca6809469e

$ cat namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: test

So, ORAS download and extract the namespace.yaml file.

So, it’s straightforward to push and pull artifacts to the OCI registry.

Sign Content

Sign Content

Now, let’s sign the just pushed resource and push & attach the signature to the content.

$ cat signature.json
{
"artifact": "registry.my.labs/oras/test:v1@sha256:ba4a52f4c16a…",
"signature": "sestegra"
}

$ oras attach registry.my.labs/oras/test:v1 \
--artifact-type=application/vnd.example.signature.v1+json \
signature.json:application/json
Uploading 2dc8164f1420 signature.json
Uploaded 2dc8164f1420 signature.json
Attached to [registry] registry.my.labs/oras/test@sha256:ba4a52f4c16a…
Digest: sha256:73499a9600d96c6bc46671affd5b58559a1bb1b99644d8638fbb563bee7a3982

Let’s assume the given JSON is a valid signature.

Use the oras attach command, to push the signature artifact to the same repository as the YAML resource and to attach the signature to the YAML resource

As before, define the artifact type pushed to the registry: example vendor of signature. And define the filename with its type: signature.json and application/json. After pushing and attached to the YAML resource, the digest of the related image manifest is returned.

Let’s see the created manifest.

$ curl https://registry.my.labs/v2/oras/test/manifests/sha256:73499a9... | jq .
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.example.signature.v1+json",
"config": {
"mediaType": "application/vnd.oci.empty.v1+json",
"digest": "sha256:44136fa355b3…",
"size": 2
},
"layers": [
{
"mediaType": "application/json",
"digest": "sha256:2dc8164f1420…",
"size": 153,
"annotations": {
"org.opencontainers.image.title": "signature.json"
}
}
],
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:ba4a52f4c16a…",
"size": 572
},
"annotations": {
"org.opencontainers.image.created": "2024-03-17T08:42:37Z"
}
}

It’s still a JSON file representing an image manifest:

  • the same mediaType as for an image
  • the new artifactType and the referenced layer are those passed to the ORAS CLI just before

Then, extra information for relationships are present. The subject field points to the image manifest of the YAML resource (see sha256:ba4a52f4c16a… when pushing this content to the local registry).

To discover all relationships of an artifact, the oras discover command can be used.

$ oras discover registry.my.labs/oras/test:v1
Discovered 1 artifact referencing v1
Digest: sha256:ba4a52f4c16afeb6af9e5fa7c833ea4d9e806bf032e9f10e4ea337ca6809469e

Artifact Type Digest
application/vnd.example.signature.v1+json sha256:73499a9600d9…

$ oras pull registry.my.labs/oras/test@sha256:73499a9600d9…
Downloading 2dc8164f1420 signature.json
Downloaded 2dc8164f1420 signature.json
Pulled [registry] registry.my.labs/oras/test:v1@sha256:73499a9600d9…
Digest: sha256:73499a9600d96c6bc46671affd5b58559a1bb1b99644d8638fbb563bee7a3982

$ cat signature.json
{
"artifact": "registry.my.labs/oras/test:v1@sha256:ba4a52f4c16a…",
"signature": "sestegra"
}

So, the list of attached artifacts is returned, here the signature (see attached digest sha256:73499a9600d9… when pushing the content’s signature to the local registry).

Now, let’s pull the signature from the registry with oras pull command as before using the returned digest.

So, ORAS download and extract the signature.

So, it’s straightforward to get the attached signature from the tag of the YAML resource.

Create and Attach the SBOM

Create and Attach the SBOM

Let’s continue to push stuff on OCI registry, let’s create a software bill of materials and push and attach it the resource.

$ cat sbom.json
{
"artifact": "registry.my.labs/oras/test:v1@sha256:ba4a52f4c16a…",
”components": […]
}

$ oras attach registry.my.labs/oras/test:v1 \
--artifact-type=application/vnd.example.sbom.v1+json \
sbom.json:application/json
Uploading adebfc263c9c sbom.json
Uploaded adebfc263c9c sbom.json
Attached to [registry] registry.my.labs/oras/test@sha256:ba4a52f4c16afeb...
Digest: sha256:e3ef102e5c87576a7cc66885bf6d50608c86469f42da10f877f75c71184ad008

A SBOM consists of the list of the used components. Let’s assume the given JSON is a valid SBOM.

Use oras attach command to push the SBOM artifact to the same repository as the YAML resource and to attach the SBOM to the YAML resource.

As before, define the artifact type pushed to the registry: example vendor of SBOM. As before, define the filename with its type: sbom.json and application/json. After pushing, and attached to the artifact, the digest of the related image manifest is returned.

The content of the created image manifest is similar than before.

So, where we are in the global process?

  • Create an artifact containing YAML resources
  • Sign and attach the signature to the YAML resources
  • Create SBOM and attach it to the YAML resources

Sign SBOM, Push & Attach its Signature

Sign SBOM, Push & Attach its Signature

Now, let’s sign the SBOM, and attach the SBOM signature to the SBOM artifact.

$ cat sbom-signature.json
{
"artifact": "registry.my.labs/oras/test:v1@sha256:ba4a52f4c16a…",
"signature": "sestegra"
}

$ oras attach registry.my.labs/oras/test@sha256:e3ef102e5c87... \
--artifact-type=application/vnd.example.signature.v1+json \
sbom-signature.json:application/json
Uploading 1743cdc854d6 sbom-signature.json
Uploaded 1743cdc854d6 sbom-signature.json
Attached to [registry] registry.my.labs/oras/test@sha256:e3ef102e5c87576a...
Digest: sha256:bae78f64dc90d9e0d135342cd4ce938b746ca9d5e194a21640475fe7d492f1df

As before, let’s assume this is a valid signature represented by a JSON file.

Use oras attach command to push the SBOM signature artifact to the same repository as the initial artifact, and to attach the SBOM signature to the SBOM.

As before, define the artifact type pushed to the registry: example vendor of signature As before, define the filename with its type: sbom-signature.json and application/json. After pushing, and attached to the SBOM artifact, the digest of the related image manifest is returned.

Discover Referrers

Now, to discover the relationships just created, use the oras discover command using the repository oras/test and v1 tag with the tree option to get the hierarchy of all pushed artifacts.

$ oras discover -o tree registry.my.labs/oras/test:v1

registry.my.labs/oras/test@sha256:ba4a52f4c16afeb6af9e5fa7c83...
├── application/vnd.example.signature.v1+json
│ └── sha256:73499a9600d96c6bc46671affd5b58559a1bb1b99644d8638fbb563bee7a3982
└── application/vnd.example.sbom.v1+json
└── sha256:e3ef102e5c87576a7cc66885bf6d50608c86469f42da10f877f75c71184ad008
└── application/vnd.example.signature.v1+json
└── sha256:bae78f64dc90d9e0d135342cd4ce938b746ca...

The hierarchy is the expected as pushed artifacts.

  • First the YAML resource on top
  • With the attached signature
  • With the attached SBOM
  • And the signature of the SBOM attached to the SBOM

So, it’s straightforward to discover and pull all artifacts in these relationships by only knowing the initial pushed content.

Conclusion

The OCI specifications provide a consistent and reliable framework for containers images and artifacts across various environments. They standardize runtime, image, and distribution, ensuring predictable, secure container operations, irrespective of the infrastructure.

This standardization promotes innovation, collaboration, and scalability in container images and artifacts deployments.

References

--

--