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
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()
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 .
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/
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()
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
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"]
}
]
}
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
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"]
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()
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
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'
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])
}
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/
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
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()
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)