CI/CD Pipeline Security: Protecting Your Software Supply Chain
As pipelines become more complex, they become bigger targets. Here’s how to secure your CI/CD workflow end-to-end.
The Attack Surface: Where Pipelines Are Vulnerable
Pipeline Attack Surface:
1. Source Control → Malicious commits, compromised accounts
2. Build Environment → Poisoned dependencies, build-time malware
3. Secrets Management → Leaked credentials, exposed tokens
4. Artifact Registry → Tampered images, typosquatting
5. Deployment → Unauthorized access, privilege escalation
Dependency Security: The First Line of Defense
Scanning Dependencies Automatically
# GitHub Actions workflow example
name: Security Scan
on: [push, pull_request]
jobs:
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
- name: Audit npm dependencies
run: npm audit --audit-level=high
- name: Snyk Security Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
- name: Check for known vulnerabilities
run: |
# Check for compromised packages
npx @socketsecurity/cli scan .
# Lockfile integrity check
npm ci --audit
Pinning Dependencies Correctly
// package.json - DO THIS
{
"dependencies": {
"express": "4.18.2", // Fixed version
"lodash": "^4.17.21", // Careful with ranges
"react": "18.2.0" // Exact version
},
"devDependencies": {
"typescript": "~5.0.4" // Patch updates only
}
}
// Dockerfile - Version pinning
FROM node:18.17.1-alpine3.18 # Specific version
RUN apk add --no-cache python3=3.10.12-r0
Secrets Management: No More .env Files in Git
GitHub Actions Secrets Pattern
# .github/workflows/deploy.yml
name: Deploy to Production
on:
workflow_dispatch:
inputs:
environment:
description: "Environment to deploy to"
required: true
default: "staging"
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to ECS
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: |
# Secrets are available as environment variables
echo "Deploying to ${{ github.event.inputs.environment }}"
./deploy.sh
HashiCorp Vault Integration
# Jenkins Pipeline with Vault
pipeline {
agent any
environment {
// Dynamic secrets from Vault
DB_CREDS = credentials('vault-db-creds')
API_TOKEN = credentials('vault-api-token')
}
stages {
stage('Deploy') {
steps {
script {
// Secrets are automatically injected
sh 'echo $DB_CREDS | docker login'
sh 'kubectl set env deployment/app API_TOKEN=$API_TOKEN'
}
}
}
}
post {
always {
// Revoke secrets after pipeline completes
sh 'vault lease revoke $LEASE_ID'
}
}
}
Container Security: From Dockerfile to Registry
Secure Dockerfile Practices
# Multi-stage build to minimize attack surface
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 # Non-root user
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
# Security scanning during build
RUN apk add --no-cache clamav && \
freshclam && \
clamscan -r --bell -i /
USER nodejs # Drop privileges
EXPOSE 3000
CMD ["node", "server.js"]
Image Scanning in Pipeline
# GitLab CI example
stages:
- build
- test
- security-scan
- deploy
container-scan:
stage: security-scan
image: docker:latest
services:
- docker:dind
script:
- docker build -t myapp:$CI_COMMIT_SHA .
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock
aquasec/trivy image --severity HIGH,CRITICAL myapp:$CI_COMMIT_SHA
# Check for secrets in image
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock
wagoodman/dive myapp:$CI_COMMIT_SHA
allow_failure: false # Block pipeline on vulnerabilities
Infrastructure as Code Security
Terraform Security Scanning
# .tflint.hcl
plugin "aws" {
enabled = true
version = "0.24.1"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}
rule "aws_s3_bucket_public_access_block" {
enabled = true
}
# In CI pipeline
# tflint --init
# tflint --force
# checkov -d .
# tfsec .
Pipeline Hardening Checklist
✅ Source Code
- Signed commits enabled
- Branch protection rules
- Required code reviews (2+ approvals)
- Status checks required
✅ Build Environment
- Ephemeral build agents
- Minimal base images
- Dependency scanning
- SBOM generation
✅ Secrets
- No hardcoded credentials
- Short-lived tokens
- Automatic rotation
- Access logging
✅ Deployment
- Approval gates for production
- Rollback capability
- Change audit trail
- Immutable infrastructure
✅ Monitoring
- Pipeline execution logs
- Failed build alerts
- Unusual activity detection
- Compliance reporting
Incident Response for Pipeline Breaches
When you suspect a compromise:
-
Immediate actions:
# Revoke all pipeline credentials aws iam list-access-keys --user-name ci-user aws iam delete-access-key --user-name ci-user --access-key-id AKIA... # Rotate all secrets kubectl delete secret --all -n production # Freeze deployments github-cli api repos/owner/repo/actions/workflows/deploy.yml/disable -
Forensic investigation:
- Audit Git history for suspicious commits
- Review pipeline logs for unusual activity
- Check artifact registry for tampered images
- Scan for credential leakage in logs
-
Recovery:
- Rebuild from known-good sources
- Verify artifact integrity (checksums)
- Gradually re-enable with increased monitoring
Conclusion
Pipeline security is not a one-time setup. It requires:
- Automated scanning at every stage
- Least privilege for all credentials
- Immutable artifacts with verified provenance
- Continuous monitoring for anomalies
- Regular audits and penetration testing
Remember: Your pipeline is only as secure as its weakest link. Start with dependency scanning and secrets management, then progressively add more layers of security.