Why should a CI build's automatic job token be scoped per job rather than per repository?
answer
- one grant, many jobs
- ceiling, not floor
- the job is the boundary, not the step
- deny by default, add per job
basics
~20 sA repository-wide grant is the ceiling for every job in it, so a docs-preview job inherits whatever the deploy job needs. Per-job scoping means code running in a low-value job cannot touch the release path.
solid answer
~50 sMost CI platforms hand every job an automatically issued token that authenticates the run to the hosting platform. If the permission set is configured once for the repository, it becomes the union of everything any job might need, and every job gets it: the lint job, the docs preview, the job that builds an untrusted contributor's branch. That matters because the token is readable by anything running inside the job, not only the steps you wrote — build plugins, test frameworks and a dependency's install script all run in the same process tree. So the boundary is the job, not the step. The shape I want is deny-by-default at the platform or repository level, with each job additively granted the narrow verb set it actually needs, and any job that would need two powerful verbs at once split into two jobs.
go deeper
Be ready to say what the automatically issued job token is and that a repository-wide grant applies to every job in that repository. Know the standard shape: nothing by default, with each job adding the verbs it needs.
Explain that anything in the job's process tree can read the token — plugins, test code, a dependency's install script — so the job, not the step, is the security boundary. Be able to name what each verb would let an attacker do.
Show how you would tighten an existing estate: set the default to deny, watch which jobs break, and split any job that needs two powerful verbs at once. Talk about verifying by removal rather than by reading the pipeline.
Own the argument that the default grant is a platform-level setting: flipping it organisation-wide shrinks every repository's blast radius in one change, and the cost is a wave of broken builds you have to sequence and staff.
## What the job token is Nearly every CI platform issues each run an automatically provisioned credential so builds do not need a human's personal access token. It authenticates the job back to the platform that hosts the code: reading the repository, reporting status, opening or commenting on a change, publishing a package, sometimes writing to the source repository itself. It is convenient precisely because nobody has to manage it — which is why its permissions are so often left at whatever the platform or the organisation set once, long ago. ## A repository-wide grant is a ceiling, not a floor If the grant is configured for the repository rather than per job, it has to satisfy the most demanding job in it. One release job needs to publish a package and push a tag, so the grant includes publish and write. Now the lint job, the docs-preview job, the flaky integration job and the job that builds a contributor's branch all carry publish and write too, because they are jobs in that repository. Nobody decided to give the docs job the ability to publish; it was inherited. ## Why that is not theoretical The token is not used only by the lines you wrote in the pipeline definition. Once it is present in the job — in the environment, in a credential file, or persisted into the checked-out workspace — any process the job spawns can read it. A build tool plugin, a test framework, a code generator, a dependency's post-install or build script, or a third-party build step you referenced by a mutable name all execute inside that job with the same access. The unit of trust is therefore the **job**, not the step: asking "which of my steps uses the token" is the wrong question; ask "what could run in this job." That reframing is what makes per-job scoping worth the effort. A job whose token can only read is a job you can afford to run on code you have not reviewed. The same job with write is a job that hands a contributor your release path. ## What per-job scoping actually buys - **Blast radius.** Code execution in a low-value job stops at that job's verbs instead of reaching the highest-value verb in the repository. - **Reviewability.** The permission set becomes a declaration of intent that sits next to the job. A reviewer reading the job can see what it can do, without cross-referencing a repository setting. - **Meaningful trigger questions.** "Who can cause this job to run?" is only answerable once you know what the job can do. Read-only plus an untrusted trigger is usually fine; write plus an untrusted trigger is not. ## How to get there 1. **Set the default to nothing.** Make the platform, organisation or repository default read-only or no permissions, so a new job starts with no reach and has to ask. 2. **Grant additively, per job.** Each job declares only the verbs it needs, on the narrowest resource it needs them on. 3. **Split jobs that need the union.** If a job needs two powerful capabilities at once, that is a design smell: separate them into two jobs with two identities and pass the artifact between them. 4. **Verify by removal.** A permission you cannot tie to a job that visibly fails without it is a permission you do not need. Remove it and run the pipeline. ## What it does not solve Scoping the token does not stop untrusted code from executing — that is a different control (isolation, and pinning what the job pulls in). It also does not help if the same job holds other credentials you injected explicitly: a read-only platform token next to a long-lived cloud key is still a job with full reach. Least privilege applies to *every* credential visible in the job. Finally, a repository-scoped token may still have reach outside the repository — organisation-level read, or rights over sibling projects on a shared CI tenant — which is a separate boundary to check. ## Common mistakes Granting write repository-wide because one job kept failing and nobody wanted to debug it. Assuming a read-only token is harmless, when read of a private repository is exactly how source code walks out. Believing that "only maintainers write the pipeline definition" bounds the risk, when the pipeline's whole purpose is to execute other people's code.
- One step out of twelve in a job needs write access. What do you do?Split that step into its own job with its own narrow grant and pass the build output between them. The other eleven stay read-only. If the platform cannot scope below the job, the job is the unit you split — that is the point of the exercise, not a workaround.
- Your CI platform only lets you set the token grant per repository. What now?Then the repository is the blast radius, so move the privileged work out of it: give the release pipeline its own repository or project with its own identity, keep the shared repository read-only, and trigger the privileged one through a reviewed event rather than by sharing a credential.
- Doesn't a read-only token make a job safe to run on untrusted code?Safer, not safe. Read of a private repository is source-code exfiltration, and the job still holds whatever other secrets you injected into it. A narrow platform token is one credential among several; least privilege has to cover all of them.
A repository-wide grant is a master key cut for the whole building because one room needs it. Per-job scoping cuts a separate key for each room.
saying these in an interview costs you the question
- Assumes only the steps you wrote can read the job token
- Grants write at repository level so no job ever fails
- Says the token is safe because only maintainers edit the pipeline
- Thinks read-only access to a private repository is harmless