DevSecOps: How to Integrate Security into Your CI/CD Pipeline in 2026
Shifting security left means more than running a scanner in your pipeline. Learn how to build security gates, automate threat detection, and create a DevSecOps culture that catches vulnerabilities before they reach production.
Security Belongs in the Pipeline, Not After It
The traditional software development lifecycle treated security as a phase that happened after development — a security team reviewed code, ran penetration tests, and signed off before production. This model has two fundamental problems: it's slow (bottlenecks at the security review gate) and it's late (finding vulnerabilities in finished software is expensive to fix).
DevSecOps integrates security into every phase of development and delivery. The goal isn't to make developers into security engineers — it's to automate the security checks that can be automated, surface findings where developers work, and make secure development the path of least resistance.
This guide covers how to build a security pipeline that catches real issues without creating so much noise that developers start ignoring alerts.
The DevSecOps Security Pipeline Architecture
A mature DevSecOps pipeline runs different security checks at each stage based on speed, feedback value, and when the information is available:
The key principle: faster checks run earlier, slower checks run later. A 3-second secret scan on every commit is valuable. A 20-minute DAST scan runs after deployment to staging.
Stage 1: Pre-Commit Hooks (Developer Workstation)
Pre-commit hooks catch issues before code leaves the developer's machine. This is the cheapest place to find problems — no CI minutes consumed, immediate feedback.
Setup with pre-commit Framework
# .pre-commit-config.yaml
repos:
# Detect hardcoded secrets
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
# General secret patterns
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: detect-private-key
- id: check-added-large-files
args: ['--maxkb=1000']
- id: check-merge-conflict
- id: no-commit-to-branch
args: ['--branch', 'main', '--branch', 'master']
# Python security (if applicable)
- repo: https://github.com/PyCQA/bandit
rev: 1.7.6
hooks:
- id: bandit
args: ['-ll'] # Only medium and high severityCritical: pre-commit hooks can be bypassed with git commit --no-verify. They're the first line of defense, not the last. The CI pipeline must enforce the same checks independently.
Stage 2: Static Application Security Testing (SAST)
SAST analyzes source code for security vulnerabilities without executing it. It catches injection flaws, insecure configurations, and dangerous API usage patterns at code review time.
Top SAST Tools by Language
| Language | Tool | Notes |
|---|---|---|
| JavaScript/TypeScript | Semgrep, SonarQube, ESLint security rules | Semgrep has excellent JS/TS rules |
| Python | Bandit, Semgrep, Pylint security | Bandit is fast and CI-friendly |
| Go | Gosec, Semgrep | Gosec covers Go-specific patterns |
| Java | SpotBugs + FindSecBugs, SonarQube | SpotBugs is free, SonarQube has deeper rules |
| C/C++ | Flawfinder, Cppcheck, Coverity | Coverity is paid but industry standard |
| Multi-language | Semgrep, CodeQL, Snyk Code | CodeQL is free for open source |
GitHub Actions: Semgrep SAST
# .github/workflows/sast.yml
name: SAST Security Scan
on:
pull_request:
branches: [main, master]
push:
branches: [main, master]
jobs:
semgrep:
name: Semgrep SAST
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: >-
p/security-audit
p/owasp-top-ten
p/javascript
p/typescript
p/secrets
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.sarifTuning Signal-to-Noise Ratio
Raw SAST output is often unusable — hundreds of findings, many false positives. Tune aggressively:
# semgrep.yml — ignore low-value rules
rules:
- id: project-overrides
paths:
exclude:
- "**/*.test.ts" # Skip test files
- "**/fixtures/**" # Skip test fixtures
- "**/vendor/**" # Skip vendored codeStart with a high-severity-only gate that blocks PRs. Add medium severity as informational warnings. Gradually migrate informational to blocking as you resolve the backlog.
Stage 3: Software Composition Analysis (SCA)
SCA scans your dependencies for known vulnerabilities (CVEs), license compliance issues, and outdated packages. It's one of the highest ROI security controls because most production vulnerabilities are in third-party libraries, not first-party code.
GitHub Actions: Dependency Review
# .github/workflows/sca.yml
name: Dependency Security Review
on:
pull_request:
branches: [main]
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high # Block on high/critical CVEs
deny-licenses: GPL-3.0, AGPL # Block GPL in proprietary code
allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Snyk SCA Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high --fail-on=allDependency Update Automation
Block new vulnerabilities from entering via Dependabot:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
day: monday
time: "09:00"
open-pull-requests-limit: 10
groups:
security-updates:
applies-to: security-updates
patterns: ["*"]Stage 4: Secrets Scanning
Hardcoded secrets (API keys, passwords, tokens, certificates) in source code are one of the most common and impactful security failures. Once committed to git history, a secret is compromised — even if later deleted.
GitHub Secret Scanning + Push Protection
Enable in repo settings → Security → Secret scanning → Push protection. This blocks pushes containing known secret patterns from 200+ providers before they enter the repo.
Gitleaks in CI
# .github/workflows/secrets.yml
name: Secret Scanning
on: [push, pull_request]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for baseline scans
- name: Gitleaks Scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}Custom Rules for Your Environment
# .gitleaks.toml
[[rules]]
id = "company-internal-token"
description = "Internal API token pattern"
regex = '''COMPANY_[A-Z0-9]{32}'''
severity = "critical"
tags = ["internal", "api"]
[[allowlist]]
description = "Test fixtures"
paths = ['''(?i)test''', '''(?i)fixture''', '''(?i)mock''']Stage 5: Infrastructure as Code (IaC) Security
If your infrastructure is defined in Terraform, CloudFormation, Kubernetes manifests, or Helm charts, those files can contain security misconfigurations just like application code.
Checkov for Terraform
# .github/workflows/iac-security.yml
name: IaC Security Scan
on:
pull_request:
paths:
- '**.tf'
- '**.yaml'
- '**.yml'
- 'Dockerfile'
- 'docker-compose*.yml'
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkov IaC Scan
uses: bridgecrewio/checkov-action@master
with:
directory: .
framework: terraform,kubernetes,dockerfile,github_actions
soft_fail: false
output_format: sarif
output_file_path: checkov.sarif
skip_check: CKV_AWS_18 # Example: skip specific check if accepted risk
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: checkov.sarifCommon Terraform findings Checkov catches:
- S3 buckets without encryption or public access blocks
- Security groups with 0.0.0.0/0 ingress on sensitive ports
- RDS instances without deletion protection or encryption
- IAM policies with wildcard (*) actions
- CloudTrail not enabled
Stage 6: Container Image Scanning
Every container image in production should be scanned for OS package vulnerabilities and secret leaks before deployment.
Trivy in CI
# .github/workflows/container-scan.yml
name: Container Security Scan
on:
push:
branches: [main]
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Image
run: docker build -t app:${{ github.sha }} .
- name: Trivy Vulnerability Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: app:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: '1' # Fail on CRITICAL/HIGH
vuln-type: os,library
ignore-unfixed: true # Don't fail on unfixed CVEs
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarifImage Hardening Checklist
# Security-hardened Dockerfile pattern
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
# Run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /app .
USER appuser
# No SUID/SGID binaries
RUN find / -perm /6000 -type f -exec chmod a-s {} \; 2>/dev/null || true
EXPOSE 3000
CMD ["node", "server.js"]Stage 7: Dynamic Application Security Testing (DAST)
DAST tests a running application by sending crafted HTTP requests and analyzing responses. Unlike SAST, it finds runtime vulnerabilities that only appear when the app is actually executing — authentication bypasses, business logic flaws, server misconfigurations.
OWASP ZAP in CI
# .github/workflows/dast.yml
name: DAST Scan
on:
workflow_run:
workflows: ["Deploy to Staging"]
types: [completed]
jobs:
zap-scan:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: ZAP API Scan
uses: zaproxy/action-api-scan@v0.7.0
with:
target: https://staging.yourapp.com/api/openapi.json
rules_file_name: .zap/rules.tsv
cmd_options: '-a -j' # Include ajax spider
- name: ZAP Full Scan
uses: zaproxy/action-full-scan@v0.10.0
with:
target: https://staging.yourapp.com
rules_file_name: .zap/rules.tsv
fail_action: trueDAST is the noisiest scan type. Configure a baseline rules file to suppress known false positives, and run it asynchronously (don't block the deployment, but create issues from findings).
The Security Gate Policy
The most important DevSecOps decision is: what blocks the pipeline vs. what's informational?
| Severity | Recommended Gate |
|---|---|
| Critical CVE (CVSS 9.0+) in dependency | Block PR merge |
| High CVE (CVSS 7.0-8.9) in dependency | Block PR merge |
| Hardcoded secret | Block PR merge + alert security team |
| Critical SAST finding (injection, XXE) | Block PR merge |
| High SAST finding | Block PR merge |
| Medium SAST finding | Warning, doesn't block |
| IaC critical misconfiguration | Block PR merge |
| Medium CVE (unfixed) | Warning, informational |
| License violation | Block PR merge |
Start conservative: block only critical severity across all scan types. Gradually add high severity as your security backlog shrinks. Teams that try to enforce medium severity gates from day one get alert fatigue and start suppressing findings indiscriminately.
SBOM Generation and Artifact Signing
In 2026, producing a Software Bill of Materials (SBOM) and signing your artifacts is a compliance expectation for many regulated industries and government supply chains.
# Add to your release workflow
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: your-registry/app:${{ github.sha }}
format: spdx-json
output-file: sbom.spdx.json
- name: Sign Image with Cosign
uses: sigstore/cosign-installer@v3
- name: Sign Container Image
run: |
cosign sign --yes \
--key env://COSIGN_PRIVATE_KEY \
your-registry/app:${{ github.sha }}
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}Building the DevSecOps Culture
Tools are the easy part. The harder part is organizational change.
Make security findings appear where developers work. SARIF integration with GitHub Security tab means developers see findings in pull requests, not in a separate security portal they'll never open.
Provide context, not just flags. A vulnerability alert that says "Use parameterized queries" with a link to the affected line and an explanation is actionable. A vulnerability alert that says "SQL Injection detected" with a file path is not.
Track security debt like technical debt. Create a Security issues project in your issue tracker. Triage findings weekly. Don't let the backlog grow unbounded — a finding that's been open for 18 months will never be fixed.
Measure what matters.
- Mean time to remediate critical findings (target: <7 days)
- Percentage of PRs passing all security gates on first attempt (target: >90%)
- New critical/high CVEs introduced per sprint (target: 0)
- Secret leak incidents (target: 0)
DevSecOps isn't a tool you install — it's a discipline you build over time. Start with the highest-value controls (secrets scanning, critical CVE blocking), get developer buy-in by reducing friction, and expand coverage incrementally. The teams that succeed treat security findings as software quality issues, not compliance checkboxes.
Microsoft Cloud Solution Architect
Cloud Solution Architect with deep expertise in Microsoft Azure and a strong background in systems and IT infrastructure. Passionate about cloud technologies, security best practices, and helping organizations modernize their infrastructure.
Questions & Answers
Related Articles
Need Help with Your Security?
Our team of security experts can help you implement the strategies discussed in this article.
Contact Us