DevSecOps & Supply Chain Security

Threat Modeling for Pipelines

18 min Lesson 2 of 28

Threat Modeling for Pipelines

A delivery pipeline is not just an automation convenience — it is a high-privilege execution environment that has read/write access to source code, production secrets, artifact registries, and cloud infrastructure. Attackers who compromise a pipeline do not need to exploit your application: they become you. The SolarWinds, Codecov, and 3CX supply chain attacks all shared one pattern — the attacker found a seam in the delivery system and used it as an insertion point, not the application itself.

Threat modeling translates that abstract risk into concrete, actionable controls. This lesson applies a lightweight STRIDE variant — tuned for delivery systems — to systematically enumerate who can inject what, where, and how to close each gap.

Why STRIDE-Lite, Not Full STRIDE

Classic STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege) was designed for interactive software components. Pipelines have a narrower threat surface but a much higher consequence per successful attack. STRIDE-lite collapses the six categories into three dominant themes that map directly to pipeline anatomy:

  • Injection — an attacker inserts malicious code or commands into the pipeline flow (covers Tampering + Spoofing)
  • Exfiltration — secrets, source code, or internal network topology leak out (covers Information Disclosure)
  • Privilege Escalation — a pipeline token, role, or runner is used to gain access beyond the intended scope (covers Elevation of Privilege)

Denial of Service and Repudiation are real but secondary; address them through SLO monitoring and audit logging covered in later lessons.

Pipeline Threat Surface: Injection, Exfiltration, Privilege Escalation entry points Source Repo git push / PR CI Runner build + test Artifact Registry image / package CD / Deploy k8s / cloud API Prod cluster INJECTION malicious PR dep confusion INJECTION script injection action poisoning EXFILTRATION secrets in build logs env var leak PRIV ESC over-scoped token IRSA mis-config Legend: Injection (code/cmd inserted into flow) Exfiltration (data exits the pipeline boundary) Privilege Escalation (token/role abused beyond scope)
The three dominant threat categories mapped to pipeline stages. Each node is both an asset and an entry point.

Threat Category 1 — Injection: Who Can Insert Code?

Injection is the most exploited class of pipeline attacks because every pipeline's core job is to run code from the repository. The question is: whose code, under what trust level?

Malicious Pull Requests

GitHub Actions and GitLab CI will execute pipeline definitions from any branch, including those opened by external contributors via fork PRs. An attacker can open a PR that modifies the workflow file to exfiltrate secrets.PROD_AWS_KEY into an outbound HTTP request. The pull_request_target trigger is especially dangerous — it runs in the context of the base branch (with secrets) but checks out the fork's code.

Never use pull_request_target with a step that checks out PR code unless you explicitly scope it. The official GitHub Security Lab documented multiple real-world attacks on popular open-source projects (PyPI, npm packages) through this single misconfiguration.

Dependency Confusion and Typosquatting

If your pipeline runs npm install or pip install against a public registry and your package name matches a private internal package name, an attacker can publish a malicious public package at a higher version number. The package manager resolves the public version first — this is dependency confusion. The Codecov breach followed a similar pattern against the CI runner itself.

Third-Party Action and Orb Poisoning

Every uses: some-vendor/action@main in a GitHub Actions workflow is a remote code execution point. If that action's repository is compromised, so is every pipeline that references it by a mutable tag. The @main or @v3 reference is not immutable.

# WRONG — mutable tag, action can change under you - uses: actions/checkout@v4 # RIGHT — pin to the exact SHA of the action's commit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Automate SHA pinning across all workflows: pip install pip-audit # for Python deps # For actions, use: npx pin-github-action .github/workflows/*.yml # Or use Renovate / Dependabot to keep SHAs current

Threat Category 2 — Exfiltration: What Leaks and Where?

Secrets injected into environment variables are the most common exfiltration vector. A pipeline that prints environment variables to stdout, or a test that logs request headers containing an Authorization header, can leak credentials into build logs that are readable by anyone with repository access.

The second vector is the runner filesystem. CI runners are ephemeral VMs, but if the runner is self-hosted and shared across jobs, a malicious job can read ~/.aws/credentials, /tmp/token-*, or cached credentials from a previous job's workspace without any elevated privileges.

# Detect secrets in build logs — run as a CI step BEFORE printing any output # Using truffleHog in CI - name: Scan for leaked secrets in git history run: | docker run --rm -v "$PWD:/repo" \ trufflesecurity/trufflehog:latest \ git file:///repo --only-verified --fail # Detect env var leakage in test output # gitleaks as a pre-push / CI scan - name: Gitleaks scan uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
GitHub Actions automatically redacts registered secrets from log output — but only if they are registered as secrets. A value passed as a plain environment variable (e.g. env: MY_KEY: ${{ steps.get-token.outputs.token }}) is NOT redacted. Always use the secrets context for sensitive values, and call add-mask for dynamically generated tokens: echo "::add-mask::$DYNAMIC_TOKEN".

Threat Category 3 — Privilege Escalation: What Can a Compromised Runner Do?

Pipeline tokens are the most over-privileged credentials in most engineering organizations. A GitHub Actions workflow token with permissions: write-all can push branches, approve PRs, modify repository settings, and create releases — all from a runner that any contributor's PR code can influence. On AWS, a CD pipeline IAM role with AdministratorAccess turns a pipeline compromise into a full cloud account takeover.

# Enforce least-privilege GitHub Actions token at the workflow level # Set the default to read-only, then grant write only where needed permissions: contents: read # can checkout — nothing else by default jobs: deploy: permissions: contents: read id-token: write # OIDC for AWS auth only — no long-lived keys steps: - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/DeployRole aws-region: us-east-1 # Role policy allows ONLY s3:PutObject to deploy bucket # and ecs:UpdateService — nothing else

Systematic Enumeration — the Pipeline Trust Boundary Table

A practical threat model output is not a slide deck — it is a table that lists every trust boundary crossing in your pipeline, the actor that can trigger it, the worst-case impact, and the control that closes it. Here is a template populated for a typical GitHub Actions + AWS deployment:

Pipeline Trust Boundary Table: entry points, actors, impact, controls Boundary / Entry Point Actor Threat Worst-case Impact Control PR from fork External contributor Injection via workflow Secret exfil pull_request (no secrets) (.github/workflows edit) + required approvals Third-party action Vendor / OSS maintainer Action repo compromise RCE in runner Pin to commit SHA (uses: vendor/action@main) + Dependabot updates npm / PyPI install Malicious package author Dep confusion / typosquat Backdoor in build lockfile + SCA scan (Snyk) (open registry fetch) + private registry scope Build log output Any repo reader Secret in stdout/stderr Credential exfil Secrets context + add-mask (env var / log verbosity) + gitleaks CI scan CD IAM role / token Compromised runner Over-scoped cloud creds Full account takeover OIDC + least-priv role (long-lived key in env) + no long-lived keys Self-hosted runner Malicious PR author Shared runner fs access Lateral movement Ephemeral runners (ARC) (persistent shared host) + network egress policy Controls shown are the first-line mitigations. Defense in depth requires all layers.
Pipeline trust boundary table: each row is a crossing point, its actor, threat, worst-case impact, and primary control.

Running a Pipeline Threat Model — the Four Questions

You do not need a threat modeling tool to start. Work through these four questions for every new pipeline component you add:

  1. What does this component receive as input? — Is that input validated? Can an external party influence it?
  2. What credentials does it hold or generate? — Are they scoped to the minimum required? Do they expire?
  3. What can it write or execute? — Can output propagate to production without a human gate?
  4. What is logged, and who can read it? — Does the log contain anything a reader should not see?
The goal of pipeline threat modeling is not a perfect threat model document — it is a prioritized list of controls to implement. Run this exercise at system design time (before you build), at PR review time (when a new job or action is added), and quarterly (as new attack patterns emerge). The 2024 XZ Utils backdoor demonstrated that even a dependency's build system is a pipeline attack surface.

Connecting the Model to Controls

Each threat maps directly to a concrete tool or configuration introduced in the rest of this tutorial: Injection threats are addressed by SAST (lesson 3), SCA scanning (lesson 4), and signing/verification (lesson 8). Exfiltration threats are addressed by secrets scanning (lesson 9). Privilege escalation threats are addressed by container and IaC scanning (lesson 6) and SBOM provenance (lesson 7). Threat modeling is not a standalone exercise — it is the map that tells you why you are running every scanner.