Securing Your CI/CD Pipeline: A 15-Point DevSecOps Checklist for 2026
The Colonial Pipeline ransomware attack. The SolarWinds supply chain breach. The Codecov bash uploader compromise. What do these headline-grabbing security incidents have in common? They all leveraged weaknesses in the software supply chain and CI/CD infrastructure.
Your CI/CD pipeline isn't just a convenience for developers—it's a critical piece of infrastructure that, if compromised, can give attackers direct access to your production environment, your secrets, and your customers' data. Yet many organizations treat pipeline security as an afterthought, focusing their security efforts on production systems while leaving their build and deployment infrastructure vulnerable.
DevSecOps is the practice of integrating security into every phase of the development lifecycle, with particular emphasis on automating security checks in the CI/CD pipeline. This article provides a comprehensive checklist for securing your CI/CD pipeline, covering everything from dependency scanning to infrastructure hardening.
Why CI/CD Security Matters
Your CI/CD pipeline has access to:
- Production credentials and secrets (database passwords, API keys, cloud credentials)
- Source code repositories (including proprietary algorithms and business logic)
- Container registries and artifact repositories (the software you ship to customers)
- Production deployment infrastructure (the ability to push code to live systems)
A compromised pipeline can lead to:
- Supply chain attacks (injecting malicious code into your artifacts)
- Data breaches (stealing production credentials)
- Credential theft (harvesting developer tokens and keys)
- Ransomware deployment (encrypting production systems)
- Intellectual property theft (exfiltrating source code)
According to the 2023 State of the Software Supply Chain report, 96% of vulnerabilities in applications are from open-source dependencies, and attackers increasingly target CI/CD systems as a force multiplier.
The DevSecOps Security Layers
Securing a CI/CD pipeline requires a defense-in-depth approach across multiple layers:
graph TD
A[Source Code] -->|Code Commit| B[Version Control Security]
B -->|Trigger Build| C[Build Environment Security]
C -->|Run Analysis| D[Static Analysis SAST]
C -->|Scan Dependencies| E[Dependency Scanning]
C -->|Check Secrets| F[Secrets Detection]
C -->|Build Artifact| G[Artifact Signing]
G -->|Deploy to Test| H[Dynamic Analysis DAST]
H -->|Security Tests Pass| I[Container Scanning]
I -->|Infrastructure Check| J[IaC Security]
J -->|Deploy to Production| K[Runtime Security]
style D fill:#f9d5e5
style E fill:#f9d5e5
style F fill:#f9d5e5
style H fill:#eeeeee
style I fill:#eeeeee
style J fill:#c5e1a5
style K fill:#c5e1a5
The Complete DevSecOps Checklist
1. Source Control and Repository Security
Access Control
- Enable multi-factor authentication (MFA) for all developers
- Implement least-privilege access (reviewers vs. committers vs. admins)
- Require signed commits (GPG or SSH signatures)
- Enable branch protection rules (require reviews, status checks)
- Restrict who can approve and merge to protected branches
Repository Configuration
- Disable force pushes to main branches
- Require linear history (no merge commits on protected branches)
- Enable secret scanning (GitHub Advanced Security, GitLab Secret Detection)
- Configure CODEOWNERS for security-critical files
- Audit repository access quarterly
Example: GitHub Branch Protection Rules
# .github/branch-protection.yml
branch-protection:
main:
required_status_checks:
strict: true
contexts:
- 'security/sast'
- 'security/dependency-scan'
- 'security/secret-scan'
- 'test/unit'
- 'test/integration'
required_pull_request_reviews:
required_approving_review_count: 2
dismiss_stale_reviews: true
require_code_owner_reviews: true
enforce_admins: true
required_signatures: true
2. Dependency Management and Scanning
Dependency Scanning
- Scan dependencies for known vulnerabilities (CVEs)
- Fail builds on high/critical vulnerabilities
- Automatically create PRs for dependency updates
- Monitor for malicious packages (typosquatting)
- Verify package checksums and signatures
Tools Comparison
| Tool | Strengths | Language Support | CI/CD Integration | Cost |
|---|---|---|---|---|
| Snyk | Good UI, actionable advice | 10+ languages | Excellent | Free tier + paid |
| Dependabot | Native GitHub integration | Multiple | GitHub Actions | Free |
| npm audit | Built into npm | JavaScript/Node | Easy | Free |
| OWASP Dependency-Check | Open source, comprehensive | Java, .NET, more | Good | Free |
| Trivy | Container + code scanning | Multiple | Excellent | Free |
Example: GitHub Actions Dependency Scanning
# .github/workflows/dependency-scan.yml
name: Dependency Scanning
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6am
jobs:
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run npm audit
run: |
npm audit --audit-level=high --json > npm-audit.json
npm audit --audit-level=high
continue-on-error: true
- name: Snyk Security Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high --fail-on=all
- name: Upload Snyk results to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: snyk.sarif
3. Static Application Security Testing (SAST)
SAST Implementation
- Run SAST on every pull request
- Scan for OWASP Top 10 vulnerabilities
- Check for hardcoded secrets and credentials
- Enforce secure coding standards
- Integrate findings into code review process
Example: Semgrep SAST Pipeline
# .github/workflows/sast.yml
name: Static Application Security Testing
on: [pull_request, push]
jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: returntocorp/semgrep
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
run: |
semgrep scan --config=auto \
--config=p/owasp-top-ten \
--config=p/security-audit \
--error \
--sarif --output=semgrep.sarif \
--json --output=semgrep.json
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
- name: Upload SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: semgrep.sarif
- name: Check for critical findings
run: |
CRITICAL=$(jq '[.results[] | select(.extra.severity == "ERROR")] | length' semgrep.json)
if [ $CRITICAL -gt 0 ]; then
echo "❌ Found $CRITICAL critical security issues"
exit 1
fi
4. Secrets Management
Secrets Security
- Never commit secrets to version control
- Use a secrets management service (Vault, AWS Secrets Manager, Azure Key Vault)
- Rotate secrets regularly (at least quarterly)
- Use short-lived, scoped credentials
- Scan commits for accidentally committed secrets
Example: Secrets Detection with TruffleHog
# .github/workflows/secrets-scan.yml
name: Secrets Scanning
on: [push, pull_request]
jobs:
trufflehog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for better detection
- name: TruffleHog Secrets Scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
extra_args: --only-verified --fail
Example: Vault Integration in CI/CD
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Import Secrets from Vault
uses: hashicorp/vault-action@v2
with:
url: https://vault.company.com
method: jwt
role: ci-cd-role
secrets: |
secret/data/production/db password | DB_PASSWORD ;
secret/data/production/api-keys stripe | STRIPE_KEY
- name: Deploy Application
env:
DATABASE_URL: 'postgres://user:${{ env.DB_PASSWORD }}@db.prod.com/app'
STRIPE_API_KEY: ${{ env.STRIPE_KEY }}
run: ./scripts/deploy.sh
5. Container and Image Security
Container Security
- Scan container images for vulnerabilities
- Use minimal base images (Alpine, distroless)
- Don't run containers as root
- Sign and verify container images
- Scan images in registries regularly
Example: Trivy Container Scanning
# .github/workflows/container-scan.yml
name: Container Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
container-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker Image
run: |
docker build -t myapp:${{ github.sha }} .
- name: Run Trivy Vulnerability Scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail on vulnerabilities
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
- name: Sign Image with Cosign
if: github.ref == 'refs/heads/main'
run: |
cosign sign --key cosign.key myapp:${{ github.sha }}
env:
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
6. Dynamic Application Security Testing (DAST)
DAST Implementation
- Run DAST against deployed test environments
- Scan for runtime vulnerabilities (injection, XSS, etc.)
- Test authentication and authorization
- Perform API security testing
- Schedule regular production scans
Example: OWASP ZAP in CI/CD
# .github/workflows/dast.yml
name: Dynamic Application Security Testing
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * *' # Daily at 2am
jobs:
zap-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Test Environment
run: |
./scripts/deploy-test.sh
echo "TEST_URL=https://test.myapp.com" >> $GITHUB_ENV
- name: Wait for deployment
run: |
timeout 300 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' $TEST_URL)" != "200" ]]; do sleep 5; done' || false
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.10.0
with:
target: ${{ env.TEST_URL }}
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a -j'
- name: ZAP Full Scan
uses: zaproxy/action-full-scan@v0.8.0
with:
target: ${{ env.TEST_URL }}
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a -j'
allow_issue_writing: false
- name: Upload ZAP Report
uses: actions/upload-artifact@v4
if: always()
with:
name: zap-report
path: |
report_html.html
report_json.json
7. Infrastructure as Code (IaC) Security
IaC Security
- Scan IaC for misconfigurations
- Enforce security policies (no public S3 buckets, encrypted databases)
- Version control all infrastructure code
- Require reviews for infrastructure changes
- Validate against compliance frameworks (CIS, SOC2)
Example: Checkov IaC Scanning
# .github/workflows/iac-scan.yml
name: Infrastructure Security Scan
on: [push, pull_request]
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: ./terraform
framework: terraform
output_format: sarif
output_file_path: checkov.sarif
soft_fail: false
download_external_modules: true
- name: Upload Checkov results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: checkov.sarif
8. CI/CD Infrastructure Hardening
Build Environment Security
- Use ephemeral build agents (destroy after each build)
- Isolate build jobs (containers, VMs)
- Use least-privilege service accounts
- Audit runner access and permissions
- Monitor for suspicious build activity
Example: Self-Hosted Runner Security (GitHub Actions)
#!/bin/bash
# Self-hosted runner hardening script
# 1. Create dedicated user (non-root)
sudo useradd -m -s /bin/bash github-runner
sudo usermod -aG docker github-runner
# 2. Install runner as service
cd /home/github-runner
curl -o actions-runner-linux.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
tar xzf ./actions-runner-linux.tar.gz
sudo chown -R github-runner:github-runner /home/github-runner
# 3. Configure with ephemeral flag
sudo -u github-runner ./config.sh \
--url https://github.com/myorg/myrepo \
--token $RUNNER_TOKEN \
--name prod-runner-1 \
--labels production,secure \
--ephemeral \
--disableupdate
# 4. Install and start service
sudo ./svc.sh install github-runner
sudo ./svc.sh start
# 5. Configure firewall (allow only necessary outbound)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 10.0.0.0/8 to any port 22
sudo ufw enable
# 6. Enable audit logging
sudo auditctl -w /home/github-runner -p wa -k github-runner
9. Monitoring and Alerting
Security Monitoring
- Monitor pipeline execution logs
- Alert on failed security checks
- Track security metrics (vulnerabilities found/fixed)
- Log all deployment events
- Monitor for unauthorized access attempts
Example: Security Metrics Dashboard
// monitoring/security-metrics.ts
interface SecurityMetrics {
vulnerabilitiesFound: {
critical: number;
high: number;
medium: number;
low: number;
};
vulnerabilitiesFixed: {
critical: number;
high: number;
medium: number;
low: number;
};
meanTimeToRemediate: {
critical: number; // hours
high: number;
medium: number;
};
securityTestsPassed: number;
securityTestsFailed: number;
secretsDetected: number;
deploymentBlockedBySecurity: number;
}
async function collectSecurityMetrics(startDate: Date, endDate: Date): Promise<SecurityMetrics> {
const snykResults = await getSnykFindings(startDate, endDate);
const sastResults = await getSASTFindings(startDate, endDate);
const dastResults = await getDASTFindings(startDate, endDate);
return {
vulnerabilitiesFound: aggregateVulnerabilities([snykResults, sastResults, dastResults]),
vulnerabilitiesFixed: calculateFixRate(snykResults, sastResults),
meanTimeToRemediate: calculateMTTR(snykResults),
securityTestsPassed: countPassedTests(),
securityTestsFailed: countFailedTests(),
secretsDetected: getSecretsDetectionCount(),
deploymentBlockedBySecurity: getBlockedDeployments(),
};
}
10. Compliance and Policies
Policy Enforcement
- Define security policies (dependency age, vulnerability SLA)
- Automate policy enforcement with policy-as-code
- Maintain audit trail of all pipeline changes
- Document security controls for compliance
- Regular security audits of CI/CD infrastructure
Example: Open Policy Agent (OPA) Policy
# policies/deployment.rego
package deployment
# Deny deployment if critical vulnerabilities exist
deny[msg] {
input.vulnerabilities.critical > 0
msg = sprintf("Deployment blocked: %d critical vulnerabilities found", [input.vulnerabilities.critical])
}
# Deny if dependencies are too old
deny[msg] {
some dep in input.dependencies
days_old := time.now_ns() - dep.last_updated_ns
days_old > (90 * 24 * 60 * 60 * 1000000000)
msg = sprintf("Dependency %s is %d days old (max 90 days)", [dep.name, days_old / (24*60*60*1000000000)])
}
# Deny if image not signed
deny[msg] {
not input.image.signed
msg = "Container image must be signed with Cosign"
}
# Require security tests to pass
deny[msg] {
input.security_tests.sast.passed == false
msg = "SAST security tests failed"
}
deny[msg] {
input.security_tests.dast.passed == false
msg = "DAST security tests failed"
}
Security Testing Maturity Model
Organizations typically progress through stages of CI/CD security maturity:
| Stage | Characteristics | Tools | Deployment Frequency |
|---|---|---|---|
| Level 1: Ad-hoc | Manual security reviews, no automation | Manual code review | Weekly/monthly |
| Level 2: Basic | Dependency scanning, basic SAST | npm audit, Dependabot | Daily |
| Level 3: Automated | SAST, DAST, dependency scanning, secrets detection | Snyk, Semgrep, TruffleHog | Multiple/day |
| Level 4: Integrated | All Level 3 + container scanning, IaC scanning, signed artifacts | Trivy, Checkov, Cosign | Continuous |
| Level 5: Advanced | Level 4 + runtime protection, policy-as-code, security chaos engineering | OPA, Falco, Chaos Mesh | Continuous |
Common Pitfalls and How to Avoid Them
1. Security Theater
- Problem: Running security tools but ignoring findings
- Solution: Fail builds on critical/high vulnerabilities, track remediation SLAs
2. Alert Fatigue
- Problem: Too many low-severity findings overwhelm teams
- Solution: Start with critical/high only, tune false positives, use risk-based prioritization
3. Slowing Down Development
- Problem: Security checks add significant time to pipelines
- Solution: Run fast checks on PR, comprehensive scans nightly; parallelize where possible
4. Secret Sprawl
- Problem: Secrets scattered across environment variables, config files, CI/CD tools
- Solution: Centralize in a secrets manager, use short-lived credentials, implement secret rotation
5. Orphaned Security Findings
- Problem: Security tools create tickets that no one acts on
- Solution: Assign ownership, integrate with existing ticketing, enforce SLAs
Implementing Your DevSecOps Transformation
Phase 1: Foundation (Weeks 1-4)
- Enable branch protection and MFA
- Add dependency scanning (npm audit or Snyk)
- Implement basic secrets scanning
- Document current state and gaps
Phase 2: Automation (Weeks 5-12)
- Add SAST to PR checks
- Implement container scanning
- Set up DAST for staging deployments
- Create security metrics dashboard
Phase 3: Maturity (Months 4-6)
- Add IaC security scanning
- Implement policy-as-code
- Sign and verify artifacts
- Automate security remediation where possible
Phase 4: Excellence (Ongoing)
- Continuous monitoring and improvement
- Regular security training for developers
- Chaos engineering for security
- Contribution to security tooling and policies
Conclusion
Securing your CI/CD pipeline isn't a one-time project—it's an ongoing practice that evolves with your organization and the threat landscape. The checklist in this article provides a roadmap, but remember:
- Start small: Implement high-impact, low-effort controls first
- Automate extensively: Manual security reviews don't scale
- Measure progress: Track security metrics and trends
- Foster culture: Make security everyone's responsibility, not just the security team's
- Iterate continuously: Security is never "done"
The most successful DevSecOps implementations treat security as an enabler of velocity, not an inhibitor. When done right, automated security checks catch issues earlier (when they're cheaper to fix), reduce risk, and actually speed up delivery by preventing production security incidents.
Ready to add comprehensive security testing to your CI/CD pipeline? Sign up for ScanlyApp and integrate automated QA and security checks into your deployment workflow today.
Related articles: Also see adding DAST scans as a security gate in your CI/CD pipeline, the continuous testing foundation your security gates sit on top of, and de-risking deployments once your security pipeline is in place.
