Antoine Duno
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- DevSecOps is not a product category — it is a set of practices that distributes security responsibility across every stage of the software delivery pipeline. This guide covers all five stages with concrete tool recommendations, YAML examples, and the common failure modes that turn a DevSecOps initiative into security theater.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
How to Build a DevSecOps Pipeline: Security at Every Stage
"Shift left" has become a marketing phrase that security vendors attach to anything remotely related to CI/CD. But the underlying idea is correct and important: the cost of fixing a security issue grows exponentially with how late in the pipeline you find it.
A hardcoded secret caught by a pre-commit hook takes 30 seconds to fix. The same secret caught in a code review takes 30 minutes. Found in production after a breach — it takes weeks, legal fees, customer notification, and reputational damage that cannot be measured in sprint points.
This guide builds a complete DevSecOps pipeline across five distinct stages, with specific tools for each stage and honest guidance on what each tool actually catches — and what it misses.
The Five Stages of a DevSecOps Pipeline
Developer machine → PR review → CI build → Staging → Production
Stage 1 Stage 2 Stage 3 Stage 4 Stage 5
Pre-commit PR checks Build scan Staging Monitoring
hooks verifyEach stage catches a different class of issue. Running the same tool at every stage is redundant and increases friction without increasing coverage. The goal is complete coverage with minimum friction at each stage.
Stage 1: Pre-Commit Hooks
Pre-commit hooks run on the developer''s machine before a commit is created. They catch issues before they ever enter the repository. This is the "fastest" feedback — the developer sees the error and fixes it in the same terminal session.
What to check at pre-commit:
- Hardcoded secrets: API keys, passwords, tokens committed by accident
- Dangerous patterns: eval(), SQL string concatenation, hardcoded IPs
- Configuration leaks: .env files, credential files accidentally staged
Installing pre-commit
pip install pre-commit
# or
brew install pre-commit# .pre-commit-config.yaml
repos:
# Secret detection
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
# General secret patterns (AWS keys, GitHub tokens, etc.)
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: [''--baseline'', ''.secrets.baseline'']
# Prevent committing .env files
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: detect-private-key
- id: check-added-large-files
args: [''--maxkb=500'']
# JavaScript/TypeScript security patterns
- repo: https://github.com/nodesecurity/eslint-plugin-security
rev: v3.0.1
hooks:
- id: eslint
files: \\.(js|ts|jsx|tsx)$
additional_dependencies:
- eslint
- eslint-plugin-security
# Python security
- repo: https://github.com/PyCQA/bandit
rev: 1.8.3
hooks:
- id: bandit
args: [''-r'', ''--severity-level'', ''medium'']Install hooks for all developers:
# Run once per repository clone
pre-commit install
# Run against all existing files (for first-time setup)
pre-commit run --all-filesWhat pre-commit misses: Runtime configuration issues, deployed infrastructure state, TLS health, HTTP header configuration. These require a running server to check — pre-commit runs before the code is ever deployed.
Stage 2: PR Checks
PR checks run on GitHub Actions (or your CI platform) on every pull request. This stage has two purposes: enforcing team-wide standards that individual developer configurations might skip, and checking things that require a full repository checkout rather than individual file analysis.
What to check at PR: - Static Application Security Testing (SAST): CodeQL, Semgrep - Software Composition Analysis (SCA): Snyk, npm audit, pip-audit - Container image scanning: Trivy (if the PR changes a Dockerfile) - Infrastructure-as-code scanning: Checkov, tfsec (if the PR changes Terraform/CloudFormation) - External security scan: ZeriFlow scan of the preview/staging URL
# .github/workflows/security-pr-checks.yml
name: Security PR Checks
on:
pull_request:
branches: [main, develop]
jobs:
# Static analysis
codeql:
name: CodeQL Analysis
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
matrix:
language: [javascript, python] # add your languages
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- uses: github/codeql-action/analyze@v3
# Dependency vulnerability scanning
dependency-scan:
name: Dependency Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high --fail-on=all
# Infrastructure as code
iac-scan:
name: IaC Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkov scan
uses: bridgecrewio/checkov-action@master
with:
directory: ./infra
framework: terraform
soft_fail: false
# External security scan (requires a deployed preview URL)
external-scan:
name: ZeriFlow Security Gate
runs-on: ubuntu-latest
steps:
- name: Wait for preview deployment
run: sleep 30 # adjust based on your deploy time
- name: Run security scan
env:
ZERIFLOW_API_KEY: ${{ secrets.ZERIFLOW_API_KEY }}
run: |
SCORE=$(curl -s \\
-X POST https://api.zeriflow.com/scan-quick \\
-H "X-API-Key: $ZERIFLOW_API_KEY" \\
-H "Content-Type: application/json" \\
-d ''{"url": "https://staging.your-app.com"}'' | jq -r ''.score'')
echo "Security score: $SCORE/100"
[ $(echo "$SCORE >= 75" | bc) -eq 1 ] || exit 1What PR checks miss: Issues in already-merged code, production-specific configuration, infrastructure changes that happen outside the repository.
Stage 3: CI Build Scan
The CI build stage runs after a merge to main, during the actual build and test process. This stage catches issues that only appear when the code is compiled, built into a container, or assembled with all dependencies present.
What to check at CI build:
- Container image CVE scanning: Trivy, Grype, Snyk Container
- SBOM generation: Software Bill of Materials for compliance
- Secret scanning of build artifacts: Ensure no secrets end up in the Docker image layers
- Dependency lock file verification: Ensure package-lock.json or yarn.lock is not drifting
# .github/workflows/build-security-scan.yml
name: Build Security Scan
on:
push:
branches: [main]
jobs:
build-and-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build -t my-app:${{ github.sha }} .
- name: Scan container image with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: my-app:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
- name: Scan image for secrets
run: |
# Scan all image layers for hardcoded secrets
docker save my-app:${{ github.sha }} | \\
docker run --rm -i zricethezav/gitleaks:latest detect \\
--source=/dev/stdin --report-format=json || {
echo "Secrets detected in Docker image layers"
exit 1
}
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: my-app:${{ github.sha }}
format: spdx-json
output-file: sbom.spdx.json
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.jsonWhat CI build scan misses: Runtime server configuration, deployed application behavior, HTTP headers set by infrastructure outside the container.
Stage 4: Staging Verification
The staging stage runs after deployment to a staging environment. This is where you verify that the deployed application behaves correctly from a security perspective — not just that the code is correct.
This stage catches a class of issues that no earlier stage can find: configuration drift, infrastructure misconfiguration, and deployment-specific issues.
# .github/workflows/staging-security-verification.yml
name: Staging Security Verification
on:
deployment_status: # triggered when deployment completes
jobs:
security-verification:
name: Security Verification
runs-on: ubuntu-latest
if: github.event.deployment_status.state == ''success''
steps:
- name: Extract staging URL
id: url
run: |
URL="${{ github.event.deployment_status.target_url }}"
echo "url=$URL" >> "$GITHUB_OUTPUT"
- name: ZeriFlow security scan
id: scan
env:
ZERIFLOW_API_KEY: ${{ secrets.ZERIFLOW_API_KEY }}
run: |
RESPONSE=$(curl -s \\
-X POST https://api.zeriflow.com/scan-quick \\
-H "X-API-Key: $ZERIFLOW_API_KEY" \\
-H "Content-Type: application/json" \\
-d "{\\"url\\": \\"${{ steps.url.outputs.url }}\\"}")
SCORE=$(echo "$RESPONSE" | jq -r ''.score'')
echo "score=$SCORE" >> "$GITHUB_OUTPUT"
echo "Staging security score: $SCORE/100"
- name: Compare with production baseline
run: |
# Compare staging score against known production baseline
STAGING_SCORE="${{ steps.scan.outputs.score }}"
PRODUCTION_BASELINE="${{ vars.PRODUCTION_SECURITY_BASELINE }}"
if (( $(echo "$STAGING_SCORE < $PRODUCTION_BASELINE - 5" | bc -l) )); then
echo "FAIL: Staging score ($STAGING_SCORE) is more than 5 points below production baseline ($PRODUCTION_BASELINE)"
exit 1
fi
echo "PASS: Staging score ($STAGING_SCORE) is within acceptable range of production baseline ($PRODUCTION_BASELINE)"
- name: Verify specific security requirements
run: |
RESPONSE=$(curl -si "https://staging.your-app.com" 2>/dev/null | head -50)
# Check for required headers
for HEADER in "Strict-Transport-Security" "X-Content-Type-Options" "X-Frame-Options"; do
if ! echo "$RESPONSE" | grep -qi "$HEADER"; then
echo "FAIL: Missing required header: $HEADER"
exit 1
fi
done
echo "PASS: All required security headers present"Staging-specific checks to run manually: - Authentication bypass attempts - Session fixation tests - IDOR (Insecure Direct Object Reference) testing on a few endpoints - Business logic edge cases that automated scanners miss
Stage 5: Production Monitoring
Production monitoring is the stage that catches what everything else missed: zero-day vulnerabilities disclosed after your last deployment, certificate expiry approaching, infrastructure changes made outside the deployment pipeline, and configuration drift over time.
This is the only stage that runs continuously rather than in response to a code change.
# ZeriFlow monitoring configuration (set in dashboard)
# Project: your-app.com
# Frequency: Daily at 02:00 America/New_York
# Threshold: 80
# Alert channels: Slack #security-alerts, PagerDuty (critical only)Supplement ZeriFlow monitoring with infrastructure-level checks:
#!/bin/bash
# /opt/monitoring/production-security-check.sh
# Run via cron: 0 6 * * * /opt/monitoring/production-security-check.sh
# TLS certificate check
check_cert_expiry() {
local domain=$1
local warn_days=30
EXPIRY=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | \\
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$EXPIRY" ]; then
echo "WARN: Could not check certificate for $domain"
return 1
fi
DAYS_LEFT=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 ))
if [ "$DAYS_LEFT" -lt 7 ]; then
echo "CRITICAL: Certificate for $domain expires in $DAYS_LEFT days"
return 2
elif [ "$DAYS_LEFT" -lt "$warn_days" ]; then
echo "WARN: Certificate for $domain expires in $DAYS_LEFT days"
return 1
else
echo "OK: Certificate for $domain valid for $DAYS_LEFT days"
fi
}
check_cert_expiry "your-app.com"
check_cert_expiry "api.your-app.com"
# ZeriFlow daily scan
SCORE=$(curl -s \\
-X POST https://api.zeriflow.com/scan-quick \\
-H "X-API-Key: $ZERIFLOW_API_KEY" \\
-H "Content-Type: application/json" \\
-d ''{"url": "https://your-app.com"}'' | jq -r ''.score'')
echo "Production security score: $SCORE/100"Avoiding Security Theater
Security theater is when all the tooling is in place but no one acts on the results. The symptoms:
- Checks run in "warn-only" mode so they never block anything
- Alert channels are subscribed to by everyone, which means no one feels responsible
- Findings accumulate in a backlog that is never prioritized
- Exception lists grow indefinitely and are never reviewed
- The security gate is bypassed by administrators on every "urgent" PR
How to make the pipeline real
Assign ownership. Every security check has one team that owns the response. Platform team owns TLS and header findings. Backend team owns API security findings. Frontend team owns CSP. When a finding fires, there is no ambiguity about who resolves it.
Set time-boxed response SLAs.
Critical: resolve within 24 hours or document an accepted risk
High: resolve within 7 days
Medium: resolve within 30 days or document
Low: scheduled quarterly reviewMake exceptions visible and expiring. Every exception must have an expiry date and an owner. An exception that expires unreviewed escalates to a critical finding automatically.
Track metrics. Mean time to remediate by severity. Number of findings introduced per sprint vs. resolved. Score trend over 90 days. These metrics make the security program visible to leadership and create accountability.
Test the gates. Once a quarter, deliberately introduce a security regression (add an endpoint with no rate limiting, remove an HSTS header) and verify the gate catches it. If the gate does not catch it, the pipeline has drifted and needs attention.
Tool Summary by Stage
| Stage | Primary tools | What it catches |
|---|---|---|
| Pre-commit | gitleaks, detect-secrets, bandit/eslint-security | Hardcoded secrets, dangerous code patterns |
| PR checks | CodeQL, Snyk, Checkov, ZeriFlow (staging URL) | SAST findings, dependency CVEs, IaC misconfig |
| CI build | Trivy, Grype, Syft (SBOM) | Container CVEs, image layer secrets |
| Staging verify | ZeriFlow, manual testing | Deployed config, header presence, runtime behavior |
| Production monitoring | ZeriFlow monitoring, cert checks | Config drift, new CVEs, certificate expiry |
Conclusion
A complete DevSecOps pipeline is not a single tool — it is five distinct stages, each catching a different class of issue at the point where it is cheapest to fix. Pre-commit hooks cost seconds. CI checks cost minutes. Production incidents cost days.
The most important step is the one most teams skip: Stage 5, continuous production monitoring. Even a perfect pre-deploy pipeline cannot catch configuration changes that happen outside it. ZeriFlow monitoring closes that gap.
ZeriFlow''s Pro plan includes the CI/CD integration and monitoring features that power Stage 4 and Stage 5 of this pipeline — the stages where automated tooling is most scarce in typical DevOps setups.
Add security scanning to your CI/CD pipeline.
Catch vulnerabilities before they reach production.