DevSecOps Integration: Security as Code
How to embed security into every stage of your CI/CD pipeline — from pre-commit hooks to production monitoring, with practical tooling and automation examples.
Security Can’t Be a Gate — It Must Be a Pipeline
Traditional security operates as a checkpoint: developers write code, security reviews it weeks later, and sends back a list of findings. By then, the code is in production, the developer has moved on, and the findings get deprioritized.
DevSecOps flips this model. Security is automated, continuous, and embedded directly into the development workflow. Every commit triggers security checks. Every pull request includes vulnerability data. Every deployment is validated against policy. Security becomes code.
The DevSecOps Pipeline
┌─────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌──────────┐
│ Code │→ │ Build │→ │ Test │→ │ Deploy │→ │ Monitor │
└────┬────┘ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │ │
Pre-commit SAST DAST/IAST Policy-as- Runtime
hooks SCA Integration Code Security
Secrets Container Pen testing RBAC SIEM
scanning scanning Fuzzing Secrets Alerting
SBOM management
Each stage has specific tools and practices. Let’s build it.
Stage 1: Code — Pre-Commit Security
Secret Detection
The #1 preventable security incident: secrets committed to version control.
# Install gitleaks for pre-commit secret scanning
brew install gitleaks # or go install github.com/gitleaks/gitleaks/v8@latest
# Scan entire repo history
gitleaks detect --source . --verbose
# Install as pre-commit hook
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
EOF
pip install pre-commit
pre-commit install
Commit-Time Linting
# .pre-commit-config.yaml — comprehensive security hooks
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
- 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=500']
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint-docker
name: Lint Dockerfiles
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.5
hooks:
- id: terraform_tfsec
name: Terraform security scan
Stage 2: Build — Static Analysis and SCA
SAST (Static Application Security Testing)
Analyze source code for vulnerabilities without executing it:
# Semgrep — lightweight, fast, multi-language SAST
pip install semgrep
# Run with OWASP rules
semgrep scan --config "p/owasp-top-ten" --json -o sast-results.json
# Run with auto detection of language and framework
semgrep scan --config auto .
# Custom rules for your codebase
cat > .semgrep/custom-rules.yml << 'EOF'
rules:
- id: no-eval-user-input
pattern: eval($USER_INPUT)
message: "Never eval user-controlled input"
severity: ERROR
languages: [python]
- id: sql-string-concat
patterns:
- pattern: |
$QUERY = "..." + $USER_INPUT + "..."
- metavariable-pattern:
metavariable: $QUERY
pattern-regex: "(?i)(select|insert|update|delete)"
message: "SQL query built via string concatenation"
severity: ERROR
languages: [python, javascript, java]
EOF
semgrep scan --config .semgrep/custom-rules.yml
SCA (Software Composition Analysis)
Your dependencies are your attack surface. 90%+ of modern applications are third-party code:
# Trivy — comprehensive vulnerability scanner
# Scans: container images, filesystems, git repos, IaC
# Scan project dependencies
trivy fs --scanners vuln,secret,misconfig .
# Scan with severity filter
trivy fs --severity CRITICAL,HIGH .
# Generate SBOM (Software Bill of Materials)
trivy fs --format spdx-json -o sbom.json .
# Scan container image
trivy image myapp:latest
# Scan Kubernetes manifests
trivy config ./k8s/
Container Image Scanning
# Scan during Docker build
# Dockerfile
FROM node:20-alpine AS build
COPY . .
RUN npm ci --production
# Scan the built image
trivy image --exit-code 1 --severity CRITICAL myapp:latest
# Alternatively, use Grype
grype myapp:latest --fail-on critical
SBOM Generation
Software Bill of Materials — know exactly what’s in your software:
# Generate SBOM with syft
syft myapp:latest -o spdx-json > sbom.spdx.json
# Scan SBOM for vulnerabilities
grype sbom:sbom.spdx.json
# Track SBOM in your CI artifacts
# Store alongside each release for supply chain transparency
Stage 3: Test — Dynamic Analysis
DAST (Dynamic Application Security Testing)
Test the running application from an attacker’s perspective:
# ZAP (Zed Attack Proxy) — automated DAST
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
-t https://staging.myapp.com \
-r zap-report.html \
-l WARN
# Full scan with authentication
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-full-scan.py \
-t https://staging.myapp.com \
-r zap-full-report.html \
--hook=/zap/auth-hook.py
# Nuclei — fast vulnerability scanner with community templates
nuclei -u https://staging.myapp.com -t cves/ -t vulnerabilities/ -severity critical,high
API Security Testing
# Scan OpenAPI spec for security issues
spectral lint openapi.yaml
# Fuzz API endpoints
cat > api-fuzz.yaml << 'EOF'
fuzzing:
- target: "{{BaseURL}}/api/v1/users"
method: POST
body: |
{"email": "{{fuzz}}", "password": "{{fuzz}}"}
payloads:
fuzz:
- type: file
path: /usr/share/seclists/Fuzzing/special-chars.txt
EOF
nuclei -t api-fuzz.yaml -var BaseURL=https://staging.myapp.com
Stage 4: Deploy — Policy as Code
OPA (Open Policy Agent)
Define deployment policies programmatically:
# policy/kubernetes.rego
package kubernetes.admission
# Deny containers running as root
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf("Container '%v' must set runAsNonRoot: true", [container.name])
}
# Deny images without digest
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not contains(container.image, "@sha256:")
msg := sprintf("Container '%v' must use image digest, not tag", [container.name])
}
# Deny privileged containers
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
container.securityContext.privileged
msg := sprintf("Container '%v' must not be privileged", [container.name])
}
Secrets Management
# Never store secrets in code, environment variables, or config files
# Option 1: HashiCorp Vault
vault kv put secret/myapp/db password=supersecret
vault kv get -field=password secret/myapp/db
# Option 2: SOPS (encrypted files in git)
sops --encrypt --age $(cat ~/.config/sops/age/keys.txt | grep public | awk '{print $NF}') \
secrets.yaml > secrets.enc.yaml
# Option 3: External Secrets Operator (Kubernetes)
cat > external-secret.yaml << 'EOF'
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: myapp-secrets
data:
- secretKey: db-password
remoteRef:
key: secret/myapp/db
property: password
EOF
Stage 5: Monitor — Runtime Security
Falco — Runtime Threat Detection
# Detect suspicious behavior in production
# falco_rules.yaml
- rule: Shell Spawned in Container
desc: Detect shell execution in a running container
condition: >
spawned_process and container and
proc.name in (bash, sh, zsh, dash, ksh)
output: >
Shell spawned in container
(user=%user.name container=%container.name
shell=%proc.name parent=%proc.pname)
priority: WARNING
- rule: Sensitive File Access
desc: Detect read of sensitive files
condition: >
open_read and container and
fd.name in (/etc/shadow, /etc/passwd, /proc/self/environ)
output: >
Sensitive file accessed
(file=%fd.name user=%user.name container=%container.name)
priority: CRITICAL
Security Alerting Pipeline
# GitHub Actions — complete security pipeline
name: Security Pipeline
on: [push, pull_request]
jobs:
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: "p/owasp-top-ten"
sca:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
severity: 'CRITICAL,HIGH'
exit-code: '1'
container:
runs-on: ubuntu-latest
needs: [secrets, sast, sca]
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
exit-code: '1'
severity: 'CRITICAL'
- name: Generate SBOM
run: |
syft myapp:${{ github.sha }} -o spdx-json > sbom.json
- uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json
Metrics That Matter
Track these to measure your DevSecOps maturity:
Mean Time to Remediate (MTTR):
Critical: < 24 hours
High: < 7 days
Medium: < 30 days
Vulnerability Escape Rate:
% of vulnerabilities that reach production
Target: < 5%
Security Debt:
Total unresolved findings weighted by severity
Trend: decreasing quarter over quarter
Pipeline Block Rate:
% of builds blocked by security gates
Target: < 10% (too high = too noisy, developers will bypass)
Coverage:
% of repos with security scanning enabled
Target: 100%
Cultural Shift
Tools are the easy part. The hard part is culture:
- Security is everyone’s job — not a team you throw tickets at
- Automate everything — manual reviews don’t scale
- Shift left, but don’t shift blame — give developers the tools and training
- Reduce friction — if security slows delivery, people will bypass it
- Celebrate findings — a caught vulnerability is a win, not a failure
The pipeline is your security perimeter. Build it strong.