Skip to main content
BlogSecurity

Software Supply Chain Security: SBOMs and Sigstore

How to generate SBOMs, sign artifacts with Sigstore and cosign, verify SLSA provenance, and enforce all of it as a CI gate.

Matthews Wong

Software Engineer

July 202610 min read
Software Supply Chain Security: SBOMs and Sigstore

Photo by Jim Bahn on flickr

In 2020 attackers compromised SolarWinds' build server and slipped a backdoor into a signed Orion update that reached roughly 18,000 customers, including US federal agencies. In 2024 a maintainer spent years earning trust on the xz-utils compression library before quietly adding an SSH backdoor to the release tarballs. Neither attack exploited a bug in application code. Both exploited the fact that most teams have no reliable way to answer a simple question: what exactly did we ship, and can we prove nobody tampered with it between the last line of code and the running container.

Closing that gap takes three pieces working together: a software bill of materials that lists every component in an artifact, a cryptographic signature that proves the artifact came from your pipeline and not an attacker's, and a provenance attestation that proves how it was built. This post walks through generating SBOMs with Syft, signing container images keylessly with Sigstore's cosign, understanding the SLSA levels that describe build integrity, and wiring all three into a CI gate that blocks unsigned or unverified artifacts from reaching production.

Why supply chain security became a build-pipeline problem

Modern applications are assembled, not written. A typical Node.js service pulls in hundreds of transitive dependencies, a base container image, and a handful of CI actions pinned by tag rather than hash. Every one of those inputs is a place an attacker can inject code without ever touching your repository. The attack surface has moved from "can someone find a bug in my code" to "can someone corrupt one of the hundred things my build depends on."

  • Dependency confusion: publishing a malicious package with the same name as an internal private package so the resolver pulls the public one instead.
  • Typosquatting: registering packages with names one character off from popular libraries, betting on a fat-fingered install command.
  • Compromised maintainer accounts: an attacker gains publish rights to a legitimate, widely used package and slips malicious code into a routine-looking release.
  • Unsigned build artifacts: a container image or binary with no cryptographic link back to the commit, pipeline, or identity that produced it, so a swapped artifact in the registry is indistinguishable from the real one.

None of these are solved by writing better application code. They are solved by making the build pipeline itself produce evidence: a list of what went into the artifact, a signature proving who built it, and a record of how it was built that a third party can independently check.

A CVE scan on your source repository tells you nothing about what actually shipped. Dependencies get resolved, bundled, and baked into a container image well after your last code review. Always generate and scan the SBOM from the final build artifact, not from package.json or a lockfile alone.

Generating an SBOM with Syft and CycloneDX

A software bill of materials is a structured, machine-readable inventory of every package, library, and version baked into an artifact. The two dominant formats are CycloneDX, an OWASP standard built primarily for security and vulnerability workflows, and SPDX, a Linux Foundation standard that started with license compliance and has since added security fields. Most tooling supports both, so pick whichever your downstream vulnerability scanner or compliance process consumes.

Syft is the most widely used open-source SBOM generator. It can scan a container image, a filesystem, or an archive, and it detects package managers for most common ecosystems automatically, including npm, pip, Go modules, and OS packages inside a base image. Generate the SBOM from the exact image you are about to push, then feed that SBOM into a vulnerability scanner like Grype so new CVEs against fixed dependency versions surface even after the image has already shipped.

# Generate a CycloneDX SBOM for a container image with Syft
syft packages docker:myorg/myapp:1.4.0 \
  --output cyclonedx-json=sbom.cdx.json

# Or scan a source directory directly
syft packages dir:. \
  --output spdx-json=sbom.spdx.json

# Scan the generated SBOM for known CVEs with Grype
grype sbom:sbom.cdx.json --fail-on high

Store the SBOM as an OCI artifact alongside the image digest, not as a build log attachment. That way anyone who pulls the image later can retrieve the exact SBOM for that specific digest, even months after the CI run that produced it has been garbage collected.

Signing artifacts with Sigstore and cosign

Sigstore is a set of free, public-good infrastructure for signing and verifying software: Fulcio issues short-lived signing certificates tied to an OpenID Connect identity instead of a long-lived private key, and Rekor records every signature in an append-only transparency log anyone can audit. Cosign is the command-line client that ties these together for container images and other artifacts.

The practical win is keyless signing. Instead of generating, rotating, and protecting a private key that some CI runner has to hold, cosign requests a certificate from Fulcio using the CI job's own OIDC token, signs the artifact with an ephemeral key that is discarded immediately after, and publishes the signature plus a Rekor transparency log entry. There is no long-lived secret to leak, and the signature is bound to a verifiable identity, such as the exact GitHub Actions workflow file and branch that produced it.

# Keyless signing inside GitHub Actions (OIDC identity, no private key to manage)
- name: Sign image with cosign
  run: |
    cosign sign --yes \
      ${{ steps.build.outputs.digest }}

# Attach the SBOM as a signed attestation on the same digest
cosign attest --yes \
  --predicate sbom.cdx.json \
  --type cyclonedx \
  myorg/myapp@sha256:${DIGEST}

# Verify later, from any machine, against the recorded identity
cosign verify \
  --certificate-identity="https://github.com/myorg/myapp/.github/workflows/release.yml@refs/heads/main" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  myorg/myapp@sha256:${DIGEST}

You can go further and attach the SBOM itself as a signed attestation on the same image digest with cosign attest. That single command produces one cryptographically verifiable object that says: this exact SBOM describes this exact image, and it was signed by this exact pipeline identity.

Verifying provenance with SLSA

SBOMs describe what is in an artifact and signatures prove who published it, but neither tells you how it was built or whether the build process itself could have been tampered with. That is what SLSA, Supply-chain Levels for Software Artifacts, addresses. It defines a build track with four levels, each adding a stronger guarantee about the integrity of the build platform and the provenance it emits.

LevelWhat it requiresWhat it protects against
Build L0No requirements. Typical of a build run on a developer laptop with no recorded provenance.Nothing. There is no way to know how or where the artifact was produced.
Build L1The build platform automatically generates provenance describing the build process and top-level inputs, and distributes it to consumers.Accidental mistakes, since provenance exists but is not tamper-proof.
Build L2Builds run on a hosted, shared build platform, and the provenance is signed by that platform.Post-build tampering with the artifact or its provenance, since the signature detects modification.
Build L3The build platform isolates each run so builds cannot influence one another, and keeps the signing key inaccessible to user-defined build steps.Tampering during the build itself and insider threats from within a single build job.

Most teams building on GitHub Actions or GitLab CI with hosted runners and a signed provenance step already sit around Build L2. Reaching L3 typically means adopting a dedicated build service designed for isolation, such as GitHub's native attestations feature or a hardened build platform, rather than a general-purpose CI runner.

Enforcing it as a CI gate

None of this protects anything if it only produces artifacts nobody checks. The value comes from making signature and SBOM verification a hard gate, both in CI before a merge and at deploy time before an image reaches a cluster.

  1. Generate the SBOM immediately after the image build step, from the pushed digest, and fail the job if the scanner finds a high or critical severity vulnerability with a known fix available.
  2. Sign the image and attach the SBOM attestation with cosign as the final CI step, so only images that passed every prior check ever get a valid signature.
  3. Add a policy-controller or admission-webhook check in the deployment cluster that rejects any image without a valid cosign signature matching an allow-listed OIDC identity, such as your release workflow's specific path and branch.
  4. Alert, rather than silently pass, when an image is deployed with a signature from an unexpected identity, for example a manual push from a laptop instead of the CI pipeline, even if the signature itself is technically valid.

The last step matters more than it looks. A valid cosign signature only proves the image was signed by whatever identity requested the certificate. If your gate accepts any valid signature rather than one from an explicitly allow-listed identity, an attacker who compromises a single developer's OIDC-capable environment can still sign and ship a malicious image.

Pinning GitHub Actions by tag, such as v4, is not enough. Tags are mutable and can be repointed by anyone with write access to that action's repository. Pin third-party actions by full commit SHA, and treat your CI workflow files with the same scrutiny as production code, since they are part of the trusted build path SLSA is meant to protect.

Putting it together

SBOMs, signing, and provenance are not three separate initiatives, they are one pipeline: generate the SBOM from the built artifact, scan it for known vulnerabilities, sign the artifact and the SBOM together with cosign's keyless flow, and gate every deployment on verifying that signature against an identity you explicitly trust. None of the individual pieces is exotic technology anymore. What most teams are missing is simply wiring them together as a gate instead of leaving them as optional reports nobody reads.

Start with SBOM generation and vulnerability scanning even if you are not ready to enforce signing yet. The visibility alone, knowing exactly which base images and packages you ship, catches real problems long before you get to the harder work of identity-based signature enforcement.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is an SBOM and why does my CI pipeline need one?

A software bill of materials is a structured inventory listing every package, library, and version baked into a build artifact. Your CI pipeline needs one because vulnerability scans on source code alone miss dependencies that get resolved, bundled, and added to the final container image after code review, so the SBOM must be generated from the actual artifact you ship, not just a lockfile.

Why use Sigstore's keyless signing instead of a traditional private key?

Keyless signing removes the operational burden of generating, storing, and rotating a long-lived private key that a CI runner would otherwise need to protect. Cosign requests a short-lived certificate from Fulcio using the CI job's own OIDC token, signs with an ephemeral key that is discarded immediately, and the signature is bound to a verifiable identity such as the exact workflow file and branch that produced it, so there is no secret to leak in the first place.

How is CycloneDX different from SPDX for generating an SBOM?

CycloneDX is an OWASP standard built primarily around security and vulnerability workflows, with native support for vulnerability data and a compact, lightweight model. SPDX originated as a Linux Foundation project focused on open-source license compliance and later added security fields. Most modern SBOM tools, including Syft, support generating either format, so pick whichever your downstream vulnerability scanner or compliance process expects.

Can I enforce signature verification before a container reaches production?

Yes, this is typically done with a policy-controller or admission-webhook check inside the deployment cluster that inspects every incoming image and rejects any without a valid cosign signature matching an explicitly allow-listed OIDC identity, such as your release workflow's specific repository path and branch. Accepting any valid signature rather than one from a trusted identity defeats the purpose of the gate.

What does reaching SLSA Build Level 3 actually require?

SLSA Build Level 3 requires the build platform to isolate each run so that concurrent builds cannot influence one another, and to keep the signing key used for provenance completely inaccessible to user-defined build steps. Most teams on general-purpose CI runners sit around Build Level 2 and need a dedicated, hardened build service, such as GitHub's native attestations feature, to reach Level 3.