Threat modeling a build pipeline means drawing every path from "someone opens a pull request" to "an artifact runs in production," then asking at each hop who could inject code, steal credentials, or swap artifacts — and the first pass reliably reveals that your CI system is a production system that never got production-grade access control. It has push access to your registries, cloud credentials in its environment, and it executes code from strangers by design. That combination deserves a model.
You do not need a methodology certification or a week offsite. You need a diagram, a checklist, and 90 minutes with the people who actually maintain the pipeline.
Step 1: draw the dataflow, not the org chart
Start with four zones and the arrows between them:
- Source — the SCM: repos, branches, webhooks, PR triggers
- Build — CI runners, their configs, caches, and injected secrets
- Artifacts — registries, package feeds, object storage buckets
- Deploy — CD tooling, cluster credentials, environment promotion, and any Kubernetes build step that bakes an image and pushes it straight to a cluster your runner already has credentials for
Then annotate two things on every arrow: what credential authorizes it and who or what can trigger it. This is where the surprises live. The diagram will show, for example, that a webhook from GitHub triggers a runner that holds an AWS_SECRET_ACCESS_KEY with ecr:PutImage on every repository, and that the trigger condition includes "external contributor opens a PR." Write that sentence down and the finding writes itself.
Keep the diagram in the repo as Markdown or as a Mermaid file next to the pipeline config. A threat model that lives in someone's slide deck is dead by the next reorg.
Step 2: run STRIDE across each hop
STRIDE is a checklist of six threat categories. Applied to a pipeline, it stops being abstract fast:
| STRIDE category | Pipeline example |
|---|---|
| Spoofing | Forged webhook triggers a release build; typosquatted internal package name |
| Tampering | PR modifies .github/workflows/ or Jenkinsfile to alter the build itself |
| Repudiation | No audit trail of who approved the deploy or changed a pipeline variable |
| Information disclosure | Secrets echoed to build logs; artifacts in a public bucket |
| Denial of service | Malicious PR burns runner capacity; cache eviction storms |
| Elevation of privilege | Build step escalates from "run tests" to "read deploy credentials" |
Work hop by hop, ask "which of the six applies here," and write findings as attacker sentences: "An external contributor can modify the workflow file in their PR and exfiltrate the NPM_TOKEN because the job exposes all secrets to all steps." Sentences like that get fixed. "Improve secret hygiene" does not.
Step 3: check your model against attacks that actually happened
A beginner model can miss classes of attack no checklist prompt surfaces, so calibrate against the known catalog — the OWASP Top 10 CI/CD Security Risks is the best single reference. The recurring ones:
Poisoned Pipeline Execution (PPE). The attacker doesn't attack your app; they attack the pipeline definition, which is just a file in the repo. On GitHub Actions, the classic footgun is pull_request_target combined with a checkout of the PR head — the workflow runs with secrets available, executing attacker-controlled code. If you use that trigger anywhere, audit it today. actionlint catches several of these patterns statically.
Credential theft from the runner. The Codecov bash-uploader compromise (2021) exfiltrated environment variables from thousands of CI jobs; the CircleCI incident (January 2023) forced a mass rotation of every secret the platform had ever stored — we walked through that one in our CircleCI rotation retrospective. The lesson both times: long-lived static secrets in CI environments are the pipeline's crown jewels.
Dependency confusion and malicious packages. Your build pulls from public registries by default. A dependency's install hook runs inside the build zone with everything that implies. This is where software composition analysis belongs in the model — not as a compliance box, but as the control on the "public registry to build runner" arrow of your diagram.
Cache and artifact poisoning. Shared caches across trust boundaries (PR builds writing caches that release builds read) let a fork's build poison main's. Scope caches by branch and trust level.
Step 4: rank ruthlessly, fix the top five
Score findings on two axes only — can an outsider reach it, and does it end in artifact or credential compromise — and take the top of the list. For most teams the first-pass fixes are the same, and none of them require buying anything:
- Replace static cloud keys with OIDC federation (GitHub/GitLab issue tokens your cloud trusts; no stored secret to steal).
- Make runners ephemeral. A fresh VM per job turns persistence from trivial to hard.
- Protect the pipeline definition: CODEOWNERS on workflow directories, branch protection, no direct pushes to release branches.
- Scope secrets per job, not per pipeline; forks get none.
- Pin third-party actions and images by digest, not by tag.
That list eliminates the attack path in most first-time findings. Everything else — signing, provenance, admission control — builds on it.
Step 5: keep it alive
A threat model is a snapshot; pipelines change weekly. Two habits keep it honest. First, make "does this change the threat model?" a PR-template question for changes under .github/workflows/, Jenkinsfile, or your CD configs. Second, re-run the 90-minute session whenever a new system joins the diagram — a new registry, a new deploy target, a new CI vendor. Quarterly at minimum. If you want structured practice first, the pipeline-security modules in the Safeguard Academy walk through a worked example end to end.
The meta-lesson from doing this across a few organizations: nobody's pipeline was designed. They accrete. Threat modeling is the first time anyone looks at the whole thing at once, which is why even a beginner session finds real issues — you are competing against nobody having ever looked.
Frequently asked questions
How long does a first pipeline threat model take?
One 90-minute session for the diagram and STRIDE pass with 3–5 people, plus a couple of hours to write up findings. Small enough to just schedule; the fixes it produces are usually configuration changes, not projects.
Do I need a tool, or is a whiteboard enough?
A whiteboard (or a Mermaid diagram in the repo) is enough and has better survival odds than tool-locked models. OWASP Threat Dragon is a fine free option if you want structured storage; the value is in the arrows and the credential annotations, not the software.
What's the single most common critical finding?
Long-lived static cloud credentials available to broadly-triggerable builds — typically an AWS key with registry push and deploy permissions, exposed to any job including PR builds. OIDC federation plus per-job secret scoping removes it, usually in under a day of work.
How is this different from threat modeling our application?
Same method, different assets and entry points. Application models center on user input and data stores; pipeline models center on code as input — PRs, dependencies, pipeline definitions — and on credentials as the target. The pipeline model is usually shorter and finds scarier things faster.