Devops Security Integration

Production-grade guide to devops security integration covering architecture patterns, implementation strategies, testing approaches, and operational best practices for enterprise engineering teams.

DevOps Security Integration is the practice of embedding security into every stage of a software delivery lifecycle—design, build, test, deploy, and operate—so that security is not a siloed activity but a continuous, automated, and observable part of the DevOps workflow. It matters when teams need to ship software faster without increasing vulnerabilities, when compliance audits demand traceable security controls, or when incidents are caused by misconfigurations or unpatched dependencies.

Automating Security in CI/CD Pipelines

Security must be automated early and consistently. Every pull request should trigger a security check, and every merge to main should produce a validated, secure artifact ready for deployment.

Static Application Security Testing (SAST)

SAST runs on source code before compilation. It detects issues like SQL injection, insecure API usage, and coding standard violations.

A typical SAST setup uses sonar-scanner with a sonar-project.properties file:

sonar.projectKey=acme-frontend
sonar.projectName=Acme Frontend
sonar.sources=src
sonar.host.url=https://sonar.acme.internal
sonar.login=abc123def456
sonar.language=js
sonar.coverage.exclusions=**/test/**,**/node_modules/**

The CI job runs:

npm install
npx sonar-scanner

This produces a report with issues like:

src/auth/login.js:42:5: S2272 - SQL injection detected in query string.
src/api/users.js:18:12: S2068 - Insecure use of JWT without signature validation.

The most common failure is misconfigured exclusions: /test/ excludes test files, but test files often contain critical code paths. When src/api/users.js is in test/api, it fails to be analyzed—resulting in uncaught vulnerabilities.

Dynamic Application Security Testing (DAST)

DAST runs on deployed, running applications. It sends traffic to the app and analyzes responses for security flaws.

Use owasp-zap to perform a scan:

docker run -v $(pwd)/zap-reports:/zap/reports \
  -e ZAP_URL=http://app:3000 \
  -e ZAP_SCAN_MODE=Quick \
  -e ZAP_REPORT_FORMAT=html \
  owasp/zap2docker-stable \
  zap-baseline.py -t http://app:3000 -r /zap/reports/baseline.html -z /zap/reports/zap-report.xml

The zap-baseline.py script starts the ZAP scanner, runs a crawl, and executes a baseline scan. It outputs an HTML report and XML for integration.

A common failure mode is incorrect target URL. If app:3000 resolves to 10.10.10.10:3000 but the application listens on 0.0.0.0:3000, the scanner misses the app. The error HTTP 503: Service Unavailable from ZAP often goes unnoticed because the log is not collected.

Dependency Scanning

Dependency scanning identifies vulnerabilities in third-party libraries. Use snyk or grype to scan container images and package manifests.

For a package.json, run:

npm install snyk
npx snyk test --file=package.json --json --severity=high

The output is a JSON report:

[
  {
    "id": "CVE-2021-3445",
    "title": "Insecure deserialization in lodash",
    "severity": "high",
    "package": "lodash",
    "version": "4.17.21",
    "url": "https://snyk.io/vuln/SNYK-JS-LODASH-123456"
  }
]

When the pipeline fails on a high-severity vulnerability, snyk can fail the build:

npx snyk test --file=package.json --fail-on=high --json --exit-code=1

The --exit-code=1 is crucial: it ensures that snyk test returns a non-zero exit code when any high-severity issue is found. Without it, the CI job succeeds even when a critical vulnerability is present.

Container Image Security

Container images must be scanned for vulnerabilities, configuration issues, and supply chain integrity.

Use grype to scan a built image:

docker build -t acme/app:v1.2.0 .
grype acme/app:v1.2.0 -o json > grype-report.json

The grype report includes:

{
  "image": "acme/app:v1.2.0",
  "vulnerabilities": [
    {
      "id": "CVE-2022-1234",
      "title": "Buffer overflow in libcrypto",
      "severity": "critical",
      "package": "libcrypto",
      "version": "1.1.1k",
      "fixedInVersion": "1.1.1l"
    }
  ],
  "sbom": {
    "spdx": {
      "version": "2.2",
      "files": [
        {
          "fileName": "package-lock.json",
          "checksum": "sha256:abc123...",
          "license": "MIT"
        }
      ]
    }
  }
}

The most common oversight is not checking the SBOM (Software Bill of Materials). Without it, teams cannot prove what’s in the image. When a CVE is found in package-lock.json, but no one checks the file, the root cause is missed.

Image Signing and Verification

To ensure images are trusted, sign them with cosign.

cosign sign --key=cosign.key acme/app:v1.2.0

This produces a signature in the image’s cosign annotation:

{
  "signatures": [
    {
      "keyid": "sha256:123456...",
      "signature": "base64-encoded-signature",
      "payload": "eyJ...eyJ...",
      "created": "2023-04-10T12:00:00Z"
    }
  ]
}

At runtime, verify the image:

cosign verify acme/app:v1.2.0

This checks the signature, the certificate chain, and the image digest. A silent failure occurs when cosign verify is not run in the deployment pipeline: a signed image is deployed, but the signature is not validated at runtime. The error cosign: failed to verify: no signature found appears in logs but is not monitored.

Runtime Security Integration

Security must extend beyond the build pipeline into the running system.

Pod Security Policies (PSP) in Kubernetes

Define a PodSecurityPolicy to enforce security at the cluster level:

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
spec:
  privileged: false
  allowHostNetwork: false
  fsGroup:
    rule: "MustRunAs"
    ranges:
      - min: 1000
        max: 2000
  runAsUser:
    rule: "MustRunAsNonRoot"
  seLinux:
    rule: "MustRunAs"
  supplementalGroups:
    rule: "MustRunAs"
  volumes:
    - "configMap"
    - "secret"
    - "persistentVolumeClaim"
  hostPorts:
    - min: 80
      max: 80
  allowedHostPaths:
    - pathPrefix: "/etc"
    - pathPrefix: "/var/log"

Apply it with kubectl:

kubectl apply -f psp-restricted.yaml

A common failure is missing hostPorts for a service that relies on a sidecar. When the sidecar tries to bind to port 80, but the PSP restricts hostPorts to 80, the pod fails to start. The error is Pod has unbound immediate PersistentVolumes, but the actual cause is hostPort 80 not allowed.

Secrets Management and Rotation

Use secrets-store-csi-driver to mount secrets from external systems into pods.

Define a SecretProviderClass:

apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
  name: vault
spec:
  provider: vault
  parameters:
    server: https://vault.acme.internal
    path: kv/data/app
    role: app-service
    auth:
      jwt:
        serviceAccountRef:
          name: app-sa
          namespace: app
    cache:
      defaultCacheDuration: "1m"

Mount secrets in a pod:

apiVersion: v1
kind: Pod
metadata:
  name: app-pod
spec:
  containers:
    - name: app
      image: acme/app:v1.2.0
      volumeMounts:
        - name: app-secrets
          mountPath: /etc/secrets
  volumes:
    - name: app-secrets
      csi:
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: vault

A subtle failure occurs when cache.defaultCacheDuration is not set. Secrets are fetched on pod start, but updates to Vault are not pulled. The pod uses old secrets for 10 minutes—until the cache expires—before updating. When a new database password is deployed, the app continues using the old one for up to 10 minutes.

Monitoring and Feedback Loops

Security is not complete until it is observable.

Use Prometheus and Grafana to monitor security metrics.

Define a Prometheus job:

- job_name: 'security'
  static_configs:
    - targets: ['security-metrics.acme.internal:9090']
  metrics_path: '/metrics'
  scheme: 'http'
  relabel_configs:
    - source_labels: [__address__]
      target_label: instance

Expose metrics from the application:

import "github.com/prometheus/client_golang/prometheus"

var (
  vulnerabilitiesHighCount = prometheus.NewCounterVec(
    prometheus.CounterOpts{
      Name: "security_vulnerabilities_high_count",
      Help: "Number of high-severity vulnerabilities found",
    },
    []string{"branch", "commit"},
  )
)

func main() {
  prometheus.MustRegister(vulnerabilitiesHighCount)
  // ...
  vulnerabilitiesHighCount.WithLabelValues("main", "abc123").Inc()
}

The most common mistake is not alerting on the metric. A Prometheus alert rule:

groups:
  - name: security
    rules:
      - alert: HighSeverityVulnerabilities
        expr: security_vulnerabilities_high_count{branch="main"} > 5
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "More than 5 high-severity vulnerabilities in main branch"
          description: "Found {{ $value }} high-severity vulnerabilities in the last 10 minutes."

This rule triggers when the number of high-severity vulnerabilities exceeds five. But if the main branch is not updated daily, the counter resets every run—leading to false positives. The fix is to use increase():

expr: increase(security_vulnerabilities_high_count{branch="main"}[1h]) > 5

This captures the number of new vulnerabilities in the last hour, not the total count.

Summary of Key Patterns

These patterns collectively ensure that security is not just a checklist but a continuous, integrated practice across the DevOps lifecycle.

This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.