skip to content

In a fan-out matrix build, why is one run-scoped token weaker than a credential minted per job?

level: middleimportance: should knowfreq 44%

answer

  1. many legs, one credential
  2. how long, how wide, to whom
  3. the union of every leg's verbs
  4. expiry bounds replay, not live use

basics

~20 s

A run-scoped token is shared by every parallel leg and stays valid for the whole run, so a credential stolen in one leg works for every other job until the run ends. A per-job credential expires with its job.

solid answer

~50 s

A run-scoped token is minted once when the run starts and lives until the run finishes, and every leg of a matrix holds the same reach. In a fan-out build, most legs are executing third-party code — module build scripts, vendor toolchains, dependency install hooks — so any one of them can read that credential and use it with the union of every leg's permissions, against jobs that have not started yet, for as long as the run takes. A per-job credential is minted at job start, dies at job end, carries only that job's verbs, and can be bound to an audience so the service it was not issued for rejects it. I would order the mitigations as scope first, audience second, lifetime third: short expiry bounds later replay from a log or a cache, but the attacker inside the job is using it live.

go deeper

for a junior

Know that a job credential has a lifetime, and that 'valid for the whole run' means every parallel job shares one credential and one window. Be able to say why that is worse than one credential per job.

for a middle

Explain the three axes — lifetime, breadth and audience — and why a thief in one matrix leg ends up with the union of every leg's permissions. Be precise that expiry bounds later replay, not use inside the job.

for a senior

Demonstrate the design call: untrusted fan-out work and the privileged publish must not share an identity, and if the platform cannot mint per job you split the pipeline instead.

for a principal

Treat per-job, audience-bound credentials as a capability you require from a CI platform, and be ready to defend the migration cost of moving an estate off run-scoped tokens against what it actually removes.

## Run-scoped versus job-scoped A **run-scoped** credential is issued once, when the pipeline run begins, and remains valid until the run completes. Every job and every parallel leg in that run either receives the same credential material or one derived from it with identical reach. A **job-scoped** credential is minted when the individual job starts and invalidated when it ends, and it can carry a permission set specific to that job. Three properties decide how much a stolen credential is worth: | Property | Question it answers | |---|---| | Lifetime | How long is it still accepted after it leaks? | | Breadth | Which verbs, on which resources? | | Audience | Which service will accept it at all? | A run-scoped token is usually poor on all three: long lifetime, the union of every job's needs, and no audience restriction. ## Why a matrix fan-out is the worst case A fan-out matrix builds many units in parallel — modules of a monorepo, target platforms, dependency versions. Each leg resolves and executes code you did not write: build plugins, generated build scripts, vendor compilers, test harnesses. That is a lot of independent code-execution surface, running concurrently, all holding the same credential. Suppose leg 7 executes a malicious module build script. With a run-scoped token it obtains: - **Reach it never needed** — the union of every leg's permissions, plus whatever the publish job at the end of the run requires. - **A window measured in the whole run** — the token stays valid after leg 7 is marked complete and while legs 1–12 are still going, so the attacker can act as, or interfere with, jobs that have not run yet. - **Concurrency cover** — activity from a build identity is unremarkable while dozens of legs are hitting the same APIs. With a per-job credential, that same code execution gets one job's verbs for one job's duration, and nothing that a later job holds. ## What audience adds that lifetime does not An audience-bound credential names the service that is allowed to accept it. A credential issued for the artifact registry is rejected by the source host, so stealing the publishing leg's credential does not yield source writes, and vice versa. This is a *categorical* restriction rather than a temporal one: it holds for the entire lifetime, and it does not depend on how fast you notice the theft. ## What short lifetime does and does not buy Short expiry is real but narrower than people assume. The attacker is executing **inside** the job while the credential is valid, so in-band use always works — and a competent one immediately trades the short-lived credential for something durable: a pushed artifact, a created release, a registered deploy key, a new grant. What expiry genuinely bounds is *later* replay: a credential that survives into a log line, a cached layer, an uploaded artifact or a developer's copy of the run output is worthless once expired. So the ordering is: do not grant it, then bind it to one audience, then expire it quickly. ## Practical consequences for a fan-out build - Keep untrusted work and privileged work in different jobs with different identities. The matrix legs that compile third-party code should hold nothing that can publish. - Make the publishing job a separate job that depends on the matrix, minted its own credential, scoped to push only, bound to the registry audience. - Prefer a platform that mints per job over one that mints per run. If yours only mints per run, split the run: the untrusted fan-out becomes one pipeline, and the privileged step becomes another, triggered by a reviewed event rather than sharing a credential. - Remember the credential is not the only cross-leg channel; anything one leg writes and another consumes is a path between them. This is also why build isolation is a *platform* property rather than a pipeline-authoring one: the SLSA Build track's level 3 asks the build platform to keep runs from influencing one another and to keep the material used to sign provenance out of reach of user-defined build steps. A run-scoped credential shared by every leg is the opposite of that, and no amount of careful pipeline authoring fixes it from the inside.

  • If the token expires in five minutes, is exfiltration still a problem?
    Yes. The attacker is executing inside the job while it is valid, so they use it in band or trade it for something durable — a published artifact, a created release, a registered key. Expiry limits replay from a log, cache or artifact later; it does nothing about the live window.
  • Should the job that publishes the release share the matrix's identity?
    No. Make publishing a separate job with its own minted credential, scoped to push and bound to the registry audience. The matrix legs, which are where third-party code actually executes, then hold nothing capable of shipping an artifact.
  • Your platform only issues one credential per run. What can you still do?
    Split the run itself: put the untrusted fan-out in one pipeline whose credential can do nothing interesting, and the privileged publish in another that is triggered by an approved event and holds its own identity. If you cannot narrow the credential, narrow what shares it.

saying these in an interview costs you the question

  • Claims a short-lived credential cannot be abused
  • Thinks the token stops working when its own leg finishes
  • Treats a short lifetime as a substitute for narrow scope
  • Assumes parallel legs cannot see each other's credentials

context