DevSecOps: Shifting Security Left in CI/CD Pipelines
Integrate security throughout the software development lifecycle. From pre-commit hooks to production monitoring, build a security-first culture without slowing delivery.
DevSecOps: Shifting Security Left in CI/CD Pipelines
Security can no longer be an afterthought bolted on at the end of development. DevSecOps integrates security practices throughout the software development lifecycle, catching vulnerabilities early when they're cheapest to fix.
The Shift Left Philosophy
Traditional vs DevSecOps Approach
Traditional Security:
Code → Build → Test → Deploy → [Security Review] → Production
↑
Late, expensive fixes
DevSecOps Approach:
[Pre-commit] → [Build] → [Test] → [Stage] → [Deploy] → Production
↓ ↓ ↓ ↓ ↓
Security Security Security Security Security
Checks Checks Checks Checks ChecksCost of Fixing Vulnerabilities
| Stage | Relative Cost | Time to Fix |
|---|---|---|
| Development | 1x | Minutes |
| Build/CI | 5x | Hours |
| Testing | 10x | Days |
| Staging | 25x | Days-Weeks |
| Production | 100x | Weeks-Months |
Security at Every Stage
1. Pre-Commit Security
Git Hooks with Husky
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged && npm run security:quick"
}
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml}": [
"prettier --write"
]
},
"scripts": {
"security:quick": "secretlint . && npm audit --audit-level=high"
}
}Secret Detection with Secretlint
// .secretlintrc.json
{
"rules": [
{
"id": "@secretlint/secretlint-rule-preset-recommend"
},
{
"id": "@secretlint/secretlint-rule-aws",
"options": {
"allows": []
}
},
{
"id": "@secretlint/secretlint-rule-gcp"
},
{
"id": "@secretlint/secretlint-rule-privatekey"
}
]
}Pre-commit Framework
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: detect-private-key
- id: detect-aws-credentials
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint
args: ['--ignore', 'DL3008']
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.5
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tfsec2. Build-Time Security
Complete CI Pipeline
# .github/workflows/devsecops-pipeline.yml
name: DevSecOps Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
# Stage 1: Quick Security Checks
security-quick:
name: Quick Security Scan
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect secrets with Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run Secretlint
run: npx secretlint "**/*"
# Stage 2: Dependency Analysis
dependency-scan:
name: Dependency Security
runs-on: ubuntu-latest
needs: security-quick
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: NPM Audit
run: npm audit --audit-level=high
- name: Snyk Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
- name: License Check
run: npx license-checker --failOn "GPL;AGPL"
# Stage 3: Static Analysis
sast-scan:
name: SAST Analysis
runs-on: ubuntu-latest
needs: security-quick
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/owasp-top-ten
p/typescript
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
# Stage 4: Build and Container Scan
build-scan:
name: Build & Container Security
runs-on: ubuntu-latest
needs: [dependency-scan, sast-scan]
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t app:${{ github.sha }} .
- name: Trivy Container Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: app:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
- name: Dockle Lint
uses: erzz/dockle-action@v1
with:
image: app:${{ github.sha }}
failure-threshold: high
# Stage 5: Infrastructure as Code Scan
iac-scan:
name: IaC Security
runs-on: ubuntu-latest
needs: security-quick
steps:
- uses: actions/checkout@v4
- name: Checkov IaC Scan
uses: bridgecrewio/checkov-action@v12
with:
directory: terraform/
framework: terraform
soft_fail: false
- name: Trivy Config Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'config'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
# Stage 6: Security Gate
security-gate:
name: Security Gate
runs-on: ubuntu-latest
needs: [dependency-scan, sast-scan, build-scan, iac-scan]
steps:
- name: Security Gate Check
run: |
echo "All security checks passed"
echo "Pipeline approved for deployment"3. Runtime Security
Container Security Context
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-app
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: app:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
cpu: "500m"
memory: "256Mi"
requests:
cpu: "100m"
memory: "128Mi"
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/.cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}Network Policies
# kubernetes/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: app-network-policy
spec:
podSelector:
matchLabels:
app: secure-app
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
- podSelector:
matchLabels:
app: api-gateway
ports:
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
name: database
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53Security Policies as Code
Open Policy Agent (OPA)
# policies/kubernetes.rego
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.securityContext.readOnlyRootFilesystem
msg := sprintf("Container %v must have readOnlyRootFilesystem", [container.name])
}
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
container.securityContext.privileged
msg := sprintf("Container %v cannot be privileged", [container.name])
}
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.resources.limits
msg := sprintf("Container %v must have resource limits", [container.name])
}
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
image := container.image
not startswith(image, "registry.company.com/")
msg := sprintf("Container %v uses non-approved registry", [container.name])
}Kyverno Policies
# policies/kyverno-policies.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
validationFailureAction: Enforce
rules:
- name: require-team-label
match:
resources:
kinds:
- Pod
- Deployment
validate:
message: "The label 'team' is required"
pattern:
metadata:
labels:
team: "?*"
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-latest-tag
spec:
validationFailureAction: Enforce
rules:
- name: validate-image-tag
match:
resources:
kinds:
- Pod
validate:
message: "Using 'latest' tag is not allowed"
pattern:
spec:
containers:
- image: "!*:latest"Security Monitoring and Response
Runtime Threat Detection
# falco/custom-rules.yaml
- rule: Detect Cryptocurrency Mining
desc: Detect cryptocurrency mining activity
condition: >
spawned_process and
(proc.name in (crypto_miner_names) or
proc.cmdline contains "stratum" or
proc.cmdline contains "cryptonight")
output: >
Crypto mining detected
(user=%user.name command=%proc.cmdline container=%container.name)
priority: CRITICAL
- rule: Shell Spawned in Container
desc: Detect shell spawned in container
condition: >
container and
proc.name in (shell_binaries) and
not proc.pname in (allowed_shell_parents)
output: >
Shell spawned in container
(user=%user.name shell=%proc.name container=%container.name)
priority: WARNING
- rule: Sensitive File Access
desc: Detect access to sensitive files
condition: >
open_read and
fd.name in (sensitive_files) and
not proc.name in (allowed_sensitive_readers)
output: >
Sensitive file accessed
(user=%user.name file=%fd.name proc=%proc.name)
priority: HIGHSecurity Dashboard Metrics
// security-metrics.ts
interface SecurityMetrics {
vulnerabilities: {
critical: number;
high: number;
medium: number;
low: number;
trend: 'increasing' | 'stable' | 'decreasing';
};
compliance: {
passRate: number;
failedChecks: string[];
lastScanTime: Date;
};
incidents: {
open: number;
resolved24h: number;
mttr: number; // Mean time to remediate
};
coverage: {
reposScanned: number;
totalRepos: number;
containersScanned: number;
};
}
async function collectSecurityMetrics(): Promise<SecurityMetrics> {
const [vulns, compliance, incidents, coverage] = await Promise.all([
fetchVulnerabilityData(),
fetchComplianceData(),
fetchIncidentData(),
fetchCoverageData()
]);
return {
vulnerabilities: vulns,
compliance: compliance,
incidents: incidents,
coverage: coverage
};
}Security Champions Programme
Building Security Culture
Security Champion Responsibilities:
├── Code Review Focus
│ ├── Review security-sensitive changes
│ ├── Ensure secure coding practices
│ └── Mentor team on security
├── Knowledge Sharing
│ ├── Run security brown bags
│ ├── Share threat intelligence
│ └── Document lessons learned
├── Tool Ownership
│ ├── Configure security tools
│ ├── Triage false positives
│ └── Improve detection rules
└── Incident Response
├── First responder for team
├── Coordinate with security team
└── Post-incident reviewsSecurity Training Matrix
| Role | Required Training | Frequency |
|---|---|---|
| All Developers | OWASP Top 10 | Annual |
| Security Champions | Advanced Secure Coding | Bi-annual |
| DevOps Engineers | Container Security | Annual |
| Architects | Threat Modelling | Annual |
| Managers | Security Awareness | Annual |
Compliance Automation
Evidence Collection
# compliance/evidence-collection.yml
name: Compliance Evidence Collection
on:
schedule:
- cron: '0 0 * * 0' # Weekly
jobs:
collect-evidence:
runs-on: ubuntu-latest
steps:
- name: Export Security Scan Results
run: |
# Export vulnerability scan results
snyk test --json > evidence/vulnerability-scan.json
# Export compliance scan results
checkov -d . --output-file evidence/compliance-scan.json
# Export audit logs
gh api /orgs/${{ github.repository_owner }}/audit-log \
> evidence/audit-log.json
- name: Generate Compliance Report
run: |
node scripts/generate-compliance-report.js
- name: Upload Evidence
uses: actions/upload-artifact@v4
with:
name: compliance-evidence-${{ github.run_id }}
path: evidence/
retention-days: 365Key Takeaways
-
Shift left, but don't stop there: Security at every stage, from pre-commit to production
-
Automate everything: Manual security reviews don't scale—automate checks in CI/CD
-
Policy as code: Use OPA, Kyverno, or similar tools to enforce security policies automatically
-
Build security culture: Security champions and training programmes make security everyone's responsibility
-
Measure and improve: Track security metrics and use them to drive continuous improvement
-
Fail fast, fix fast: Quick feedback loops help developers fix issues while context is fresh
-
Defence in depth: No single control is sufficient—layer security throughout the pipeline
DevSecOps isn't about adding more gates—it's about integrating security seamlessly into how teams already work. The goal is secure software delivered at speed.