Software Supply Chain Security in 2026: Pinning, Provenance, Signing, and SBOMs That Get Consumed

A few years ago, “supply chain security” meant checking your Docker base images for critical CVEs and hoping your dependency tree was boring. That bar is no longer close to adequate. Modern attacks rarely target your code — they target the machinery around it: the CI runner, the build cache, the GitHub Action at a floating @master tag, the npm package whose maintainer got phished. Thexz/xz-style backdoors and the ongoing wave of npm and PyPI package takeovers made one thing clear: an attacker who owns your build pipeline doesn’t need to own your repository.

This post is a practical tour of the defenses that actually matter in 2026: pinning and verifying every step of your build, generating provenance automatically, signing artifacts with Sigstore Cosign, scanning with Syft and Trivy, and producing SBOMs that someone actually consumes. Everything here fits into a normal GitHub Actions workflow without buying a platform.

Pin Everything — Including Your Actions

The cheapest and highest-leverage fix is also the most commonly skipped. A workflow that references actions/checkout@v4 is trusting a mutable tag: whoever controls the repository can move that tag to a commit that exfiltrates your secrets. Tag-rugging is a real attack vector — several malicious updates to popular actions have shipped exactly this way. The mitigation is to pin third-party actions to a full commit SHA, and to keep the version comment next to it so reviews stay readable:

steps:
  - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8  # v5.0.0
  - uses: aquasecurity/trivy-action@18f2510ea6a4d2f56cb3b11955d8173d2fb34f2e  # v0.36.0
    with:
      image-ref: myapp:ci
      severity: CRITICAL,HIGH
      exit-code: "1"

Pin by SHA for every third-party action, and let Dependabot or Renovate keep those SHAs updated with PRs you can review. First-party actions (the ones living in your own org) are a judgment call, but the rule of thumb holds: anything you did not write gets a SHA.

The same principle applies to package installs. A bare npm install inside CI silently respects anything a compromised maintainer publishes. Lockfiles (package-lock.json, go.sum, uv.lock) plus the install flags that enforce them — npm ci instead of npm install, Go’s -mod=readonly default — turn “whatever is newest” into “exactly what was reviewed.” Tools like Aikido Firewall go further and gate installs against known-malicious versions, but lockfile discipline alone closes the accidental-upgrade hole.

Provenance: Proof the Artifact Came From Your Pipeline

Pinning protects the inputs of your build. Provenance protects the claim about the output: this container image was produced by this workflow, from this commit, using these inputs. The open standard here is SLSA, which grades provenance from Level 1 (the build exists and emits metadata) through Level 3 (hardened, isolated builds). You do not need to memorize the ladder — you need to know that GitHub can generate provenance for free, and that a verifiable claim beats a self-reported one.

For public repositories, GitHub emits provenance automatically for builds triggered through workflow_dispatch, push, and pull_request. For anything else — private repos, custom pipelines, artifact registries outside GHCR — use the official attestation action:

permissions:
  id-token: write
  contents: read
  attestations: write

steps:
  - run: go build -o dist/myapp ./cmd/myapp
  - uses: actions/attest-build-provenance@e8998f949152b193b067cb32968e610a15b2b316  # v4.2.2
    with:
      subject-path: dist/myapp

Note the permissions block: provenance is generated via keyless Sigstore signing, which requires id-token: write. The resulting attestation is stored against the repository and can be verified with the GitHub CLI:

gh attestation verify dist/myapp -R your-org/your-repo

That single command answers a question that used to be unanswerable: “is this binary actually the one CI built from commit abc123, or something a laptop produced and pushed?” Deploy pipelines can enforce it as a gate rather than treating it as a nice-to-have.

Sign Your Artifacts With Cosign

Provenance ties artifacts to builds; signatures tie artifacts to you. Cosign has become the de facto standard for container signing, and the keyless mode removes the part that killed most signing initiatives a few years ago — key management. In keyless mode, Cosign obtains a short-lived certificate from Sigstore’s Fulcio CA using your OIDC identity, and the signature plus certificate go into the transparency log (Rekor). Nothing to store, nothing to rotate, nothing to leak:

cosign sign --yes ghcr.io/your-org/myapp:v1.4.2

cosign verify ghcr.io/your-org/myapp:v1.4.2 \
  --certificate-identity-regexp 'https://github.com/your-org/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

The verify command is where the value is. It fails unless the image was signed by a GitHub Actions workflow under your organization. Wire that into your deployment manifests — Kubernetes policies via Kyverno or a simple gate in your CD step — and unsigned images stop being deployable at all. Detailed options are documented in the Cosign signing overview.

Sign commits too, if your threat model includes repo-level compromise: gitsign applies the same keyless model to Git commits, so a review history can be verified rather than trusted.

SBOMs That Get Consumed, Not Just Generated

Every serious incident response now starts with the same question: “where do we run the affected component?” If answering it takes two days of grepping Dockerfiles, you have no SBOM program — you have an SBOM file. An SBOM (Software Bill of Materials) is a machine-readable inventory of every component in an artifact, and the US federal baseline for suppliers is defined in CISA’s SBOM resources.

Generate one as part of the build with Syft and attach it to the image:

syft ghcr.io/your-org/myapp:v1.4.2 -o cyclonedx-json > sbom.cdx.json
cosign attach sbom --sbom sbom.cdx.json ghcr.io/your-org/myapp:v1.4.2

The consumption side matters more than generation. When the next log4shell lands, the query is:

grype sbom:sbom.cdx.json --only-fixed --fail-on high

Store SBOMs per release alongside the image digest — not in a wiki. Registry attachment (as above) keeps the inventory versioned with the artifact it describes, which is exactly what an incident responder needs at 3am.

Scanning Is a Gate, Not a Report

Scanners produce so much noise that most teams mute them. The fix is not better scanners — it’s policy discipline. Three rules that keep signal high:

  • Fail only on fixable, high-severity issues. Trivy’s --only-fixed (for SBOM-based scans via Grype above) or severity filters in the action avoid blocking on vulns with no available patch.
  • Scan at build time and continuously. A clean image on Monday can be vulnerable by Friday. A scheduled re-scan of deployed images catches the drift; Trivy, Grype, and registry-native scanning all support this.
  • Record a baseline, gate on the delta. Blocking on legacy debt fails on day one and gets disabled by Friday. Blocking on new issues keeps the pipeline green and the bleeding stopped.

For Go specifically, govulncheck beats generic scanners on signal because it traces whether vulnerable functions are actually reachable from your code — a CVE in a package you depend on but never call is noise, and govulncheck knows the difference:

govulncheck ./...

A Minimal End-to-End Workflow

Putting it together, here is the shape of a workflow that covers all four defenses. Trim to your stack; the ordering is the point — verify inputs, build, inventory, scan, sign, attest:

name: build-and-attest
on:
  push:
    tags: ["v*"]

permissions:
  contents: read
  packages: write
  id-token: write
  attestations: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8  # v5.0.0

      - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00  # v6.0.0
        with:
          go-version: "1.25"
          check-latest: true

      - run: go mod verify
      - run: govulncheck ./...
      - run: go build -trimpath -o dist/myapp ./cmd/myapp

      - uses: anchore/sbom-action@7c04f8c7dbe7fedb57e44b4b0e2b4b1d4b0a1d15  # v0.20.9
        with:
          artifact: dist/myapp
          format: cyclonedx-json
          upload-artifact: true

      - uses: aquasecurity/trivy-action@18f2510ea6a4d2f56cb3b11955d8173d2fb34f2e  # v0.36.0
        with:
          scan-type: fs
          scan-ref: .
          severity: CRITICAL,HIGH
          exit-code: "1"

      - uses: actions/attest-build-provenance@e8998f949152b193b067cb32968e610a15b2b316  # v4.2.2
        with:
          subject-path: dist/myapp

The OIDC token permissions (id-token, attestations) are required for provenance generation; everything else is least-privilege by default.

What to Do This Week

Supply chain security fails as a big-bang project and succeeds as a series of small, permanent gates. If you can only do three things: pin third-party actions to SHAs (one afternoon, closes a live attack vector), turn on build provenance for your release artifacts (one workflow change), and start attaching SBOMs to images so the next CVE query takes minutes instead of days. Signing with Cosign follows naturally once provenance is flowing — it reuses the same OIDC machinery. None of this requires new infrastructure; it requires treating your build pipeline as production, because that is exactly what it is.

Leave a Reply

Your email address will not be published. Required fields are marked *