Safeguard
DevSecOps

CI/CD Supply Chain Attacks Explained: Anatomy and Defense

From SolarWinds to tj-actions, CI/CD pipelines are where one foothold reaches thousands of victims. This guide explains the anatomy of a pipeline supply chain attack and the layered defenses that stop it.

Daniel Osei
DevSecOps Engineer
Updated 7 min read

A CI/CD supply chain attack is an attack that compromises the systems that build and ship your software, rather than the software itself, so that malicious code is signed, packaged, and distributed with your own legitimate identity. The reason attackers love it is asymmetry: one foothold in a build pipeline reaches everyone downstream who trusts your output. The SolarWinds Sunburst attack, disclosed in December 2020, proved the model — attackers sat inside the build environment for months and inserted a backdoor into the Orion update process that reached roughly 18,000 customers, including U.S. federal agencies. Five years later the March 2025 tj-actions compromise (CVE-2025-30066) showed the same pattern at ecosystem scale: a malicious change to a popular GitHub Action, propagated through a mutable tag, dumped CI secrets across roughly 23,000 repositories, with the initial access traced to a leaked token from a second compromised action, reviewdog (CVE-2025-30154). This guide explains how these attacks work and how to defend against them.

The anatomy of a pipeline supply chain attack

Most pipeline compromises follow the same five stages, regardless of platform:

  1. Initial access. The attacker gets a foothold — a leaked token (tj-actions), a compromised maintainer account, a malicious or typosquatted dependency, or a poisoned build tool. The 2024 xz-utils backdoor (CVE-2024-3094) is the extreme case: an attacker spent roughly two years building trust as a co-maintainer before slipping a backdoor into liblzma that targeted OpenSSH.
  2. Pipeline foothold. The malicious code executes inside CI, where scrutiny is low and trust is high. A tampered build script — as in the 2021 Codecov Bash Uploader compromise — runs in thousands of customer pipelines undetected.
  3. Privilege via ambient secrets. The build job already holds cloud keys, registry tokens, and signing credentials, so the attacker inherits them without escalating.
  4. Tamper. The attacker modifies the build output, exfiltrates secrets, or both. Because it happens inside your trusted build, the output is signed and packaged as legitimate.
  5. Distribution. Every downstream consumer that trusts your artifact receives the compromise. The 2023 3CX incident was a cascade — a compromised upstream vendor led to a poisoned 3CX build that then reached 3CX's own customers.

Understanding these stages is what turns a scattered checklist into a layered defense, because each control below breaks a specific stage — which is really the short answer to how to mitigate supply chain attacks: match a control to the stage it breaks, rather than reaching for one silver-bullet fix.

Defense 1: eliminate mutable references (breaks stages 1-2)

Mutable references are how a single upstream compromise becomes yours automatically. Pin third-party actions, orbs, tasks, and template repositories to immutable commit SHAs so a re-pointed tag cannot change the code you run.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # Immutable SHA pin - a re-pointed tag cannot alter this
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0

Repositories pinned to a SHA were unaffected by the tj-actions tag re-pointing; those on floating tags pulled the malicious code on the next run. This one control would have stopped the propagation stage of that attack.

Defense 2: least-privilege pipelines (breaks stage 3)

An attacker who reaches your pipeline inherits whatever it can do, so give each job the minimum. Default CI tokens to read-only and grant write scopes narrowly, per job.

permissions:
  contents: read          # workflow-wide default

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write      # only the publish job can write packages

A read-only job that gets code execution cannot rewrite your branch or push a release, which contains the blast radius even after a foothold.

Defense 3: provenance and verification (breaks stages 4-5)

Sign what you build and verify signatures before you deploy or consume, so a tampered artifact fails a check rather than shipping. Frameworks like SLSA formalize this: generate signed build provenance (an attestation of what was built, from what source, by which builder) and enforce a policy that rejects artifacts without valid provenance. Cosign with keyless Sigstore signing and in-toto attestations make this practical across container images and language packages. Verification is what turns "we hope the build was clean" into "we can prove it, or we do not ship."

Secrets and OIDC (breaks stage 3)

The single most effective way to blunt stage 3 is to have no long-lived secrets for the attacker to inherit. OIDC federation lets each job exchange a short-lived signed token for temporary cloud credentials at runtime.

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/deploy
          aws-region: us-east-1

Scope the cloud trust policy to your repository and branch so a fork cannot assume the role. A credential that expires in minutes and works from one branch is a fraction of the risk of a static key the attacker can carry away.

Adding Safeguard scanning to the pipeline

The stages above are prevention; you also need detection for the malicious or vulnerable dependency that slips through. Add a step that runs the Safeguard CLI on every pull request.

- name: Run Safeguard scan
  env:
    SAFEGUARD_TOKEN: ${{ secrets.SAFEGUARD_TOKEN }}
  run: |
    curl -sSfL https://get.safeguard.sh/install.sh | sh
    safeguard scan --fail-on high --sbom cyclonedx

Safeguard's SCA engine generates an SBOM on every build and prioritizes findings by reachability, so you can answer the critical post-incident question — "are we affected, and where" — in seconds instead of days. Where a fix exists, Auto-Fix opens the upgrade pull request automatically.

Defense checklist

If you only take one list away from this guide on how to mitigate supply chain attacks, make it this one:

  • Pin all third-party actions, orbs, tasks, and templates to immutable SHAs
  • Default CI tokens to read-only; grant write scopes per job
  • Generate and verify signed build provenance (SLSA, Sigstore, in-toto)
  • Replace static cloud keys with OIDC scoped to your repo and branch
  • Make runners ephemeral so a foothold cannot persist
  • Generate an SBOM on every build and prioritize findings by reachability
  • Require review for changes to pipeline definitions

Frequently Asked Questions

How is a CI/CD supply chain attack different from a normal breach?

The difference is trust and reach. In a normal breach the attacker compromises one target. In a supply chain attack they compromise your build system, so the malicious output is signed with your legitimate identity and distributed to everyone who trusts you. SolarWinds reached 18,000 organizations from a single build-system foothold.

If I pin dependencies, am I safe from the xz-utils style of attack?

Pinning defends against mutable-reference propagation, but the xz-utils backdoor was introduced by a trusted maintainer into a specific release, so a pin would have pinned you to the backdoored version. Defense there needs behavioral and reachability analysis plus provenance verification, not pinning alone — which is why layered defense matters.

What is build provenance and why does it matter?

Provenance is a signed, verifiable record of how an artifact was built: from which source commit, by which builder, with which inputs. It matters because it lets a deploy step reject anything that was not built by your legitimate pipeline, breaking the distribution stage of an attack. SLSA is the framework that standardizes provenance levels.

How does Safeguard fit into this defense-in-depth model?

Safeguard covers the detection and response layers: continuous SBOM generation, reachability-based prioritization, and the Griffin AI investigation engine that correlates SBOMs, pipeline config, and commit provenance to flag anomalies before they ship. See how it compares on the comparison page.

To build this defense, see Safeguard CLI, SCA, Auto-Fix, Griffin AI, the comparison page, and full setup steps in the documentation at docs.safeguard.sh.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.