Skip to main content

ORAS: Ship More Than Just Code

·810 words·4 mins
Rich
Author
Rich
Build. Break. Understand.

I have a confession to make. I used to think container registries had one job: store container images. Spoiler: not exactly true. ORAS, short for “OCI Registry As Storage”, is a CNCF project that lets you use a registry for more than images. But before we get there, let’s roll back a couple of steps.

Typically (simplified), this is how we ship code inside an image:

  • It all starts with a Dockerfile, a list of instructions on how to create a container image.
  • Content (your code) is typically added towards the end.
  • Commands tell the container how to launch that content.
  • The Dockerfile goes through a build stage, which produces a container image and tags it as myimage:today.
  • You push your image to a container registry, and if it’s public anyone can use it.

When someone issues a command like

docker run myimage:today

If the image is missing, Docker will pull it from Docker Hub by default. Docker assumes Docker Hub, but other registries can be used.

Problem Time
#

What if you want to bundle supplemental data with the image?

ORAS has entered the chat. Instead of reaching for a separate object store, an S3 bucket, or a file server, you can store related files in the same registry and link them back to the image: SBOMs, signatures, attestations, configs, documentation. They travel with the image. They are versioned alongside it. No extra infrastructure, but how?

What is this magic?
#

The OCI image spec defines an image as a manifest pointing at config and layers. The OCI distribution spec also defines the referrers API: a way to discover manifests that point back to another manifest as their subject. Each referrer is tagged by its artifact type so consumers can filter for what they want.

ORAS wraps all of this into four commands you will use in practice:

Command What it does
oras attach Attach a file to an existing image as an OCI artifact
oras discover List artifacts attached to an image
oras pull Download a specific artifact by digest
oras push Push a standalone artifact (not attached to an image)

The attach model is the interesting one. You run oras attach after your image has been built and pushed. ORAS creates a new OCI manifest, with your file as a layer, and links it back to the image via the referrers API. Modern registries can expose this through the OCI 1.1 referrers API; ORAS can also use a tag-based fallback where needed. The original image is untouched. Consumers who do not know about the attachment see nothing different. Consumers who call oras discover get the full list.

Real Examples: hugo-builder and caddy-cloudflare
#

Both of my public images on Codeberg, hugo-builder and caddy-cloudflare, attach a CycloneDX SBOM to every build. The CI pipeline does this automatically on every push to main, and on a weekly schedule.

What the CI does
#

After building and pushing the image, the pipeline:

  1. Installs Syft and generates a CycloneDX SBOM from the image.
  2. Installs ORAS.
  3. Attaches the SBOM to the image with oras attach.
  4. Installs Grype and runs a vulnerability scan against the SBOM.

The attach step looks like this:

oras attach \
  --artifact-type application/vnd.cyclonedx+json \
  codeberg.org/richj/hugo-builder:latest \
  sbom.json:application/vnd.cyclonedx+json

The first argument after --artifact-type tells any tooling what kind of thing this is. The last argument is <local-file>:<media-type>. ORAS uses the media type to set the layer descriptor in the manifest.

Discovering and pulling the SBOM yourself
#

Once the SBOM is attached, anyone can inspect it without needing to pull the full image.

Discover what is attached:

oras discover codeberg.org/richj/hugo-builder:latest

Or, if you want to be precise about a specific build, resolve the image tag first and discover against the digest:

oras resolve codeberg.org/richj/hugo-builder:latest
oras discover codeberg.org/richj/hugo-builder@sha256:<image-digest>

You will get output like this, showing the artifact type and digest:

codeberg.org/richj/hugo-builder:latest
└── application/vnd.cyclonedx+json
    └── sha256:abc123...

Pull the SBOM using that digest:

oras pull codeberg.org/richj/hugo-builder@sha256:<sbom-digest>

This writes sbom.json to your current directory. You can then scan it locally:

grype sbom:sbom.json

The same commands work identically for caddy-cloudflare:

oras discover codeberg.org/richj/caddy-cloudflare:latest
oras pull codeberg.org/richj/caddy-cloudflare@sha256:<sbom-digest>

Why this is useful
#

The SBOM travels with the image digest. If someone resolves hugo-builder:latest today and keeps the digest, they can discover the SBOM attached to that exact image six months from now and scan it against current vulnerability databases. They do not need to trust me to host a separate SBOM file somewhere. They do not need to check a separate URL. The registry holds everything.

For supply chain security, this is the right model. You know what is in the image. You can verify it yourself. And the tooling to do it is three commands.

That is the bit I like most: the registry stops being just a place where the image lives, and becomes a place where the evidence around the image lives too.

Related