advanced 3 min answer

Between 31 January and 1 April 2021 an altered Codecov Bash Uploader script exfiltrated the environment variables of every CI job that ran it, and was discovered by a customer comparing the script's checksum against the published one. What is the underlying architectural failure, and which changes remove the class rather than the instance?

codecovci-secretssupply-chainoidcleast-privilege
Show the full answer Hide the answer

The trigger

A widely used pattern: a build step that downloads a script over the network and pipes it to a shell. An attacker with access to the hosting of that script added a line that sent the job's environment to a remote host. The script did what it was asked to do and one extra thing, undetected for roughly two months across a very large number of pipelines.

Why it propagated

Because a CI job's environment is a bag of long-lived credentials and every step in the job can read all of it. A step that needs coverage upload permissions could also read the cloud deployment keys, the registry password, the signing key and the package-publishing token, because the environment is flat and the job is one process tree.

The second amplifier is the credential type. Static long-lived tokens remain valuable after exfiltration, so the damage is not bounded by the window of compromise: a key stolen in February works in July. The affected companies' response was mass credential rotation, which is the only available remedy and an expensive one.

Why detection lagged

Nothing on the victim side was watching. A remote script has no version, no digest and no review, so there was no expectation to violate. Detection came from a customer who compared the downloaded script against the published checksum — a control almost nobody applies to build steps, though everyone applies it to dependencies.

The structural fix versus the tempting local one

The tempting fix is to vendor that one script, or to check that one checksum. It closes this instance and leaves the class open, since the next compromise will be a different action, image or plugin.

The changes that remove the class:

  1. Short-lived credentials via workload identity federation. The job exchanges a signed identity token for a credential valid for minutes and scoped to one action. An exfiltrated credential is worthless by the time it is used, which is the single highest-value change on this list.
  2. No standing secrets in the environment. Fetch a secret at the moment of use, in the step that needs it, with the narrowest scope, and never place deployment or signing credentials in a job that also runs third-party code.
  3. Split the pipeline by privilege. Build and test in a job with no credentials at all; publish and deploy in a separate job that runs no third-party steps. Untrusted code and privileged credentials must not share a process tree, and that separation is free.
  4. Pin every third-party step by digest, not by tag or branch, so the code you reviewed is the code that runs. For a downloaded script, verify a checksum; for an action or plugin, pin the commit.
  5. Egress control on the build network. A build that may reach any host on the internet can exfiltrate to any host. An allowlist turns this attack into a failed connection and a log line.
  6. Treat rotation as a drill. The recovery here was rotating everything; the teams that managed it in hours were the ones that had done it before.

The general lesson

Your build system is a production system with your most powerful credentials and the weakest review of what executes in it. Dependencies get scanned, pinned and reviewed; build steps get pasted from documentation. The test worth applying is one question: if this step were malicious, what could it reach? Answer it for each step, and the privilege split follows on its own.

When this is less urgent

A pipeline with no credentials at all — a fork's pull-request build, a documentation site — does not need this. That is also the design principle in miniature: most jobs can be arranged to hold nothing worth stealing, and the effort concentrates on the few that must hold something. Choose the privilege split first, because it makes every other control cheaper, and leave egress allowlisting for later unless you handle regulated data, since it is the most operationally intrusive item here.