Security Scanning - Street-Level Ops¶
Real-world workflows for scanning images, dependencies, and infrastructure for vulnerabilities.
Quick Image Scan¶
# Scan a local image for HIGH and CRITICAL vulnerabilities
trivy image --severity HIGH,CRITICAL myapp:latest
# Output:
# myapp:latest (debian 12.4)
# Total: 8 (HIGH: 6, CRITICAL: 2)
#
# ┌──────────────┬────────────────┬──────────┬────────────────┬───────────────┐
# │ Library │ Vulnerability │ Severity │ Installed Ver │ Fixed Ver │
# ├──────────────┼────────────────┼──────────┼────────────────┼───────────────┤
# │ libssl3 │ CVE-2024-0727 │ CRITICAL │ 3.0.11-1 │ 3.0.13-1 │
# │ curl │ CVE-2023-46218 │ HIGH │ 7.88.1-10 │ 7.88.1-10+deb │
# └──────────────┴────────────────┴──────────┴────────────────┴───────────────┘
# Scan and fail CI if CRITICAL found (exit code 1)
trivy image --exit-code 1 --severity CRITICAL myapp:latest
# Add --ignore-unfixed to skip CVEs with no available patch
# JSON output for automation
trivy image --format json --output scan-results.json myapp:latest
One-liner:
--ignore-unfixedis your best friend in CI pipelines. Without it, Trivy fails on CVEs that have no available patch, blocking deploys for issues you cannot fix. Use it alongside--severity CRITICALto keep the gate meaningful without causing false blocks.
Scanning All Running Images in a Cluster¶
# Extract unique images from all pods
kubectl get pods -A -o jsonpath='{range .items[*]}{.spec.containers[*].image}{"\n"}{end}' | sort -u
# Scan each running image
kubectl get pods -A -o jsonpath='{range .items[*]}{.spec.containers[*].image}{"\n"}{end}' | \
sort -u | while read -r img; do
echo "=== ${img} ==="
trivy image --severity CRITICAL --quiet "${img}" 2>/dev/null
done
# Count criticals per image
kubectl get pods -A -o jsonpath='{range .items[*]}{.spec.containers[*].image}{"\n"}{end}' | \
sort -u | while read -r img; do
count=$(trivy image --severity CRITICAL --quiet --format json "${img}" 2>/dev/null | \
jq '[.Results[]?.Vulnerabilities // [] | length] | add // 0')
echo "${count} CRITICAL: ${img}"
done | sort -rn
Default trap: Trivy scans the OS package database inside the image. If your app bundles dependencies via vendoring, static linking, or a language-specific package manager (pip, npm, go modules), those are only caught with
--scanners vuln(the default) plus a lock file present. Missingrequirements.txtorgo.suminside the image means language-level vulnerabilities go undetected.
Dockerfile and IaC Scanning¶
# Scan Dockerfile for misconfigurations
trivy config Dockerfile
# Output:
# Dockerfile (dockerfile)
# Tests: 23 (SUCCESSES: 20, FAILURES: 3)
# Failures: 3 (LOW: 1, MEDIUM: 1, HIGH: 1)
#
# HIGH: Specify a tag in the 'FROM' statement
# MEDIUM: Add HEALTHCHECK instruction
# LOW: Use COPY instead of ADD
# Scan Terraform files
trivy config --severity HIGH,CRITICAL ./terraform/
# Scan Kubernetes manifests
trivy config ./k8s/manifests/
# Scan for secrets in the filesystem
trivy fs --scanners secret .
Remember: Trivy scanning modes mnemonic: V-M-S — Vulnerabilities (CVEs in packages), Misconfigurations (IaC and Dockerfile issues), Secrets (leaked credentials). Pass
--scanners vuln,misconfig,secretto run all three in one shot.
SBOM Generation and Scanning¶
# Generate SBOM for an image (CycloneDX format)
trivy image --format cyclonedx --output sbom.json myapp:v1.2.3
# Scan an existing SBOM when a new CVE drops (no image pull needed)
trivy sbom sbom.json --severity CRITICAL
# Generate SBOM with syft (alternative)
syft myapp:v1.2.3 -o spdx-json > sbom-spdx.json
# Quick check: which of my images contain libssl3?
for sbom in sboms/*.json; do
if jq -e '.components[] | select(.name == "libssl3")' "${sbom}" >/dev/null 2>&1; then
echo "AFFECTED: ${sbom}"
fi
done
Secret Detection in Code¶
# Scan git history with gitleaks
gitleaks detect --source . --verbose
# Output:
# Finding: AWS Access Key
# Secret: AKIA...
# File: deploy/config.env
# Line: 12
# Commit: a3b4c5d
# Scan staged files only (pre-commit hook)
gitleaks protect --staged
# Scan with trufflehog (deeper git history analysis)
trufflehog git file://. --only-verified
# Trivy secret scanning
trivy fs --scanners secret --severity HIGH,CRITICAL .
War story: A team ran
gitleaks detectand found zero results. They assumed the repo was clean. Later, a leaked key was exploited. The problem:.gitleaksignorehad a wildcard entry added by a frustrated developer six months earlier that suppressed all findings. Always audit your ignore files —cat .gitleaksignore— before trusting a clean scan result.
Image Signing and Verification¶
# Generate a cosign key pair
cosign generate-key-pair
# Sign an image after build
cosign sign --key cosign.key myregistry.io/myapp:v1.2.3
# Verify signature before deployment
cosign verify --key cosign.pub myregistry.io/myapp:v1.2.3
> **Remember:** Image signing workflow mnemonic: **G-S-V** — Generate key pair (once), Sign after every build (CI), Verify before every deploy (admission controller). Use Kyverno or OPA Gatekeeper as a Kubernetes admission controller to enforce signature verification automatically — no unsigned image runs in production.
# Output:
# Verification for myregistry.io/myapp:v1.2.3 --
# The following checks were performed on each of these signatures:
# - The cosign claims were validated
# - The signatures were verified against the specified public key
CI Pipeline Gate¶
# Full CI scan script
#!/usr/bin/env bash
set -euo pipefail
IMAGE="${1:?Usage: scan.sh IMAGE:TAG}"
echo "--- Scanning ${IMAGE} ---"
# 1. Vulnerability scan (fail on CRITICAL)
trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed "${IMAGE}"
# 2. Misconfiguration scan
trivy image --exit-code 1 --scanners misconfig "${IMAGE}"
# 3. Secret scan
trivy image --exit-code 1 --scanners secret "${IMAGE}"
# 4. Generate SBOM for audit trail
trivy image --format cyclonedx --output "sbom-$(date +%Y%m%d).json" "${IMAGE}"
echo "--- All scans passed ---"
Trivy Ignore File for Accepted Risks¶
# .trivyignore — document why each CVE is accepted
cat .trivyignore
# CVE-2023-44487 # HTTP/2 rapid reset — mitigated at LB level
# CVE-2023-39325 # Go stdlib — function not reachable in our code
# CVE-2024-1234 # Fix not available yet, tracked in JIRA-5678
# Scan with ignore file
trivy image --ignorefile .trivyignore --severity HIGH,CRITICAL myapp:latest
Gotcha: Suppressing a CVE in
.trivyignoresilences it forever — even after the fix becomes available. Set a calendar reminder to review your ignore list quarterly. Better yet, add an expiration comment and grep for stale entries:# expires: 2026-06-01.
Comparing Base Image Vulnerability Counts¶
# Quick comparison of base image options
for img in ubuntu:22.04 python:3.11-slim alpine:3.19 gcr.io/distroless/python3-debian12; do
count=$(trivy image --severity HIGH,CRITICAL --quiet --format json "${img}" 2>/dev/null | \
jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length')
echo "${count} HIGH+CRITICAL: ${img}"
done
# Output:
# 5 HIGH+CRITICAL: ubuntu:22.04
# 1 HIGH+CRITICAL: python:3.11-slim
# 0 HIGH+CRITICAL: alpine:3.19
# 0 HIGH+CRITICAL: gcr.io/distroless/python3-debian12
Under the hood: Distroless and Alpine images have fewer vulnerabilities because they contain fewer packages. Alpine uses musl libc (~1 MB) instead of glibc (~8 MB), eliminating a major CVE source. The tradeoff: musl has subtle behavior differences (DNS resolution, threading, locale handling) that can cause application bugs. Test thoroughly before switching production images to Alpine.