Software Supply Chain Security: SBOMs and Sigstore

Photo by Jim Bahn on flickr
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.
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.
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.
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.
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.

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.
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."
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.
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 highStore 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.
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.
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.
| Level | What it requires | What it protects against |
|---|---|---|
| Build L0 | No 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 L1 | The 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 L2 | Builds 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 L3 | The 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.
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.
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.
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.