Safeguard
DevSecOps

Hardening CI/CD Pipelines End to End

A leaked Codecov credential let attackers read CI secrets from 23,000+ customers for two months in 2021. Here's how to close every stage of that gap.

Safeguard Research Team
Research
8 min read

On January 31, 2021, an attacker used a leaked credential — planted through a flaw in how Codecov built its Docker images — to silently modify the company's Bash Uploader script. For roughly two months, every CI job that pulled and ran that script leaked its environment variables, including tokens, keys, and credentials, to an external server. Codecov didn't discover the tampering until April 1, 2021, when a customer noticed the script's hash no longer matched the one Codecov had published. By the company's own estimate, more than 23,000 customers were affected. The incident is a clean case study in why CI/CD pipelines deserve the same threat model as production infrastructure: a single compromised link — a build script, a stale token, a shared runner — gives an attacker a vantage point over every downstream build. This piece walks through four areas that, taken together, close most of the gap Codecov exposed: how you handle secrets, how you isolate runners, how you sign and verify what your pipeline produces, and how you gate what gets merged and deployed.

What actually made the Codecov breach possible?

The root cause wasn't a sophisticated exploit — it was a long-lived credential sitting somewhere it shouldn't have, embedded in an image-creation process that let an attacker extract it and push a modified script to production undetected. That pattern — a static, broadly-scoped secret with no expiry, no rotation, and no runtime verification — recurs across nearly every major CI-related breach, because a leaked API key or CI token typically works until someone notices and revokes it, which in Codecov's case took two months. The lesson security teams drew from the incident (documented in Codecov's own security update and covered extensively by outlets like SecurityWeek and GitGuardian) wasn't "audit your Docker build process" specifically — it was that any CI system trusting a static secret has an unbounded blast radius the moment that secret leaks, whether through a compromised build step, a misconfigured log, or a modified third-party script.

Why should CI secrets be short-lived instead of static?

Short-lived, cryptographically scoped credentials shrink the blast radius of exactly the failure mode Codecov hit: even if a token leaks into a build log or a tampered script, it expires in minutes rather than remaining valid for months. OIDC federation is the mechanism that makes this practical — GitHub Actions, GitLab CI, and most major CI platforms can now exchange a short-lived OIDC token for temporary cloud credentials (AWS STS, Azure Workload Identity, GCP Workload Identity Federation) scoped to a single job, with no static access key stored anywhere. This eliminates an entire category of risk: there's no long-lived AWS access key sitting in a CI secrets store for an attacker to exfiltrate in the first place. Where a static secret is unavoidable — a third-party SaaS API key, for instance — the compensating control is continuous verification: checking that the credential is still live and scoped correctly by calling the issuing service directly (an AWS sts:GetCallerIdentity call, a GitHub /user lookup, a Stripe /v1/balance check), rather than assuming a secret is safe just because it's stored in an encrypted vault.

How do you keep one build from contaminating the next?

Runner isolation matters because CI runners routinely execute untrusted code — a pull request from an external contributor, a dependency's install script, a Makefile you haven't read closely — with access to the same filesystem, cache, and sometimes network namespace as every other job on that host. The well-established mitigation is ephemeral, single-use runners: spin up a clean VM or container per job, destroy it when the job finishes, and never let state (cached credentials, environment variables, leftover build artifacts) persist across jobs from different repositories or trust levels. Shared, long-lived self-hosted runners are the pattern to avoid for anything that builds untrusted pull requests, because a malicious build step on one job can read whatever the previous job left behind on disk. GitHub's own guidance for self-hosted runners explicitly warns against using them on public repositories for this reason — a fork-based pull request can execute arbitrary code on a runner that also has access to your organization's other jobs and secrets.

How does artifact signing prove a build wasn't tampered with?

Artifact signing answers a question secrets management and runner isolation can't: once a build finishes, how does anyone downstream know it wasn't altered afterward, or that it came from the pipeline it claims to? Sigstore's keyless signing model — binding a short-lived certificate to a CI workload's OIDC identity rather than a long-lived private key — became the default answer for open-source ecosystems. npm rolled out Sigstore-backed package provenance to public beta in April 2023 and reached general availability on October 3, 2023; by GA, adoption during the beta period had already reached more than 3,800 projects and over 500 million downloads of provenance-enabled package versions, according to Sigstore's and GitHub's own published figures. Each signed provenance statement is published to Rekor, a public transparency log, so anyone — not just the publisher — can verify which build system produced a given artifact from which source commit, closing exactly the kind of silent-tampering gap that let the modified Codecov script go undetected for two months.

What do the SLSA levels actually require, and which one is realistic?

SLSA (Supply-chain Levels for Software Artifacts) gives teams a graded framework instead of a binary "signed or not" answer, and the levels differ by how much tampering they rule out rather than just whether provenance exists. Level 1 only requires that some provenance is generated. Level 2 requires a tamper-resistant, versioned build service — most CI platforms clear this by default. Level 3 is the meaningful jump for most organizations: it requires an isolated build environment with no way for one build to influence another's inputs, plus non-forgeable provenance generated by the build platform itself rather than by a script the build can tamper with. Level 4 adds hermetic, fully reproducible builds and two-person review of every change before it ships — a bar few organizations outside a handful of large tech companies currently clear, given the engineering cost of fully hermetic builds. For most teams, targeting Level 3 — isolated, parameterless builds with platform-generated provenance — closes the realistic gap without requiring a from-scratch build system redesign.

How should branch protection and CVE gating work together?

Branch protection rules (required reviews, status checks, no direct pushes to protected branches) stop untrusted or unreviewed code from merging, but they don't tell you whether the dependencies that code pulls in are safe — that's a job for policy gates evaluated at merge and build time. A widely adopted gating signal is CISA's Known Exploited Vulnerabilities (KEV) catalog, launched in November 2021 with 287 initial entries: unlike a raw CVSS score, KEV membership means the vulnerability has confirmed evidence of exploitation in the wild, which makes "block on any KEV-listed CVE" a much lower-noise policy than "block on any CVE above severity 7." Pairing branch protection with a CI-time policy gate — no merge without required reviews, no build promotion without a passing SBOM and CVE gate — turns two independent controls into a single checkpoint an attacker has to clear twice: once to get code merged, once to get a vulnerable or unsigned artifact promoted.

How Safeguard helps

Safeguard maps directly onto each of these stages. Its secrets scanning doesn't just pattern-match for leaked keys in source, git history, container layers, and CI build logs — it verifies each finding against the issuing service (an AWS sts:GetCallerIdentity call, a Stripe /v1/balance check) so a team knows immediately whether a leaked credential is still live, and can run the built-in revoke-rotate-purge playbook rather than manually chasing down every place a secret might have landed — precisely the detection gap that let the Codecov script run tampered for two months. On the build side, the safeguard/attest GitHub Action step and safeguard sign CLI produce Sigstore-backed SLSA provenance and CycloneDX/SPDX SBOM attestations published to Rekor, mapping cleanly onto SLSA Levels 1 through 4 depending on your build environment. And safeguard gate enforces policy as code at the CI checkpoint — blocking a build on a KEV-listed CVE, a missing SBOM attestation, or an unsigned image — so branch protection and supply-chain policy operate as one connected control instead of two separate tools a team has to keep in sync by hand.

Never miss an update

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