Skip to main content
Back to blog
April 10, 2026·Updated May 2, 2026|14 min read|Antoine Duno|Devops Security

How to Build a DevSecOps Pipeline: Security at Every Stage

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.

Antoine Duno

2,018 words

AD

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                                        verify

Each 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

bash
pip install pre-commit
# or
brew install pre-commit
yaml
# .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:

bash
# Run once per repository clone
pre-commit install

# Run against all existing files (for first-time setup)
pre-commit run --all-files

What 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

yaml
# .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 1

What 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

yaml
# .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.json

What 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.

yaml
# .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.

yaml
# 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:

bash
#!/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 review

Make 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

StagePrimary toolsWhat it catches
Pre-commitgitleaks, detect-secrets, bandit/eslint-securityHardcoded secrets, dangerous code patterns
PR checksCodeQL, Snyk, Checkov, ZeriFlow (staging URL)SAST findings, dependency CVEs, IaC misconfig
CI buildTrivy, Grype, Syft (SBOM)Container CVEs, image layer secrets
Staging verifyZeriFlow, manual testingDeployed config, header presence, runtime behavior
Production monitoringZeriFlow monitoring, cert checksConfig 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.

Related articles

Keep reading