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 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.
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.
| 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.
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.
- 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.
- 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.
- 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.
- 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.

