Skip to main content
Back to Blog
15 November 202515 min read

DevSecOps: Shifting Security Left in CI/CD Pipelines

DevSecOpsSecurityCI/CDBest Practices

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    Checks

Cost of Fixing Vulnerabilities

StageRelative CostTime to Fix
Development1xMinutes
Build/CI5xHours
Testing10xDays
Staging25xDays-Weeks
Production100xWeeks-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_tfsec

2. 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: 53

Security 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: HIGH

Security 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 reviews

Security Training Matrix

RoleRequired TrainingFrequency
All DevelopersOWASP Top 10Annual
Security ChampionsAdvanced Secure CodingBi-annual
DevOps EngineersContainer SecurityAnnual
ArchitectsThreat ModellingAnnual
ManagersSecurity AwarenessAnnual

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: 365

Key Takeaways

  1. Shift left, but don't stop there: Security at every stage, from pre-commit to production

  2. Automate everything: Manual security reviews don't scale—automate checks in CI/CD

  3. Policy as code: Use OPA, Kyverno, or similar tools to enforce security policies automatically

  4. Build security culture: Security champions and training programmes make security everyone's responsibility

  5. Measure and improve: Track security metrics and use them to drive continuous improvement

  6. Fail fast, fix fast: Quick feedback loops help developers fix issues while context is fresh

  7. 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.

Share this article