concept

CI Secret Exposure Surface

also called Build Credential Blast Radius, Runner Secret Surface

The set of credentials reachable by any code that executes in a build job, which is usually far larger than the job needs and is exposed in full by a single compromised step.

cisecretssupply chaincodecovworkload identity

A build job is an unattended process with network access, third-party code and, in most organisations, a generous set of credentials in its environment: cloud access keys, registry logins, deploy keys, signing tokens, package publishing tokens, test accounts for third-party services.

Every one of those is reachable by every step in the job, including steps written by someone else. A dependency's install script, a test helper, a linter plugin and a coverage uploader all run with the same environment. The exposure surface is not the step that was compromised; it is everything the job can see.

Why it matters

This is where an attacker gets the most for the least. Compromising one widely used build tool reaches every organisation that runs it, and in each one it harvests in a single line what would otherwise take weeks of lateral movement.

Codecov disclosed in 2021 that its Bash Uploader had been modified to send continuous integration environment variables to a remote host. The attacker's initial access came from an HMAC key for a service account that could be extracted from a public Docker image. The modification was live from late January and was found on 1 April by a customer who compared the checksum published on the source repository against the file they had downloaded. Nothing in the pipeline itself reported anything, because the URL had not changed.

Implementation patterns

  • Short-lived credentials issued per run, through workload identity federation between the build system and the cloud provider, scoped to exactly what that job does and typically valid for 15 minutes. This is the control with the largest effect, because it attacks the value of a theft rather than trying to prevent every possible theft.
  • Split jobs by trust. Tests and third-party tooling run with no deploy or publish credentials; a separate job holds them and executes no third-party code. Credentials for publishing are available only on the protected branch and only after review.
  • Pin by digest and verify a signature published where the same credential cannot modify it. A script fetched from a mutable URL and piped into a shell is a remote code execution primitive with a subscription model.
  • Egress control on runners, so a build with no reason to reach arbitrary hosts cannot, turning silent exfiltration into a failed connection in a log.
  • Scan your own published artefacts for credentials. The initial access in the Codecov case was a credential recoverable from a published image, which is cheap to look for and rarely looked for.
  • Ephemeral runners, so nothing persists between jobs and a foothold has nowhere to live.

Industry example

Beyond Codecov, the pattern recurs wherever a shared build tool reaches many organisations at once. The structural property is the same each time: one mutable artefact fetched by thousands of pipelines, executed in the context where credentials are most concentrated, with no consumer-side verification. The defences that worked in each case were consumer-side: pinning, verification, and credentials too short-lived and too narrow to be worth stealing.

Failure scenarios

  • A compromised third-party action or plugin printing the environment to a remote host.
  • Secrets available to pull-request builds from forks, so an outsider's code runs with the repository's credentials.
  • Long-lived cloud keys in the build environment, still valid months after exfiltration.
  • Debug output printing the environment into logs that are readable by more people than the secrets are.
  • Credentials reachable by a test that runs arbitrary downloaded fixtures.
  • A self-hosted runner reused between jobs, where one job leaves a process behind for the next.

Trade-offs

Splitting jobs by trust and issuing per-run credentials costs pipeline complexity and some wall-clock time, and it breaks the convenience of one environment where everything is available. Teams feel that cost weekly and feel the benefit once.

Pinning by digest costs upgrade friction: the pinned version has to be moved deliberately, which is the point and is also why unpinned configurations proliferate. Automated dependency updates with review are the usual compromise.

When not to use it

A personal project with no production credentials in the pipeline has little to protect and this discipline is overhead. The moment a pipeline can deploy, publish, or read customer data, the surface is real. Attestation and provenance are the later move, and they matter most when others consume what you publish - at that point you are the upstream in somebody else's threat model, and their pipeline's verification depends on your signing.

Interview question

Q: An attacker compromises one step in your build pipeline for two months without detection. Tell me what they get, how you would have found out, and what you would change first.

What a strong answer covers: that the job's whole environment is reachable, not just the compromised step · why that environment is the most concentrated credential store in the organisation · that detection came from consumer-side checksum comparison in the Codecov case, not from the pipeline · short-lived per-run credentials as the highest-value change, because it devalues the theft · trust-splitting jobs and egress control as the next moves · pinning with signature verification · and the point that attestation is the second phase, not the first.

Quick check

Quiz: Why is a build runner such a valuable target? Because it runs unattended with network access and holds cloud, registry, deploy and signing credentials in one environment reachable by every step.

Flashcard: Which single control most reduces the value of stealing CI secrets? Short-lived per-run credentials from workload identity federation, scoped to that job, so exfiltrated values expire in minutes.