CI/CD Security — Harden Your Pipelines and Prevent Supply Chain Attacks
A practical guide to securing CI/CD pipelines: secrets management, least-privilege runners, artifact signing, dependency scanning, and defending against supply chain attacks.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
CI/CD pipelines are high-value attack targets. They have access to source code, secrets, production environments, and deployment credentials. A compromised pipeline can lead to code injection, data breaches, and supply chain attacks that affect every downstream consumer.
This guide covers practical techniques to harden your CI/CD infrastructure from source to deployment.
When to Use
- You manage CI/CD pipelines that deploy to production
- You want to reduce the blast radius of a compromised build system
- You need to comply with security standards (SOC 2, ISO 27001, FedRAMP)
- You have experienced or want to prevent supply chain attacks
- You are migrating from self-hosted runners to cloud-native CI/CD
Core Concepts
| Concept | Description |
|---|---|
| Supply Chain Attack | Injecting malicious code via compromised dependencies or build tools |
| Least Privilege Runner | Build agents with minimal access to secrets and infrastructure |
| Artifact Signing | Cryptographically verifying that built artifacts came from a trusted pipeline |
| Dependency Scanning | Automatically detecting known vulnerabilities in libraries |
| Pipeline as Code | Version-controlled CI/CD definitions that enforce security policies |
| SBOM (Software Bill of Materials) | Inventory of all components used in an application |
Step-by-Step CI/CD Security Hardening
1. Secure Pipeline Configuration
Treat your pipeline definitions as production code:
# 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@e3dd6a429a4c6c8c8f55e0e0b9e8e8e8e8e8e8e8 # v4.0.2
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
- Use
permissions:blocks with minimal required scopes - Never run CI on
pull_request_targetfrom untrusted forks - Disable script execution during dependency installation (
--ignore-scripts) - Use branch protection rules to prevent direct pushes to main
2. Secrets Management
Secrets in CI/CD are a common attack vector:
# BAD: Hardcoded secrets in pipeline files
# AWS_SECRET_ACCESS_KEY=AKIA... # NEVER DO THIS
# GOOD: Use native secret management
# GitHub Actions: secrets stored in repository settings
# GitLab CI: CI/CD variables with masked flag
# Azure DevOps: Variable groups with secret type
# BETTER: Use OIDC to eliminate long-lived secrets entirely
# AWS: configure-aws-credentials with role-to-assume
# GCP: workload identity federation
# Azure: managed identity + federated credentials
Secrets best practices:
- Rotate secrets automatically (every 30-90 days)
- Use short-lived tokens (1-hour TTL) where possible
- Scope secrets to specific job stages, not global pipeline
- Audit secret access logs regularly
- Never log secrets (most CI systems mask them automatically)
3. Runner Hardening
Your build agents are as critical as production servers:
| Strategy | Description | Implementation |
|---|---|---|
| Ephemeral runners | Fresh VM for every build | GitHub-hosted, GitLab SaaS runners |
| Network isolation | Restrict runner egress | VPC, private subnets, no internet |
| Least privilege | Minimal IAM roles | Separate roles per pipeline/project |
| Immutable images | Pre-hardened runner images | Packer, custom AMI/Golden Image |
| No secrets on disk | Memory-only credentials | tmpfs mounts, secret injection |
# Example: Self-hosted runner hardening checklist
# 1. Run in isolated VPC with no internet egress
# 2. Use separate runner pools per team/application
# 3. Disable sudo/root access for runner user
# 4. Mount /tmp as noexec, nodev, nosuid
# 5. Run container builds in rootless podman/docker
# 6. Clear workspace between jobs (do not reuse)
# 7. Scan runner images weekly for CVEs
4. Dependency and Artifact Security
Your dependencies are your weakest link:
# 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
- name: Run Dependabot (enabled in repo settings)
# Or use 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 }}'
format: 'sarif'
output: 'trivy-results.sarif'
Artifact security practices:
- Sign all artifacts (cosign, Notary, Sigstore)
- Generate and publish SBOMs for every release
- Scan container images before pushing to registry
- Verify artifact signatures before deployment
- Store artifacts in immutable registries with retention policies
5. Deployment Security
The final step must be as secure as the first:
# Example: Secure deployment script
#!/bin/bash
set -euo pipefail
# 1. Verify artifact signature
cosign verify \
--key cosign.pub \
--signature artifact.sig \
myapp:${DEPLOY_VERSION}
# 2. Verify SBOM matches expected components
sbom-diff expected.sbom generated.sbom
# 3. Run smoke tests before 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 deployments
- Implement automated rollback on health check failure
- Use blue-green or canary deployments to limit blast radius
- Log all deployment events to immutable audit trail
- Separate staging and production deployment credentials
Best Practices
- Assume your pipeline will be compromised. Design for containment, not just prevention.
- Use ephemeral infrastructure. Fresh runners prevent persistent malware.
- Verify everything. Signatures, SBOMs, and checksums should be mandatory.
- Minimize pipeline permissions. If a job only needs read access, enforce it.
- Monitor pipeline behavior. Alert on unexpected outbound connections or credential usage.
- Practice incident response. Have a plan for rotating all secrets after a compromise.
Common Mistakes
- Using long-lived secrets in CI. Rotate to OIDC or short-lived tokens.
- Running CI on self-hosted runners without hardening. They often have broader network access than production.
- Trusting
pull_request_targetworkflows. These run with write tokens on untrusted code. - Not scanning dependencies. Known CVEs in dependencies are the most common attack vector.
- Ignoring container base image CVEs. Start with minimal, hardened base images.
Variants
- GitHub Actions security: Focus on
permissions,pull_requestvspull_request_target, OIDC, Dependabot - GitLab CI security: CI/CD variables, job permissions, runner tags, container scanning
- Jenkins security: Agent isolation, credential scopes, pipeline shared libraries
- Cloud-native: Use managed build services (AWS CodeBuild, Google Cloud Build) with IAM integration
FAQ
Q: How do I migrate from long-lived secrets to OIDC?
Configure workload identity federation in your cloud provider, then update configure-aws-credentials (or equivalent) to use role-to-assume without access keys.
Q: Should I use self-hosted or cloud-hosted runners? Cloud-hosted runners are ephemeral and isolated by default. Self-hosted runners require hardening but offer more control and faster builds with caching.
Q: How do I prevent dependency confusion attacks? Use private registries with namespace reservation, verify package signatures, and pin exact versions with lock files.
Q: What is the minimum viable CI/CD security setup? Enable Dependabot, use OIDC for cloud auth, pin action versions, enable branch protection, and scan containers before deployment.
Conclusion
CI/CD security is a continuous process, not a one-time hardening task. Every component — from the runner image to the deployment script — is a potential attack surface. Apply defense in depth, verify every artifact, and assume compromise will happen.