CI/CD Pipeline Security: Prevent Supply Chain Attacks
A practical guide to securing CI/CD pipelines: secrets management, least-privilege runners, artifact signing, and supply chain defense.
Overview
CI/CD pipelines are high-value attack targets. They hold source code, secrets, production access, and signing keys — everything an attacker needs to reach your users without touching your app directly. We’ve watched this stop being theoretical: SolarWinds’ build system was poisoned in 2020 and shipped backdoored updates to 18,000 organizations; in 2021 attackers modified Codecov’s bash uploader inside its CI process and exfiltrated environment variables from thousands of pipelines; in early 2025 a compromised tj-actions/changed-files version leaked secrets in GitHub Actions logs across more than twenty thousand repositories.
The pattern behind all three is the same: the pipeline was trusted more than anything it produced — every one of those attacks reached production because nobody verified the middle. This guide works through the controls that break that assumption, layer by layer: what to lock down, in what order, and how to prove it’s working.
Before the controls, a quick threat model. Your pipeline has four things an attacker wants: secrets, the ability to modify what gets built, network access to move laterally, and signing identity to make malicious artifacts look legitimate. Every layer below exists to deny one or more of those four prizes — when a control doesn’t map to one, it’s probably ceremony, not security.
When to Use
- You manage CI/CD pipelines that deploy to production
- You want to shrink the blast radius of a compromised build system
- You need to meet compliance frameworks (SOC 2, ISO 27001, FedRAMP)
- You’ve suffered, or want to prevent, a supply chain attack
- You’re moving from self-hosted runners to cloud-native CI/CD and back again
For a checklist-oriented version of these controls, the CI/CD pipeline security template maps each one to a verification method. For alternatives, see the Complete Guide to Secrets Management.
Core Concepts
| Concept | Description |
|---|---|
| Supply chain attack | Injecting malicious code via compromised dependencies, actions, or build tools |
| Least-privilege runner | Build agents with minimal access to secrets and infrastructure |
| Artifact signing | Cryptographically proving a built artifact came from your pipeline |
| Dependency scanning | Detecting known vulnerabilities in libraries automatically |
| Pipeline as code | Version-controlled CI/CD definitions that enforce security policy |
| SBOM | A bill of materials listing every component in a release |
| OIDC federation | Short-lived identity tokens instead of stored cloud credentials |
| SLSA | A framework grading supply chain integrity from SLSA 0 to SLSA 4 |
Two concepts deserve a second look. SLSA levels give you a vocabulary for pipeline maturity: SLSA 1 means a documented build process, SLSA 2 means a hosted build service, SLSA 3 means hardened builders, SLSA 4 means two-person review plus reproducibility. Most teams sit at 1-2; signing plus provenance gets you toward 3. OIDC federation is the control with the best effort-to-impact ratio in this guide — it swaps long-lived cloud secrets for tokens that expire in minutes and can’t leak from a fork.
Step-by-Step CI/CD Security Hardening
Every stage below maps to a real incident pattern. Work top to bottom; each layer assumes the previous one holds.
1. Secure Pipeline Configuration
Treat pipeline definitions as production code — review them, version them, and assume they will be modified by someone you didn’t intend.
# Example: GitHub Actions security hardening
name: Secure Build
on:
push:
branches: [main]
pull_request:
branches: [main]
# Minimal permissions by default
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
# Pin actions to specific commit hashes
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Use OIDC for cloud authentication instead of long-lived secrets
- name: Authenticate to AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/CICD-Build-Role
aws-region: us-east-1
# Verify dependencies before installing
- name: Install dependencies
run: |
npm ci --ignore-scripts # Skip post-install scripts
npm audit --audit-level=moderate
Security checklist for pipeline definitions:
- Pin all third-party actions to commit SHA, not version tags — tags move, SHAs don’t. The
tj-actionsincident happened because a tag was repointed at a malicious commit. - Use
permissions:blocks with minimal scopes; an explicitcontents: readat workflow level is the cheapest win in this entire guide. - Never run CI on
pull_request_targetwith checkout of untrusted code — it executes PR code with write tokens and secrets. - Pass
--ignore-scripts(or equivalent) when installing dependencies; post-install hooks run arbitrary code with your pipeline’s privileges. - Enforce branch protection with required reviews so the pipeline definition itself can’t be bypassed.
2. Secrets Management
Secrets in CI/CD are the most common attack vector because they’re the easiest prize: one leaked AWS_SECRET_ACCESS_KEY in a log or a forked-repo build and the attacker is inside.
# BAD: Hardcoded secrets in pipeline files
# AWS_SECRET_ACCESS_KEY=AKIA... # NEVER DO THIS
# GOOD: Native secret stores
# GitHub Actions: repository/environment secrets
# GitLab CI: masked, protected CI/CD variables
# Azure DevOps: secret variable groups
# BETTER: OIDC eliminates stored cloud credentials entirely
# AWS: configure-aws-credentials with role-to-assume
# GCP: Workload Identity Federation
# Azure: managed identity + federated credentials
Secrets: what works
- Rotate whatever remains stored every 30-90 days — and after any incident, rotate everything at once, not one at a time.
- Prefer short-lived tokens (1-hour TTL); an expired token can’t be replayed.
- Scope secrets to jobs and environments, never pipeline-wide.
- Audit secret access logs; most platforms expose who used what and when.
- Never echo secrets. Most CI systems mask known secrets, but derived values — tokens minted from secrets — often aren’t masked.
3. Runner Hardening
Your build agents are as critical as production servers — often more so, because they hold credentials production never sees.
| Strategy | Description | Implementation |
|---|---|---|
| Ephemeral runners | Fresh VM or pod for every build | GitHub-hosted, GitLab SaaS, K8s jobs |
| Network isolation | Restrict runner egress | VPC, private subnets, egress proxy |
| Least privilege | Minimal IAM per job type | Separate roles per pipeline/project |
| Immutable images | Pre-hardened, scanned runner images | Packer, golden AMI, image signing |
| No secrets on disk | Memory-only credentials | tmpfs mounts, runtime injection |
# Self-hosted runner hardening checklist
# 1. Isolate in a VPC with no direct internet egress
# 2. Separate runner pools per team or trust level
# 3. No sudo/root for the runner user
# 4. Mount /tmp as noexec, nodev, nosuid
# 5. Run container builds rootless (podman/docker rootless mode)
# 6. Wipe the workspace between jobs — never reuse build dirs
# 7. Scan runner images weekly for CVEs and rebuild often
A practical note: self-hosted runners behind your firewall frequently have more network access than production services. If you can’t isolate them properly, cloud-hosted ephemeral runners are the safer default.
4. Dependency and Artifact Security
Your dependencies are your weakest link — you inherit every vulnerability and every compromised maintainer account in your dependency tree.
# Example: GitHub Actions dependency scanning pipeline
jobs:
security-scan:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
# SAST (Static Application Security Testing)
- name: Run CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript, python
- uses: github/codeql-action/analyze@v3
# Dependency vulnerability scanning
# Dependabot alerts + updates in repo settings,
# or Snyk / OWASP Dependency-Check
# Container image scanning
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan image with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
severity: 'CRITICAL,HIGH'
exit-code: '1'
Artifact security practices:
- Sign every artifact — cosign keyless signing via OIDC means no private key to manage or lose.
- Generate and publish SBOMs for every release; you can’t respond to the next Log4Shell without an inventory.
- Scan images before they reach the registry and block on HIGH/CRITICAL.
- Verify signatures at deploy time — signing without verification is decoration.
- Use immutable registries with retention policies so yesterday’s artifact can’t be silently replaced.
- Defend against dependency confusion: private registries with namespace reservation, lockfiles with integrity hashes, and signature verification for internal packages.
5. Deployment Security
The last stage must be as hardened as the first — a compromised deploy step defeats everything upstream.
# Example: Secure deployment script
#!/bin/bash
set -euo pipefail
# 1. Verify artifact signature
cosign verify --key cosign.pub artifact.sig myapp:${DEPLOY_VERSION}
# 2. Confirm SBOM matches expected components
sbom-diff expected.sbom generated.sbom
# 3. Smoke tests before any traffic shift
./smoke-tests.sh --target staging
# 4. Deploy with automatic rollback on failure
./deploy.sh --version ${DEPLOY_VERSION} --rollback-on-failure
# 5. Verify deployment health
./health-check.sh --target production
Deployment security rules:
- Require manual approval for production — environments with required reviewers give you an audit trail for free.
- Auto-rollback on health-check failure; blue-green or canary limits blast radius when a bad artifact slips through.
- Separate staging and production credentials — the deploy role for staging should be useless in production.
- Log every deployment event to an append-only audit trail outside the CI platform itself.
Companion files: the ci-cd-security-guide companion repo has a hardened GitHub Actions workflow implementing these steps (secure-workflow.yml) plus the self-hosted runner hardening checklist.
What Works
- Assume the pipeline will be compromised. Design for containment — blast radius, detection, recovery — not just prevention.
- Ephemeral infrastructure beats hardened infrastructure. A fresh runner can’t harbor yesterday’s malware.
- Verify everything downstream: signatures, SBOMs, checksums. Trust is what attackers exploit.
- Minimum permissions per job. If a job only reads, don’t let it write; if it doesn’t need secrets, don’t grant them.
- Watch pipeline behavior, not just results: alert on unexpected egress, credential use outside deploy jobs, or builds that suddenly take 3x longer.
- Practice incident response: when a pipeline is compromised, can you rotate every secret it touched within an hour? If you don’t know, drill it.
Common Mistakes
- Long-lived secrets in CI: OIDC or short-lived tokens wherever the platform supports it.
- Unhardened self-hosted runners: they usually hold broader network access than production and persist between jobs.
- Trusting
pull_request_target: it runs untrusted code with write tokens — a documented, repeatedly-exploited pattern. - Skipping dependency scanning: known CVEs in dependencies remain the most exploited vector in the chain.
- Trusting version tags on actions: tags get repointed; pin SHAs.
- Ignoring base-image CVEs: start from minimal, maintained base images (distroless, Alpine where appropriate) and rebuild regularly.
- Masking only the raw secret: derived tokens and base64-encoded variants leak through logs unmasked.
Variants
- GitHub Actions:
permissionsscoping,pull_requestvspull_request_targetsemantics, OIDC viaid-token: write, Dependabot, environments with required reviewers. - GitLab CI: masked/protected variables,
id_tokenOIDC per job, runner tags for isolation, built-in container and secret scanning. The OIDC equivalent looks like this:
# GitLab: OIDC token per job, aud claim scoped to the job
deploy:
id_tokens:
AWS_ID_TOKEN:
aud: sts.amazonaws.com
script:
- aws sts assume-role-with-web-identity
--role-arn arn:aws:iam::123456789012:role/gitlab-deploy
--role-session-name gitlab-$CI_JOB_ID
--web-identity-token $AWS_ID_TOKEN
--duration-seconds 3600
The token lives only inside the job, is bound to the project and ref, and dies with it — same trade as GitHub’s id-token: write. Pair it with protected variables so secrets only reach jobs on protected branches.
- Jenkins: agent isolation (one job per agent or K8s agents), credential scopes, pipeline shared libraries as a review boundary, script security.
- Cloud-native builds: AWS CodeBuild, Google Cloud Build, Azure DevOps give you IAM-integrated identities and isolated builders — less runner hardening for you, but you still own permissions and artifact verification.
Responding to a Pipeline Compromise
Every control above assumes the day it fails. When — not if — a pipeline is compromised, the response needs to be faster than the attacker’s next move. This is the playbook.
1. Isolate first. Disable the affected workflow or runner pool immediately; don’t investigate a live target. Pause deploys. If the runner was self-hosted, kill the VM or pod rather than cleaning it — ephemeral beats forensics on a compromised box.
2. Assume every secret it touched is gone. Rotate all of them, in dependency order: cloud credentials first (they unlock everything else), then registry and deploy keys, then third-party tokens, then signing keys. With OIDC this step shrinks to revoking trust policies — one more reason to migrate.
3. Audit what the pipeline produced. Check which artifacts were built and deployed during the exposure window: image digests, provenance attestations, deploy logs. Any artifact you can’t prove clean gets redeployed from a verified build.
4. Audit what it touched. Look for unexpected egress, new webhooks, modified workflow files, suspicious package installs, and access to resources the job shouldn’t have needed. The pipeline’s own logs are the first thing an attacker scrubs — ship them to an external, append-only store before an incident, not after.
5. Rebuild, don’t patch. Recreate runners, re-issue tokens, re-deploy from known-good artifacts. A patched compromised system is still a compromised system.
Compliance mapping
If you’re doing this for a framework, here’s where the controls land:
| Framework requirement | Pipeline control that satisfies it |
|---|---|
| SOC 2 CC6.1 (logical access) | OIDC, scoped secrets, minimal permissions |
| SOC 2 CC7.1 (monitoring) | Pipeline egress alerts, audit log shipping |
| ISO 27001 A.8.9 (config mgmt) | Pipeline as code, branch protection, review |
| ISO 27001 A.8.25 (secure dev) | SAST/SCA gates, Trivy scanning, code review |
| NIST SSDF PO.5 / PW.6 | Artifact signing, SBOM, provenance |
| FedRAMP CM-7 / SI-7 | Least privilege runners, signature verification at deploy |
None of this is ceremonial. Auditors ask for evidence — and the approval logs, signed artifacts, and SBOMs these controls produce are exactly the evidence they’re looking for.
Threat-to-Control Quick Reference
When a control doesn’t map to a threat on this table, question whether it’s earning its keep.
| Attack | Entry point | Primary control |
|---|---|---|
| Malicious PR | pull_request_target + secrets | pull_request only, no secrets on forks |
| Poisoned action | Repointed version tag | SHA pinning + review |
| Secret exfiltration | Log output, forked builds | OIDC, masking, job-scoped secrets |
| Lateral movement | Self-hosted runner access | Ephemeral, isolated runners |
| Dependency confusion | Public package same-name | Private registry + namespace |
| Tampered artifact | Registry write access | cosign sign + verify at deploy |
| Unauthorized deploy | Stolen deploy credentials | OIDC + manual approval + audit |
Keep Improving
CI/CD security is a continuous practice, not a hardening project that ends. Every new action, dependency, and runner type adds surface — and every convenience shortcut you take in the pipeline is one an attacker can take too. Pick the control your next audit would flag first — for most teams that’s OIDC or artifact signing — and put it in this sprint, not the backlog. Then schedule the next review; a pipeline you hardened six months ago has drifted since, and drift is where incidents live.
Further Reading
- OWASP CI/CD Security Risks (Top 10) — the canonical taxonomy of pipeline attack patterns
- SLSA Framework — supply chain integrity levels and provenance requirements
- Sigstore / cosign docs — keyless artifact signing and verification
- GitHub OIDC in Actions — official hardening guide for OIDC federation
- GitLab CI/CD security — protected variables, OIDC, and runner isolation
- Trivy documentation — vulnerability and secret scanning
Frequently Asked Questions
How do I migrate from long-lived secrets to OIDC?
Configure workload identity federation in your cloud provider (AWS IAM OIDC provider, GCP Workload Identity, Azure federated credentials), then swap static credentials for role-to-assume in your auth step. The OIDC token is scoped to the job's repository and branch, so a leaked token is nearly worthless.
Should I use self-hosted or cloud-hosted runners?
Cloud-hosted runners are ephemeral and isolated by default — the safer baseline. Self-hosted runners buy you caching speed and network proximity but hand you runner hardening, persistence risk, and egress control. Choose them only when you can isolate them properly.
How do I prevent dependency confusion attacks?
Reserve your internal namespace on a private registry, pin exact versions with lockfiles containing integrity hashes, and prefer private-over-public resolution order so internal-lib never resolves to a public package of the same name.
What is the minimum viable CI/CD security setup?
Branch protection, Dependabot or equivalent, OIDC for cloud auth, SHA-pinned actions, and image scanning before deploy. That set blocks most documented incidents and takes days, not months.
How does artifact signing actually stop an attacker?
It changes the trust question at deploy time from "did this image come through our pipeline?" to "can this image prove it came through our pipeline?" An attacker with registry access can swap an image — they can't forge a signature without your keys or identity.
How do I measure whether pipeline security is improving?
Track secrets eliminated by OIDC, percentage of signed artifacts, mean time to patch critical dependency CVEs, and SLSA level progress. If those numbers move, the posture is real — not just policy documents.
What's the first thing to do after discovering a compromised pipeline?
Isolate it before investigating: disable the workflow, kill the runner, pause deploys. Then rotate every secret the pipeline could access, in dependency order — cloud credentials first, since they unlock everything else. Auditing can wait an hour; an open pipeline cannot.
Do I need all these controls on day one?
No. Start with the minimum viable set — branch protection, dependency scanning, OIDC, SHA pinning, image scanning — and layer the rest in order of what a breach would actually cost you. Signing and provenance matter most once external consumers depend on your artifacts, not before.
Related Resources
Complete Guide to Supply Chain Security
Secure your software supply chain end-to-end: SBOMs, dependency scanning, Sigstore signing, SLSA provenance, and CI/CD hardening with runnable examples.
GuideZero Trust Architecture — Never Trust, Always Verify
A practical guide to implementing Zero Trust architecture: identity verification, least privilege, micro-segmentation, and continuous validation for modern systems.
GuideComplete Guide to Secrets Management
Manage application secrets securely in production: Vault, AWS, Azure, GCP, Kubernetes secrets, rotation, CI/CD, and when a manager is overkill.
DocCI/CD Pipeline Security Template
A template for securing build and deployment pipelines against credential leaks, tampering, supply chain attacks, and unauthorized deployments.
DocCI/CD Pipeline Design Template
A template for documenting CI/CD pipeline stages, gates, environments, deployment strategies, rollback procedures, and security scanning.
RecipeContainer Security: Scan, Harden, and Sign Docker Images
Scan container images for CVEs and secrets with Trivy and Grype. Harden Dockerfiles, generate SBOMs, sign with Cosign, and gate deployments.