skip to content

Branch protection covers main, but CI runs a credentialed job on every branch push. How do you map and close the PPE paths?

level: seniorimportance: should knowfreq 52%

answer

  1. protection gates merges, not runs
  2. one row per trigger
  3. who can cause it, what it executes
  4. tags and release branches hold the most
  5. filters gate the job, not the checkout

basics

~20 s

Enumerate every trigger — branch pushes, tag pushes, path-filtered runs, schedules, manual dispatch — and record for each who can cause it and what code it executes. Then remove credentials from any job an unreviewed ref can reach.

solid answer

~50 s

The finding is stated in the setup: branch protection gates merges, but the credentialed job runs on a push, so any contributor with write access already has direct poisoned pipeline execution and never needs an approval. I would build a small table with one row per trigger and four columns — the trigger, who can cause it, what code it executes, and what identity or secret that job holds. That exposes the paths teams miss: `release/*` and tag pushes, often unprotected while main is; path filters, which decide whether a job runs and not what it may execute; scheduled runs against the default branch. The fix is structural: build arbitrary refs with a read-only token and no secrets, put privileged steps in a separate job that only a protected ref plus an approval can reach, scope short-lived credentials to that ref, and use ephemeral runners.

go deeper

for a junior

Know that a pipeline can be triggered in several ways and that a build of an unmerged branch still runs real code. Recognising that merge review does not gate a branch build is the point at this level.

for a middle

Be able to enumerate the triggers and say, for each, which ref is checked out and what identity the job carries. Explain why a path filter gates whether a job runs and not what it executes.

for a senior

Produce the mapping for a real pipeline and then propose the structural fix: unprivileged builds for arbitrary refs, credentials behind a protected ref plus approval, ref-scoped short-lived identity, ephemeral runners. Verify the split by running it, not by reading the definition.

for a principal

Decide who owns this inventory and how it is kept current as pipelines multiply, and be ready to defend spending build-platform effort on trigger governance ahead of more visible controls further down the chain.

### Name the exposure first On a settlement-service monorepo at a bank, every engineer has repository write access, and the pipeline builds every branch with the credential it needs to publish artifacts. Branch protection on the default branch is doing exactly what it was designed to do — nothing merges without review — and it is irrelevant to this attack. A low-privilege insider edits the build script on a feature branch, pushes, and their code runs on the CI host with that credential before any human opens the pull request. The attacker position is an authenticated insider with legitimate commit rights; the asset is the build credential and, downstream of it, money movement. No outside access is required at any point. ### The reachability table The systematic version of the question is: for each way this pipeline can start, who can cause it, and what does the resulting job execute and hold? Write it down. | Trigger | Who can cause it | What code it executes | What the job holds | | --- | --- | --- | --- | | Push to default branch | Anyone whose PR merges | Reviewed content | Publish credential | | Push to any feature branch | Any contributor with write | Unreviewed content from that ref | Same credential — the finding | | Push to `release/*` | Any contributor, if unprotected | Unreviewed content | Release credential | | Tag push | Anyone who can create a tag | Content at that tag | Usually the strongest credential | | Path-filtered docs-only run | Any contributor | The whole checked-out tree | Whatever that job holds | | Scheduled run | Nobody directly; fires on default branch | Whatever is merged | Often long-lived credentials | | Manual dispatch | Whoever holds the run permission | Depends on the ref chosen | Depends on the job | Two rows are the ones teams get wrong. **Tag and release-branch pushes** frequently carry the most powerful credential in the estate while being governed less strictly than the default branch — protect the ref patterns, not just the one branch. **Path filters** are not a security boundary: a docs-only filter decides whether the job starts, not what it may execute once it starts, and the job still checks out the whole tree. Skipping CI is also not the same as being safe — it changes only whether the payload runs *now*. Scheduled runs are the mirror image. They cannot be reached from a feature branch, so they are not a PPE path — but they run the default branch with long-lived credentials on a cadence, which makes them the thing an attacker aims at *after* getting something merged. ### Closing it Procedural answers do not hold. Telling everyone to review build-script changes carefully depends on a human noticing six lines in a file they have no context for, and it does nothing about the ordering problem — the job already ran. The structural fixes, in the order they buy the most: 1. **Split trust in the pipeline.** One job compiles and tests arbitrary refs with a read-only token, no secrets, and no ability to publish. A second job does the privileged work, and is reachable only from a protected ref, behind an approval gate that records who approved. 2. **Scope the credential to the ref.** Where the platform supports federated short-lived identity, condition the trust policy on the ref, so a token issued to a feature-branch run cannot publish at all. This turns the whole class from a breach into a failed authorisation. 3. **Protect the ref patterns that matter.** Tag creation and `release/*` need the same governance as the default branch, or better, because their jobs hold more. 4. **Ephemeral runners.** A poisoned job on a reused runner leaves credentials, caches and processes for the next tenant. Destroy the machine after each job. 5. **Path-scoped required review on files the build executes.** This is a real control, but it is the last one on the list because it is the one that depends on a person. ### Verifying rather than assuming Do not take the pipeline definition's word for it. Take a token or a secret that the credentialed job holds and check where it can actually be used from — then push a harmless branch that prints the identity the job assumed, and confirm it is the unprivileged one. The table is a hypothesis; the run is the evidence. ### Detective controls, clearly labelled as such After the structural work, alert on credential use from an unexpected ref, on egress from build jobs to unknown destinations, and on the first appearance of build-affecting file changes in a branch that has no reason to carry them. These are backstops. They fire after the code has run, and they are worth having only because prevention is never complete — never in place of it.

  • Why are path filters not a security boundary here?
    A path filter decides whether a job starts, based on which files changed. It does not restrict what the job may execute once it starts — the checkout is still the whole tree, and the job still holds whatever credential it was given. Treating a docs-only skip as a control also confuses not running now with not being able to run.
  • Where does the credential belong once you split the pipeline?
    In a job reachable only from a protected ref, behind an approval that records an identity, on an ephemeral runner. The build of arbitrary refs hands it an artifact; it never hands the arbitrary-ref job anything worth stealing. Where short-lived federated identity is available, condition the trust policy on the ref so a feature-branch run cannot mint a usable credential at all.
  • Which of these controls are preventive and which are detective?
    Splitting jobs, scoping credentials to a ref, protecting tag and release patterns and using ephemeral runners are preventive — they stop the attacker gaining anything. Alerting on unexpected credential use, on build-job egress and on build-file changes is detective and arrives after execution. Artifact signing and provenance are also detective, and they protect the consumer rather than the build.
  • Scheduled runs cannot be triggered from a feature branch — so are they safe?
    They are not a PPE path, but they are frequently the highest-value target in the table: they execute the default branch on a cadence with long-lived credentials and no human watching the run. They matter after a merge rather than before one, so they belong in the inventory with their own governance.

saying these in an interview costs you the question

  • Claims branch protection on main already covers CI
  • Proposes careful review of build files as the primary fix
  • Treats a docs-only path filter as a security boundary
  • Leaves tag and release-branch pushes ungoverned
  • Reuses long-lived runners after a build of an arbitrary ref
  • Offers artifact signing as prevention against a poisoned build

context