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.
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.
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.
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 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 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.
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.
Security must extend beyond the build pipeline into the running system.
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.
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.
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.
sonar-scanner with precise sonar-project.properties; ensure exclusions cover all code paths.owasp-zap with zap-baseline.py; verify target URL and capture full logs.snyk test --fail-on=high --exit-code=1 to enforce security gates.grype with --output=json; validate SBOMs and include them in reports.cosign sign and cosign verify with explicit --key and --certificate options; monitor for signature verification failures.PodSecurityPolicy with strict hostPorts and allowedHostPaths; validate with kubectl describe pod.SecretProviderClass with cache.defaultCacheDuration; monitor cache expiration.Prometheus with increase() for time-based metrics; alert on new vulnerabilities, not just totals.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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy