skip to content

A release job's token can push to the org registry and commit to the default branch. What is the blast radius, and how do you split it?

level: seniorimportance: must knowfreq 53%

answer

  1. two powers, one credential
  2. who could contradict the artifact
  3. the record and the thing it records
  4. split the identity, not the step order

basics

~10 s

Any code executing in that job can both publish a malicious artifact and rewrite the source and tags that would have contradicted it. Separate publishing from source writes into two jobs with two identities.

solid answer

~50 s

The danger is not the sum of two permissions, it is the combination. One code execution inside that job — a build plugin, a dependency's build script, a compromised build step — gets the ability to ship an artifact *and* the ability to edit the record that would explain what was shipped. Anyone later reconciling "the release tag points at commit X, and the published image is the build of commit X" is checking two things the same identity controls. I split it: the build-and-publish job holds a push-only credential bound to the registry, with server-side tag immutability so a published tag cannot be re-pointed; the version bump or tag write becomes a separate job with an identity restricted to that path, landing through a reviewed change rather than a direct push. Best of all, remove the need — derive the version from the triggering commit so the release job never writes source at all.

go deeper

for a junior

Recall that a job's credential is available to everything running in the job, and that publishing an artifact and writing to source are two distinct powers that should not sit in the same identity.

for a middle

Explain the mechanism: with tag or branch write, an attacker can make the source appear to match the artifact they shipped, defeating a later comparison of the two.

for a senior

Design the split out loud — separate jobs and identities, a push-only registry credential, server-side tag immutability, review on the source write, or removing the write entirely by deriving the version from the trigger.

for a principal

Own the rule that no single automated identity may both ship an artifact and rewrite the record of it, and be able to cost enforcing it across an estate of release pipelines that all currently violate it.

## Why the combination is the problem Taken alone, each permission is defensible. A release job has to publish; a release process often has to write a version bump or move a tag. Together, in one identity, they collapse a separation that most verification quietly depends on. Think of it as a productive capability and a detective one. Publishing is what the job is for. The source history, the tag and whatever build record you keep are the **evidence** that says what was supposed to be published. When one identity holds both, code that executes in the job can ship a modified artifact and then adjust the evidence so nothing contradicts it. The artifact and the record agree, because the same attacker wrote both. ## The attack chain, as a class No exotic access is needed — just code execution inside a job you already trust: 1. Something in the build's own dependency or plugin tree executes during the release job. That is what a build *is*; thousands of lines of third-party code run with the job's credentials. 2. It reads the job's credential from the environment, a credential file, or the checked-out workspace. 3. It publishes an artifact built from modified sources under the expected release coordinates. 4. It re-points the tag, or amends the version-bump commit, so the source someone would diff matches what shipped. A consumer who checks "the tag exists and the artifact came from our CI" sees a clean release. The reconciliation that would have caught it — comparing the shipped artifact against the source it claims to be built from — was defeated because both sides were writable by one credential. ## Splitting the identity The goal is that no single code execution holds both capabilities: - **Publish job.** Holds a credential scoped to push, bound to the registry, and nothing else. No source write, no ability to create releases, no organisation-level rights. - **Source write, if unavoidable, as a proposal.** Instead of a direct push, the automation opens a change that a human or a policy approves. The identity that can propose is not the identity that can land. - **Registry-side immutability.** Configure the registry so a published release tag cannot be re-pointed at different content. A digest is content-addressed and cannot be forged, but the *tag* is a mutable pointer, and that is what gets moved. - **Branch protection on the record.** If the default branch requires review, a stolen token alone cannot land a rewritten bump commit. - **Delete the requirement.** The strongest version: derive the version from the tag or commit that triggered the release so the release job has no reason to write source. A permission that does not exist cannot be split, stolen or reviewed. - **Keep the vouching identity out of the job.** If the material that signs a release or its provenance is readable by user-defined build steps, the attacker signs their own artifact with your identity. Platform-level signing, where build steps cannot reach the key, is the property that makes a signature mean anything — and it is what the SLSA Build track's top level asks a build platform to provide. ## Things that feel like fixes but are not - **A shorter credential lifetime.** The malicious code runs while the credential is valid; expiry does not help against in-band use. - **"Only maintainers can trigger a release."** The threat is code executing during the build, not who pressed the button. - **"We review the pipeline definition."** The pipeline definition is twenty lines that invoke a build which executes an enormous amount of code you never read. - **Signing inside the same job.** A signature says who vouches for an artifact; it is worth exactly as much as the isolation of the signing identity, and it says nothing about whether the source record was edited. ## Residual risk and detection Even after splitting, keep an independent record: publish what was built, by whom, and from which source revision, into a store the build identity cannot write. Then reconcile out of band — alert when a release tag moves, when a published artifact has no matching build record, or when a source write lands from an automation identity outside its allowed path. Detection only works if the attacker's credential cannot reach the detector, which is the same separation argument one level up.

  • The release job genuinely has to write the new version back to the source. How do you avoid branch write?
    Prefer deriving the version from the triggering tag or commit so no write is needed. If it truly is, have the job propose a change that review or policy lands, or hand the write to a separate identity restricted to that one file and branch. The publishing identity never gains it.
  • Does signing the artifact solve this?
    No. A signature says who vouches for an artifact, not that the artifact matches its declared source. If signing happens inside the compromised job, the attacker signs their artifact with your identity. Signing helps only when the signing material is out of reach of build steps and consumers actually verify.
  • How would you detect this after the fact?
    Reconcile published artifacts against an append-only build record the build identity cannot write to, and alert when a release tag is re-pointed or a published artifact has no matching record. Detection is only meaningful if the stolen credential cannot also reach the evidence store.

saying these in an interview costs you the question

  • Says a short-lived credential makes the combination safe
  • Claims only maintainers trigger releases, so it is fine
  • Thinks reviewing the pipeline file covers code run during the build
  • Assumes signing the artifact repairs a tampered source record

context