Payload Logo
Artifact Management

My First Open Source Contribution: Fixing Grype OCI Scans in Artifact Keeper

Author

Ramy Bouchareb

Date Published

Introduction

I recently made my first open source contribution, and it started from something very practical: I was building a Proof of Concept around Artifact Keeper to evaluate how it could fit into the infrastructure at my job.

While testing the flow for storing and scanning OCI/Docker images, I ran into a scan failure that did not look like a local setup issue. After digging into it, I found that the behavior matched an existing open issue in Artifact Keeper.

The issue was reported as artifact-keeper/artifact-keeper#2053 . Since I was already trying to understand the problem for the PoC, I decided to contribute a fix upstream instead of only working around it locally.

In short, Grype scans for Docker/OCI images stored inside Artifact Keeper could fail with an authentication error.

The interesting part was not just the error itself. The fix required understanding how Artifact Keeper stores OCI images, how Grype accepts scan targets, and why routing a scanner through the same authenticated registry endpoint used by external clients can be fragile.

The Problem

Artifact Keeper can store OCI/Docker images in a local Docker repository. For example, an image might be pushed as:

localhost:8080/docker-local/alpine:3.20

When Artifact Keeper triggered a Grype scan for that image, the scanner was invoked with a registry target similar to:

registry:localhost:8080/docker-local/alpine:3.20

That caused Grype to make an HTTP request back into Artifact Keeper's registry API:

GET /v2/docker-local/alpine/manifests/3.20

But that endpoint requires authentication. Since Grype was not given Artifact Keeper credentials for that internal registry request, the scan failed with:

UNAUTHORIZED: authentication required

Why This Was Subtle

At first glance, it looks like an authentication bug: "just pass credentials to Grype."

But that is not necessarily the best fix.

Artifact Keeper already owns the image and stores the manifest and blobs locally. Making Grype call back into Artifact Keeper's authenticated registry API means the scanner is treated like an external client, even though the data is already available inside the system.

That creates several problems:

  • It couples internal scanning to public registry authentication behavior.
  • It requires some form of scanner credential handling.
  • It risks leaking or logging sensitive tokens if implemented carelessly.
  • It fails even though the artifact is already present in local storage.

The better direction was to avoid the internal registry route when possible.

How Grype Can Scan Images

Grype supports several source types. Some common examples are:

registry:<image>
oci-dir:<path>
oci-archive:<path>
docker-archive:<path>
sbom:<path>
dir:<path>

The existing path used registry:<image>, which forced Grype through the authenticated /v2/... endpoint.

The fix direction was to use:

oci-dir:<path>

That means Artifact Keeper can build a temporary local OCI image layout in the scan workspace, then point Grype at that local directory.

OCI Layout in Brief

An OCI image layout is a filesystem representation of an image. At a high level, it looks like this:

oci-layout
index.json
blobs/
sha256/
<manifest-digest>
<config-digest>
<layer-digest>

For Grype, this is enough to inspect the image without pulling it from a registry.

Suggested diagram: registry scan path versus local OCI layout scan path.

Reproducing the Bug

The reproduction flow was:

  1. Start Artifact Keeper from current source using Docker Compose.
  2. Create a local Docker repository named docker-local.
  3. Push a small image, such as alpine:3.20.
  4. Trigger a Grype scan for the stored artifact.
  5. Watch backend logs for Grype errors.

The failing log looked like this:

Grype OCI registry scan target: registry:localhost:8080/docker-local/alpine:3.20
GET /v2/docker-local/alpine/manifests/3.20
UNAUTHORIZED: authentication required

That confirmed the scanner was going through the internal registry endpoint and hitting auth.

The Fix Direction

The fix was to materialize a temporary OCI layout in the scan workspace.

Instead of this:

Grype -> registry:localhost:8080/docker-local/alpine:3.20
-> Artifact Keeper /v2/... endpoint
-> authentication failure

Artifact Keeper now prefers this:

Artifact Keeper storage -> temporary OCI layout -> Grype oci-dir:<path>

The flow becomes:

  1. Detect that the artifact is an OCI image.
  2. Find the manifest digest.
  3. Look up referenced config and layer blobs from Artifact Keeper's OCI metadata tables.
  4. Write a temporary OCI layout into the scan workspace.
  5. Invoke Grype with oci-dir:<layout-path>.
  6. Clean up the temporary workspace.
  7. Fall back to registry: only when local blob metadata is genuinely unavailable.

Key Implementation Pieces

The first implementation focused on single-arch image manifests.

The important code path was in:

backend/src/services/grype_scanner.rs

The scanner now builds a temporary layout containing:

  • oci-layout
  • index.json
  • the image manifest blob
  • referenced config blobs
  • referenced layer blobs

The local blob references come from tables such as:

  • manifest_blob_refs
  • oci_blobs

The scanner target also needed access to repository-local context:

backend/src/services/scanner_service.rs

ScanTarget was extended so scanners can inspect local artifact state without going back through the public registry route.

Multi-Arch Images: The Important Follow-Up

The maintainer review caught an important real-world case: multi-arch images.

Images like alpine:3.20 are commonly stored as image indexes. An image index points to one or more platform-specific child manifests.

That matters because manifest_blob_refs intentionally has no rows for index manifests. The actual config and layer blobs belong to the platform child manifest.

So for an image index, the scanner needs to:

  1. Resolve the index to a platform child manifest.
  2. Load the child manifest body.
  3. Build the local OCI layout from the child manifest's blob references.
  4. Point index.json at the child digest.

The maintainers pushed a follow-up commit on top of my branch to close that multi-arch gap while keeping my commits intact.

This was a useful lesson: a fix can be correct for a narrow reproduction and still miss the common production case.

Validation

The final behavior was validated against real Grype.

For the fixed flow, backend logs showed:

Grype OCI local layout scan target: oci-dir:/scan-workspace/grype-oci/<artifact-id>
Grype OCI local layout scan complete for alpine:3.20
Scan grype completed

Most importantly, the logs no longer showed:

registry:localhost:8080/docker-local/alpine:3.20
UNAUTHORIZED: authentication required

The maintainers also verified:

  • single-arch image scans complete through oci-dir:
  • multi-arch index scans complete through oci-dir:
  • registry fallback still exists when local metadata is unavailable
  • formatting, clippy, tests, and duplication checks pass

What I Learned

This contribution taught me a lot about how real open source work happens.

Some of the biggest lessons:

  • Reproducing the bug is as important as writing the fix.
  • The best fix is often not the most obvious one.
  • Internal service flows should avoid unnecessary auth boundaries when the data is already local.
  • Tests need to cover the real production shape, not only the simplest case.
  • Maintainer review can improve both correctness and code quality.

I also got more practice with:

  • Rust service code
  • OCI image storage concepts
  • Docker registry behavior
  • Grype scanner sources
  • SQL-backed metadata lookup
  • CI feedback loops

Final Thoughts

This was my first open source contribution, and it was a good one because it was not just a typo fix or documentation change. It involved understanding a real bug, reproducing it, testing a fix, and iterating with maintainers.

The final solution helps Artifact Keeper scan locally stored OCI images more reliably by avoiding unnecessary authenticated registry calls and using a local OCI layout instead.

Thanks to the Artifact Keeper maintainers for the review, guidance, and follow-up work to get the fix ready for the 1.2.3 milestone.

Useful Links

  • Artifact Keeper repository
  • Issue #2053: Grype OCI scan fails because internal registry endpoint requires authentication
  • Grype repository
  • OCI Image Layout Specification


Kubernetes

How pod QoS classes, the scheduler's blind spot, and vertical autoscaling work together illustrated with real overcommitment data from a prod cluster.