Back to Modern DevOps & Platform Engineering Series

Part 13: DevSecOps Foundations

May 15, 2026 Wasil Zafar 30 min read

Shift security left into every stage of the software delivery lifecycle — supply chain security, container scanning, SBOM generation, policy-as-code, and automated security gates in CI/CD pipelines.

Table of Contents

  1. Introduction
  2. Supply Chain Security
  3. Container Image Scanning
  4. Policy-as-Code
  5. Secrets & Runtime Security
  6. Secure CI/CD Pipeline
  7. Conclusion & Next Steps

What is DevSecOps?

DevSecOps integrates security practices into every phase of the software development lifecycle — from code commit through build, test, deploy, and runtime. Instead of security being a final gate before production (the traditional "throw it over the wall" approach), DevSecOps makes security a shared responsibility embedded in every pipeline stage.

Think of it like building safety into a car's design from day one — crumple zones, airbags, ABS — rather than adding crash padding as an afterthought. The car is safer because safety was part of the engineering process, not bolted on at the end.

Key Insight: DevSecOps is not about adding more security tools. It's about making the secure path the easiest path. When developers can't accidentally deploy an unscanned image, use a root container, or expose secrets in code — that's DevSecOps working correctly.

The Shift-Left Philosophy

Shift-Left Security — Find Issues Earlier
flowchart LR
    Code["Code
SAST, Linting"] --> Build["Build
Dependency Scan"] Build --> Test["Test
DAST, Fuzzing"] Test --> Deploy["Deploy
Image Scan, Sign"] Deploy --> Runtime["Runtime
Monitoring, WAF"] style Code fill:#e8f4f4,stroke:#3B9797,color:#132440 style Build fill:#e8f4f4,stroke:#3B9797,color:#132440 style Test fill:#f0f4f8,stroke:#16476A,color:#132440 style Deploy fill:#f0f4f8,stroke:#16476A,color:#132440 style Runtime fill:#fff5f5,stroke:#BF092F,color:#132440

The cost of fixing a security vulnerability increases exponentially the later it's found. A misconfigured Dockerfile caught in a PR review costs minutes to fix. The same vulnerability discovered in production during a penetration test costs days of incident response, patching, and auditing.

Software Supply Chain Security

Supply chain attacks target the tools, libraries, and processes used to build software rather than the software itself. The SolarWinds attack (2020) and Log4Shell vulnerability (2021) demonstrated that a single compromised dependency can affect thousands of organisations. DevSecOps addresses this through provenance tracking, artifact signing, and SBOM generation.

Image Signing with Cosign & Sigstore

# Install Cosign — keyless container image signing
# Part of the Sigstore project (Linux Foundation)
go install github.com/sigstore/cosign/v2/cmd/cosign@latest

# Sign a container image (keyless — uses OIDC identity)
cosign sign myregistry/web-app:v1.2.0
# Opens browser for OIDC authentication
# Signature stored in the registry alongside the image

# Verify an image signature before deployment
cosign verify myregistry/web-app:v1.2.0 \
  --certificate-identity=ci@example.com \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com
# Verification for myregistry/web-app:v1.2.0 --
# The following checks were performed:
# - The cosign claims were validated
# - The signatures were verified against the specified public key

echo "Image signed and verified successfully"

SBOM Generation (Software Bill of Materials)

Definition: A Software Bill of Materials (SBOM) is a machine-readable inventory of all components, libraries, and dependencies in a software artifact. US Executive Order 14028 (2021) requires SBOMs for all software sold to the federal government. Common formats include SPDX and CycloneDX.
# Generate SBOM with Syft (Anchore)
# Scans container images and produces CycloneDX or SPDX output
syft myregistry/web-app:v1.2.0 -o cyclonedx-json > sbom.json

# Attach SBOM to the container image in the registry
cosign attach sbom --sbom sbom.json myregistry/web-app:v1.2.0

# Generate SBOM from a Dockerfile / project directory
syft dir:./src -o spdx-json > project-sbom.spdx.json

# Scan the SBOM for known vulnerabilities
grype sbom:./sbom.json
# NAME          INSTALLED  VULNERABILITY  SEVERITY
# openssl       3.0.8      CVE-2023-xxxx  High
# libcurl       7.88.1     CVE-2023-yyyy  Medium

echo "SBOM generated and scanned"

Container Image Scanning

Trivy — Comprehensive Vulnerability Scanner

# Trivy scans container images, filesystems, Git repos, and K8s clusters
# Install Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# Scan a container image for vulnerabilities
trivy image myregistry/web-app:v1.2.0
# web-app:v1.2.0 (debian 12.4)
# Total: 23 (UNKNOWN: 0, LOW: 10, MEDIUM: 8, HIGH: 4, CRITICAL: 1)

# Scan with severity filter (fail on HIGH or CRITICAL)
trivy image --severity HIGH,CRITICAL --exit-code 1 myregistry/web-app:v1.2.0

# Scan Kubernetes manifest files for misconfigurations
trivy config ./k8s-manifests/
# Checks: 52 (UNKNOWN: 0, LOW: 5, MEDIUM: 12, HIGH: 3, CRITICAL: 0)

# Scan a running Kubernetes cluster
trivy k8s --report summary cluster
echo "Trivy scan complete"

CI Pipeline Integration

# .github/workflows/security-scan.yaml
name: Security Scan Pipeline
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Step 1: Secret scanning (prevent leaked credentials)
      - name: Scan for secrets
        uses: trufflesecurity/trufflehog@main
        with:
          extra_args: --only-verified

      # Step 2: SAST — Static Application Security Testing
      - name: Run Semgrep SAST
        uses: semgrep/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/javascript
            p/typescript

      # Step 3: Dependency vulnerability scan
      - name: Dependency scan
        run: trivy fs --severity HIGH,CRITICAL --exit-code 1 .

      # Step 4: Build and scan container image
      - name: Build Docker image
        run: docker build -t myregistry/web-app:${{ github.sha }} .

      - name: Scan container image
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myregistry/web-app:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          exit-code: 1

      # Step 5: Upload results to GitHub Security tab
      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-results.sarif

      # Step 6: Generate and attach SBOM
      - name: Generate SBOM
        run: |
          syft myregistry/web-app:${{ github.sha }} -o cyclonedx-json > sbom.json
          cosign attach sbom --sbom sbom.json myregistry/web-app:${{ github.sha }}

      # Step 7: Sign the image
      - name: Sign image
        run: cosign sign myregistry/web-app:${{ github.sha }}
        env:
          COSIGN_EXPERIMENTAL: 1

Policy-as-Code

Policy-as-code encodes security and compliance rules as declarative configurations that are automatically enforced. Instead of relying on documentation and manual reviews, policies are evaluated by admission controllers in the Kubernetes API server — blocking non-compliant resources before they're created.

Kyverno — Kubernetes-Native Policy Engine

# kyverno-require-labels.yaml — Enforce mandatory labels
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
  annotations:
    policies.kyverno.io/title: Require Labels
    policies.kyverno.io/severity: medium
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: require-team-label
      match:
        any:
          - resources:
              kinds: ["Deployment", "StatefulSet", "DaemonSet"]
      validate:
        message: "The label 'team' is required on all workloads."
        pattern:
          metadata:
            labels:
              team: "?*"
# kyverno-disallow-privileged.yaml — Block privileged containers
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged-containers
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: deny-privileged
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Privileged containers are not allowed."
        pattern:
          spec:
            containers:
              - securityContext:
                  privileged: "false"
    - name: require-non-root
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Containers must run as non-root."
        pattern:
          spec:
            containers:
              - securityContext:
                  runAsNonRoot: true
# kyverno-verify-images.yaml — Only allow signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds: ["Pod"]
      verifyImages:
        - imageReferences:
            - "myregistry/*"
          attestors:
            - entries:
                - keyless:
                    subject: "ci@example.com"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev

OPA Gatekeeper

# OPA Gatekeeper constraint template — Resource limits required
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredresources
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredResources
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredresources
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.cpu
          msg := sprintf("Container '%v' must have CPU limits", [container.name])
        }
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.memory
          msg := sprintf("Container '%v' must have memory limits", [container.name])
        }
Case Study Shopify

Shopify's Policy-as-Code at Scale

Shopify runs over 400,000 Kubernetes pods across multiple clusters. Their DevSecOps framework uses Kyverno with 85 cluster policies covering security (no privileged containers, image signing verification), compliance (resource limits, labels), and operational standards (pod disruption budgets, topology spread). Policies are stored in Git, version-controlled, and deployed via Argo CD — making security policy changes go through the same PR review process as application code. Non-compliant resources are blocked before creation, with clear error messages guiding developers toward the correct configuration.

400K+ Pods 85 Policies Kyverno

Secrets & Runtime Security

# TruffleHog — scan Git history for leaked secrets
trufflehog git file://. --only-verified --json

# Gitleaks — fast secret scanner for CI pipelines
gitleaks detect --source . --verbose
# Finding:     AWS Access Key ID
# Secret:      AKIA...
# File:        config/settings.py
# Line:        42
# Commit:      a3f7c2d

# Pre-commit hook to prevent secret commits
# .pre-commit-config.yaml
cat > .pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks
EOF

echo "Secret scanning configured"

Runtime Security with Falco

# Falco rule — detect shell in container at runtime
- rule: Terminal shell in container
  desc: Detect a shell process spawned in a container
  condition: >
    spawned_process and container and
    proc.name in (bash, sh, zsh, dash) and
    not proc.pname in (cron, containerd-shim)
  output: >
    Shell spawned in container
    (user=%user.name container=%container.name
     shell=%proc.name parent=%proc.pname
     image=%container.image.repository)
  priority: WARNING
  tags: [container, shell, mitre_execution]

- rule: Sensitive file access
  desc: Detect read of sensitive files like /etc/shadow
  condition: >
    open_read and container and
    fd.name in (/etc/shadow, /etc/passwd, /root/.ssh)
  output: >
    Sensitive file opened for reading
    (file=%fd.name user=%user.name container=%container.name)
  priority: CRITICAL
  tags: [container, filesystem, mitre_credential_access]

Building a Secure CI/CD Pipeline

End-to-End Security Pipeline Architecture

DevSecOps Pipeline Stages
flowchart TD
    PR["PR Created"] --> Secrets["Secret Scan
(Gitleaks)"] Secrets --> SAST["SAST
(Semgrep)"] SAST --> Deps["Dependency Scan
(Trivy FS)"] Deps --> Build["Docker Build"] Build --> ImgScan["Image Scan
(Trivy Image)"] ImgScan --> SBOM["Generate SBOM
(Syft)"] SBOM --> Sign["Sign Image
(Cosign)"] Sign --> Deploy["Deploy to K8s"] Deploy --> Admission["Admission Control
(Kyverno)"] Admission --> Runtime["Runtime Monitor
(Falco)"] style PR fill:#f0f4f8,stroke:#16476A,color:#132440 style Secrets fill:#e8f4f4,stroke:#3B9797,color:#132440 style SAST fill:#e8f4f4,stroke:#3B9797,color:#132440 style Deps fill:#e8f4f4,stroke:#3B9797,color:#132440 style Build fill:#f0f4f8,stroke:#16476A,color:#132440 style ImgScan fill:#e8f4f4,stroke:#3B9797,color:#132440 style SBOM fill:#e8f4f4,stroke:#3B9797,color:#132440 style Sign fill:#e8f4f4,stroke:#3B9797,color:#132440 style Deploy fill:#f0f4f8,stroke:#16476A,color:#132440 style Admission fill:#fff5f5,stroke:#BF092F,color:#132440 style Runtime fill:#fff5f5,stroke:#BF092F,color:#132440
Critical Rule: Never store secrets in Git — not in code, not in environment files, not in Kubernetes manifests. Use External Secrets Operator to sync from Vault/AWS Secrets Manager, encrypt with Sealed Secrets or SOPS, and enforce secret scanning in pre-commit hooks and CI pipelines. A single leaked API key can cost millions.

Conclusion & Next Steps

DevSecOps transforms security from a bottleneck into an accelerator. By embedding security scanning, policy enforcement, artifact signing, and runtime monitoring into every pipeline stage, teams ship faster because they don't need to wait for manual security reviews — the pipeline handles it automatically.

  • Shift left — Find vulnerabilities at code commit, not in production. SAST, dependency scanning, and secret detection belong in every PR.
  • Sign everything — Container images, SBOMs, and attestations should be cryptographically signed. Verify signatures before deployment.
  • Enforce with admission control — Kyverno and OPA Gatekeeper prevent non-compliant workloads from running. Policy-as-code is auditable and version-controlled.
  • Monitor at runtime — Falco detects anomalous behaviour (shell access, sensitive file reads) in running containers.
  • Automate compliance — Git history + SBOM + signed images + policy reports = continuous compliance evidence.

Next in the Series

In Part 14: FinOps & Cloud Economics, we'll explore cloud cost optimisation — resource right-sizing, showback and chargeback models, Kubernetes cost attribution, spot instances, and building a FinOps practice.