close

DEV Community

InstaDevOps
InstaDevOps

Posted on • Originally published at instadevops.com

Building a DevSecOps Pipeline: Shift Security Left Without Slowing Down

Introduction

Every week brings news of another data breach, supply chain attack, or misconfigured cloud resource exposing millions of records. The traditional approach of bolting security on at the end of the development cycle no longer works. By the time a penetration tester finds a vulnerability in staging, the code has been through weeks of development, and fixing it means expensive rework.

DevSecOps integrates security testing directly into your CI/CD pipeline so vulnerabilities are caught in minutes, not months. The goal is not to slow down development - it is to catch security issues at the same speed you catch bugs: automatically, on every commit.

This guide walks through building a practical DevSecOps pipeline with real tool configurations, covering static analysis, dependency scanning, container image scanning, infrastructure security, and policy-as-code.

The DevSecOps Pipeline Stages

A mature DevSecOps pipeline runs security checks at every stage:

Code Commit → SAST → Dependency Scan → Build → Container Scan → IaC Scan → DAST → Deploy → Runtime Protection
Enter fullscreen mode Exit fullscreen mode

You do not need all of these on day one. Start with the highest-impact, lowest-friction stages and layer on additional checks as your team matures.

Stage 1: Static Application Security Testing (SAST)

SAST tools analyze your source code for vulnerabilities without executing it. They catch issues like SQL injection, cross-site scripting, hardcoded secrets, and insecure cryptographic patterns.

Semgrep for Custom and Community Rules

Semgrep is fast, supports 30+ languages, and lets you write custom rules in a simple YAML syntax:

# .github/workflows/sast.yml
name: SAST Scan
on: [pull_request]

jobs:
  semgrep:
    runs-on: ubuntu-latest
    container:
      image: semgrep/semgrep
    steps:
      - uses: actions/checkout@v4
      - run: semgrep scan --config auto --error --sarif --output semgrep.sarif .
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif
        if: always()
Enter fullscreen mode Exit fullscreen mode

The --config auto flag uses Semgrep's curated community ruleset. For stricter scanning, add specific rule packs:

semgrep scan \
  --config p/security-audit \
  --config p/secrets \
  --config p/owasp-top-ten \
  --error .
Enter fullscreen mode Exit fullscreen mode

Writing Custom Security Rules

Create organization-specific rules for patterns your team should avoid:

# .semgrep/custom-rules.yml
rules:
  - id: no-raw-sql-queries
    patterns:
      - pattern: |
          db.query($SQL, ...)
      - pattern-not: |
          db.query($SQL, [...])
    message: "Use parameterized queries to prevent SQL injection"
    severity: ERROR
    languages: [javascript, typescript]

  - id: no-console-log-in-production
    pattern: console.log(...)
    message: "Remove console.log before merging to main"
    severity: WARNING
    languages: [javascript, typescript]
    paths:
      include:
        - src/
Enter fullscreen mode Exit fullscreen mode

Stage 2: Dependency and Supply Chain Scanning

Your application's dependencies are a massive attack surface. A single vulnerable transitive dependency can compromise your entire system.

Scanning with Trivy

Trivy scans not just containers but also filesystems for vulnerable dependencies:

# .github/workflows/dependency-scan.yml
jobs:
  scan-dependencies:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Trivy filesystem scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scan-ref: .
          severity: HIGH,CRITICAL
          exit-code: 1
          format: sarif
          output: trivy-fs.sarif

      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-fs.sarif
        if: always()
Enter fullscreen mode Exit fullscreen mode

Software Bill of Materials (SBOM)

Generate an SBOM for compliance and vulnerability tracking:

# Generate SBOM with Syft
syft packages dir:. -o spdx-json > sbom.spdx.json

# Scan the SBOM for vulnerabilities with Grype
grype sbom:sbom.spdx.json --fail-on high
Enter fullscreen mode Exit fullscreen mode

Automated Dependency Updates

Use Renovate or Dependabot to keep dependencies current:

// renovate.json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "vulnerabilityAlerts": {
    "enabled": true,
    "labels": ["security"]
  },
  "packageRules": [
    {
      "matchUpdateTypes": ["patch"],
      "automerge": true,
      "automergeType": "branch"
    },
    {
      "matchUpdateTypes": ["major"],
      "labels": ["breaking-change"],
      "assignees": ["team-lead"]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Stage 3: Container Image Scanning

Container images often contain vulnerable OS packages, misconfigured permissions, and unnecessary tools that increase the attack surface.

Scanning During Build

jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - 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 }}
          severity: HIGH,CRITICAL
          exit-code: 1

      - name: Check for root user
        run: |
          USER=$(docker inspect --format='{{.Config.User}}' myapp:${{ github.sha }})
          if [ -z "$USER" ] || [ "$USER" = "root" ]; then
            echo "ERROR: Container runs as root. Add a USER directive to your Dockerfile."
            exit 1
          fi
Enter fullscreen mode Exit fullscreen mode

Hardening Dockerfiles

A secure Dockerfile follows these patterns:

# Use specific version, not :latest
FROM node:20.11-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Multi-stage build - no build tools in final image
FROM node:20.11-alpine

# Create non-root user
RUN addgroup -g 1001 -S appuser && \
    adduser -S appuser -u 1001 -G appuser

WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=appuser:appuser . .

# Drop all capabilities
USER appuser

# Use dumb-init to handle PID 1 properly
RUN apk add --no-cache dumb-init
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

Stage 4: Infrastructure as Code Security

Misconfigured infrastructure is the leading cause of cloud breaches. Scan your Terraform, CloudFormation, and Kubernetes manifests before they reach production.

Checkov for IaC Scanning

jobs:
  iac-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform/
          framework: terraform
          soft_fail: false
          output_format: sarif
          output_file_path: checkov.sarif

      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: checkov.sarif
        if: always()
Enter fullscreen mode Exit fullscreen mode

Checkov catches misconfigurations like:

  • S3 buckets without encryption
  • Security groups open to 0.0.0.0/0
  • RDS instances without encryption at rest
  • IAM policies with wildcard permissions
  • EBS volumes without encryption

tfsec for Terraform-Specific Scanning

# Run tfsec on your Terraform directory
tfsec terraform/ --format sarif --out tfsec.sarif

# Common findings:
# - aws-ec2-no-public-ingress-sgr
# - aws-s3-enable-bucket-encryption
# - aws-iam-no-policy-wildcards
# - aws-rds-encrypt-instance-storage-data
Enter fullscreen mode Exit fullscreen mode

Stage 5: Dynamic Application Security Testing (DAST)

DAST tools test your running application by sending requests and analyzing responses for vulnerabilities. Run these against a staging environment after deployment.

OWASP ZAP Baseline Scan

jobs:
  dast:
    runs-on: ubuntu-latest
    needs: deploy-staging
    steps:
      - name: OWASP ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: https://staging.example.com
          rules_file_name: zap-rules.tsv
          fail_action: true
          cmd_options: '-a -j'
Enter fullscreen mode Exit fullscreen mode

DAST scans are slower (minutes to hours) and noisier than SAST, so run them post-deployment rather than on every commit.

Stage 6: Policy-as-Code with Open Policy Agent

Policy-as-Code enforces organizational rules programmatically. Open Policy Agent (OPA) lets you write policies in Rego that gate deployments:

# policy/kubernetes.rego
package kubernetes.admission

deny[msg] {
  input.request.kind.kind == "Deployment"
  container := input.request.object.spec.template.spec.containers[_]
  not container.resources.limits.memory
  msg := sprintf("Container '%v' must have memory limits set", [container.name])
}

deny[msg] {
  input.request.kind.kind == "Deployment"
  container := input.request.object.spec.template.spec.containers[_]
  container.image
  not contains(container.image, "@sha256:")
  not regex.match("^.*:[0-9]+\\.[0-9]+\\.[0-9]+$", container.image)
  msg := sprintf("Container '%v' must use a specific version tag or digest, not :latest", [container.name])
}

deny[msg] {
  input.request.kind.kind == "Deployment"
  container := input.request.object.spec.template.spec.containers[_]
  container.securityContext.privileged == true
  msg := sprintf("Container '%v' must not run in privileged mode", [container.name])
}
Enter fullscreen mode Exit fullscreen mode

Conftest for CI Integration

Use Conftest to run OPA policies against Kubernetes manifests, Terraform plans, and Dockerfiles in CI:

# Test Kubernetes manifests
conftest test k8s/deployment.yaml --policy policy/

# Test Terraform plans
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json --policy policy/terraform/

# Test Dockerfiles
conftest test Dockerfile --policy policy/docker/
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

Here is a complete pipeline that ties all stages together:

name: DevSecOps Pipeline
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  # Fast checks first (< 2 minutes)
  secrets-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: trufflesecurity/trufflehog@main
        with:
          extra_args: --only-verified

  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: |
          docker run --rm -v "${PWD}:/src" semgrep/semgrep \
            semgrep scan --config auto --error /src

  # Medium checks (2-5 minutes)
  dependency-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          severity: HIGH,CRITICAL
          exit-code: 1

  iac-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform/

  # Build and container scan
  build:
    needs: [secrets-scan, sast, dependency-scan, iac-scan]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t app:${{ github.sha }} .
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: app:${{ github.sha }}
          severity: HIGH,CRITICAL
          exit-code: 1

  # Deploy to staging, then DAST
  deploy-staging:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - run: echo "Deploy to staging"

  dast:
    needs: deploy-staging
    runs-on: ubuntu-latest
    steps:
      - uses: zaproxy/action-baseline@v0.12.0
        with:
          target: https://staging.example.com
Enter fullscreen mode Exit fullscreen mode

Managing False Positives

Every security scanning tool produces false positives. If you do not manage them, your team will start ignoring security alerts entirely.

Create a baseline. On initial integration, capture existing findings as a baseline and only alert on new issues.

Use inline suppressions with justification. Every suppressed finding should include a reason:

# nosemgrep: python.lang.security.audit.insecure-hash.insecure-hash
# Justification: MD5 used for non-security cache key, not for cryptographic purposes
cache_key = hashlib.md5(data).hexdigest()
Enter fullscreen mode Exit fullscreen mode

Triage weekly. Assign someone to review and triage findings weekly. Close false positives, create tickets for real issues, and tune rules that produce too much noise.

Security Metrics That Matter

Tracking the right metrics tells you whether your DevSecOps pipeline is actually improving security or just generating noise.

Mean Time to Remediate (MTTR)

How long does it take from when a vulnerability is detected to when it is fixed in production? Track this by severity:

  • Critical: Target under 24 hours
  • High: Target under 7 days
  • Medium: Target under 30 days
  • Low: Target under 90 days

Vulnerability Escape Rate

What percentage of vulnerabilities make it past your CI/CD pipeline into production? This is your pipeline's effectiveness metric. Track it by comparing CI findings against production security scans and penetration test results.

Coverage Percentage

What percentage of your repositories have security scanning enabled? In most organizations, the answer is disturbingly low. Aim for 100% of production services covered by at minimum dependency scanning and SAST.

Fix Rate vs Find Rate

If you are finding vulnerabilities faster than you are fixing them, your backlog will grow indefinitely. This signals that your pipeline is too noisy (tune it) or your team needs more security training.

Getting Started: A Phased Approach

Do not try to implement everything at once. Here is a realistic rollout plan:

Week 1-2: Secrets scanning and dependency scanning. These have the highest signal-to-noise ratio and catch the most impactful issues. Enable Dependabot or Renovate for automated updates.

Week 3-4: SAST with Semgrep. Start with the default auto config. Do not write custom rules yet. Let the team get comfortable with the findings.

Month 2: Container image scanning. Add Trivy to your Docker build pipeline. Fix the critical and high findings, suppress the false positives with documentation.

Month 3: IaC scanning with Checkov. Scan your Terraform and Kubernetes manifests. This catches misconfigurations before they reach production.

Month 4+: DAST and policy-as-code. These require more setup and tuning but provide the deepest security coverage.

Need Help with Your DevOps?

Building a DevSecOps pipeline that catches real vulnerabilities without drowning your team in false positives takes careful tuning. At InstaDevOps, we implement security-hardened CI/CD pipelines for startups and growing teams - baking security in from day one.

Plans start at $2,999/mo for a dedicated fractional DevOps engineer.

Book a free 15-minute consultation to discuss your security posture.

Top comments (0)