CI/CD pipelines have quietly become one of the highest-value targets in the software supply chain, because a single compromised workflow can push malicious code to every downstream consumer at once. In March 2025, the GitHub Action tj-actions/changed-files was compromised (CVE-2025-30066), silently modifying its code to dump CI secrets into public workflow logs across more than 23,000 repositories before anyone noticed. In 2021, attackers altered the Codecov Bash Uploader script for roughly two months, exfiltrating tokens and credentials from every customer pipeline that ran it, including HashiCorp and Twilio. These weren't exotic zero-days — they were pipelines with too much trust, too little verification, and no one watching what actually executed. Below are eight concrete practices to close those gaps, organized around the questions security teams ask most when they start hardening their build systems.
What makes CI/CD pipelines such an attractive attack target?
CI/CD pipelines are attractive because they combine broad access with weak scrutiny: a build job routinely holds cloud credentials, signing keys, and source code access that would trigger alarms anywhere else, yet it's treated as "just automation." The SolarWinds Sunburst attack, disclosed in December 2020, proved the model at scale — attackers sat inside SolarWinds' build environment for months, inserting a backdoor into the Orion software update process that reached roughly 18,000 customers, including nine U.S. federal agencies. A pipeline compromise doesn't just hit one target; it hits everyone who trusts your build output. That asymmetry — one foothold, thousands of victims — is why pipeline security now sits alongside application security as a board-level concern rather than a DevOps nicety.
How do you keep secrets out of your CI/CD pipeline? (Tips 1-2)
You keep secrets safe by never hardcoding them and by replacing long-lived credentials with short-lived, scoped tokens issued per job. Tip 1: run automated secret scanning on every commit and pull request, not just on merge to main — GitGuardian's 2024 State of Secrets Sprawl report found over 12.7 million hardcoded secrets exposed on public GitHub in 2023 alone, and internal repos fare no better without enforcement. Tip 2: adopt OpenID Connect (OIDC) federation so your pipeline requests a temporary cloud credential at runtime instead of storing a static AWS or GCP key in a CI variable. GitHub Actions, GitLab CI, and CircleCI have all supported OIDC-to-cloud-provider federation since 2021-2022, so there's no reason a workflow should still hold a credential that's valid for 90 days when it only needs to be valid for 90 seconds.
How do you stop a malicious or compromised third-party action from running?
You stop it by pinning every third-party action or plugin to an immutable commit SHA instead of a mutable version tag, and by restricting which marketplace actions are allowed to run at all. The tj-actions incident is the canonical case study: repositories that referenced tj-actions/changed-files@v35 (a floating tag) automatically pulled the compromised code the moment the tag was repointed, while repositories pinned to a specific SHA were unaffected by the retroactive tampering. Tip 3: enforce SHA-pinning via a policy check in your workflow linter — GitHub's own actionlint and OpenSSF's pin-github-action tool both automate this. Tip 4: use an allowlist (via GitHub's "Allow actions created by GitHub" plus a verified-creator list, or GitLab's CI/CD component catalog controls) so a compromised or typosquatted action published by an unknown developer can't be added to a workflow without review.
How do you prevent unauthorized code from ever reaching your build?
You prevent it with branch protection rules that require signed commits and at least one independent code review before a change can trigger a build against production infrastructure. Tip 5: require pull request reviews from a second engineer for any change to .github/workflows/, .gitlab-ci.yml, or equivalent pipeline definitions — pipeline config is code with root-equivalent power, and it deserves stricter review than a UI tweak, not looser review. Tip 6: enable commit signing (GPG, SSH, or Sigstore's keyless signing via gitsign) and enforce it at the branch-protection level so an attacker who steals a developer's session token still can't push an unsigned, unverified commit that the pipeline will build. The 2024 xz-utils backdoor (CVE-2024-3094) shows why authorship matters: the malicious code was added by an account that had spent roughly two years building trust as a co-maintainer before slipping a backdoor into liblzma 5.6.0 and 5.6.1, targeting OpenSSH sessions on Linux systems.
How do you harden the runners and environments where builds actually execute?
You harden them by making runners ephemeral and single-use, and by giving each build the minimum permissions it needs and nothing more. Tip 7: switch from persistent, long-lived self-hosted runners to ephemeral runners that spin up fresh for each job and are destroyed immediately after — a persistent runner that executes a malicious job once stays compromised for every job after it, since cached credentials, installed tooling, and filesystem state carry forward. GitHub Actions, GitLab, and Buildkite all support ephemeral/autoscaling runner modes natively as of 2023. Pair this with least-privilege GITHUB_TOKEN / CI job token scopes (read-only by default, write access granted only to the specific jobs that need to push tags or publish packages) so a compromised job can't pivot into modifying your repository, registry, or release pipeline even if it does get code execution.
How do you know which CI/CD and dependency vulnerabilities actually deserve your attention?
You know by tracing whether a vulnerable component is actually reachable and executed in your build and runtime paths, not by triaging every CVE that shows up in a scan. Tip 8: generate and continuously ingest a Software Bill of Materials (SBOM) for every build artifact — in CycloneDX or SPDX format — so you have a real-time, versioned inventory of what's actually shipping, then layer reachability analysis on top so your team spends its limited hours on the small fraction of findings that are exploitable in context. A 2023 study cited by the OpenSSF found that fewer than 10% of dependency vulnerabilities flagged by traditional SCA tools are actually reachable from application code, meaning teams chasing every CVE alert are burning 90%+ of their remediation effort on noise. Without an SBOM tied to reachability, teams can't answer the single most important post-incident question fast enough: "are we affected by this, and where?"
How Safeguard Helps
Safeguard is built around the gap those eight tips expose: knowing what's in your pipeline isn't the same as knowing what's exploitable. Our reachability analysis traces vulnerable dependencies and compromised packages (like the tj-actions or xz-utils scenarios above) down to whether they're actually invoked in your build and runtime code paths, cutting alert volume to the fraction that matters. Griffin AI, Safeguard's investigation engine, correlates SBOM data, pipeline configuration, and commit provenance to flag anomalies — like a newly re-pinned action tag or an unreviewed workflow change — before they reach production. Safeguard generates and ingests SBOMs automatically on every build so you always have a current, queryable inventory, and where a fix exists, it opens an auto-fix pull request with the corrected dependency version or pinned SHA already applied, turning a multi-day remediation cycle into a single review-and-merge.