DevSecOps Guide 2026 — Security in Every Stage of the CI/CD Pipeline
Advertisement
Introduction
Why This Matters
The average data breach costs $4.45 million and takes 277 days to identify. DevSecOps catches vulnerabilities at the source — during development and in CI/CD — when fixes cost minutes instead of millions. Shifting security left means automated scanning on every commit, not a manual penetration test after the product ships. This guide builds a complete security scanning pipeline you can drop into any GitHub Actions workflow.
SAST: Static Code Analysis with CodeQL
# .github/workflows/security.yml
name: Security Scanning
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 8 * * 1' # Weekly on Monday
jobs:
codeql:
name: CodeQL Analysis
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
strategy:
fail-fast: false
matrix:
language: ['javascript', 'typescript']
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-and-quality
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
with:
category: '/language:${{ matrix.language }}'Dependency Scanning
dependency-scan:
name: Dependency Vulnerabilities
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: npm audit
run: npm audit --audit-level=high
# Fails if HIGH or CRITICAL vulnerabilities found
- name: Snyk scan
uses: snyk/actions/node@master
with:
args: --severity-threshold=high
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'my-app'
path: '.'
format: 'HTML'
args: >
--failOnCVSS 7
--enableRetired
- uses: actions/upload-artifact@v4
if: always()
with:
name: dependency-report
path: reports/Secrets Detection
secrets-scan:
name: Secrets Detection
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for Gitleaks
- name: Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: TruffleHog
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
extra_args: --only-verified# .gitleaks.toml — custom rules
[[rules]]
description = "Custom API Key"
regex = '''(?i)myapp[_-]?api[_-]?key['":=\s]+[a-zA-Z0-9]{20,}'''
tags = ["api", "custom"]
[allowlist]
paths = [".gitleaks.toml", "tests/fixtures/"]
regexes = ["EXAMPLE_KEY", "YOUR_KEY_HERE", "test_key_.*"]Container Security Scanning
container-scan:
name: Container Vulnerability Scan
runs-on: ubuntu-latest
needs: build
steps:
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'ghcr.io/${{ github.repository }}:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'Harden your Dockerfile to reduce attack surface:
# Use minimal Alpine base
FROM node:20-alpine
# Install security updates first
RUN apk update && apk upgrade && rm -rf /var/cache/apk/*
# Create non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nodeuser
WORKDIR /app
# Copy and install dependencies
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy application with correct ownership
COPY --chown=nodeuser:nodejs . .
# Switch to non-root user
USER nodeuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]SBOM: Software Bill of Materials
sbom:
name: Generate SBOM
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate SBOM with Syft
uses: anchore/sbom-action@v0
with:
format: spdx-json
output-file: sbom.spdx.json
- name: Scan SBOM with Grype
uses: anchore/scan-action@v3
with:
sbom: sbom.spdx.json
fail-build: true
severity-cutoff: high
- uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.jsonInfrastructure Security with Terraform
# Least-privilege security group — allow only HTTPS inbound
resource "aws_security_group" "api" {
name = "api-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP — redirect to HTTPS only"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}Run checkov to scan Terraform for misconfigurations:
pip install checkov
checkov -d terraform/ --framework terraform --quiet
# Or in CI
checkov -d terraform/ \
--soft-fail \
--output-file-path checkov-results.xml \
--output sarifPR Security Checklist
## Security Review Checklist
- [ ] Gitleaks passed — no secrets in diff
- [ ] npm audit passed — no HIGH or CRITICAL CVEs
- [ ] Input validated on all new API endpoints
- [ ] Authorization check for user-owned resources
- [ ] SQL queries use parameterized statements only
- [ ] New third-party packages reviewed for security
- [ ] Sensitive fields not included in logs
- [ ] New environment variables documented and added to Vault/Secrets ManagerCommon Mistakes
- Ignoring npm audit warnings —
moderateseverity vulnerabilities become critical when chained with other issues - Scanning only on merge to main — by then, the vulnerable code is in the default branch; scan on every PR
- No SBOM generation — without a software bill of materials, you cannot quickly assess exposure to new CVEs like Log4Shell
- Running containers as root — most container images default to root; always set
USER nonrootin your Dockerfile - SSH port 22 open to 0.0.0.0/0 — use AWS SSM Session Manager or restrict SSH to specific bastion IPs
Best Practices
- Make security gates non-blocking in the first week when adopting DevSecOps — build trust before enforcing failures
- Store all secrets in a secret manager (Vault, AWS Secrets Manager) and rotate them automatically; never commit
.envfiles - Enable Dependabot in every repository to get automatic PRs for dependency security patches
- Use separate AWS accounts for production and development — limits blast radius of compromised credentials
- Review CodeQL Security Advisories weekly — GitHub notifies you when new CVEs affect your dependencies
Key Takeaways
- CodeQL performs deep static analysis that finds injection flaws, XSS, and authentication bypasses that linters miss
- Gitleaks scans the full git history, not just the current diff — it catches secrets committed months ago
- Trivy scans OS packages, language dependencies, and Kubernetes manifests in a single tool with near-zero false positives
- SBOM (Software Bill of Materials) in SPDX or CycloneDX format documents every dependency so you can assess CVE exposure instantly
- Running containers as non-root eliminates an entire class of privilege escalation exploits at zero performance cost
npm audit --audit-level=highfails the build on HIGH and CRITICAL CVEs — the right threshold for most production applications- DevSecOps does not slow teams down — automated scanning is faster than manual security reviews and catches more issues
- IaC scanning with checkov or tfsec finds misconfigurations (open security groups, unencrypted S3) before
terraform apply
Advertisement
Related reading
Docker Security Best Practices 2025 — Hardening Containers for Production7 min readDocker Best Practices 2025 — Production Checklist for Secure, Lean Images6 min readDocker vs Podman 2025 — Which Container Runtime Should You Use?8 min readGitHub Actions Secrets and Environments — Security Guide 20267 min readSSH Keys — Setup, Management, and Security Best Practices 20265 min readDocker Best Practices in 2026 — Production-Ready Containers6 min read