Best Practice: Vulnerability & Patch Management
Context
Known vulnerabilities (CVEs) are the most easily exploitable attack vector – because the attack methods are publicly documented and automated exploits exist. At the same time, patch management is often the most neglected security area, because it delivers no new features and creates effort.
The consequence: Log4Shell (CVE-2021-44228) was in widespread exploitation 12 days after public disclosure. Organizations without structured patch management could not even identify affected systems – let alone patch them.
Typical oversights:
-
Container images are based on Ubuntu 18.04 LTS – end-of-life for years
-
ECR image scanning disabled – known CVEs in production undetected
-
No SBOM process – when new CVEs emerge, it is unclear which systems are affected
-
Dependabot PRs are ignored – 200 open security updates
-
No SLA for patches – prioritization is done by gut feeling, not severity
Related Controls
-
WAF-SEC-070 – Vulnerability & Patch Management
-
WAF-SEC-110 – Supply Chain Security & SBOM
Container Security
ECR Image Scanning
Amazon ECR offers two scanning modes:
| Mode | Technology | Recommendation |
|---|---|---|
Basic Scanning |
Clair; open-source CVE database |
Minimum requirement; only for low-risk workloads |
Enhanced Scanning |
Amazon Inspector; AWS Security Hub integration; continuous scanning even after push |
Required for production workloads; CVE findings visible in Security Hub |
resource "aws_ecr_repository" "payment_service" {
name = "payment-service"
image_tag_mutability = "IMMUTABLE" # Tags cannot be overwritten
image_scanning_configuration {
scan_on_push = true # Automatic scanning on every push
}
encryption_configuration {
encryption_type = "KMS"
kms_key = aws_kms_key.ecr_cmk.arn
}
tags = {
owner = "payment-team"
environment = "production"
workload = "payment-service"
}
}
# Lifecycle policy: automatically delete old images
resource "aws_ecr_lifecycle_policy" "payment_service" {
repository = aws_ecr_repository.payment_service.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep only the last 10 images per tag"
selection = {
tagStatus = "tagged"
countType = "imageCountMoreThan"
countNumber = 10
}
action = { type = "expire" }
},
{
rulePriority = 2
description = "Delete untagged images after 7 days"
selection = {
tagStatus = "untagged"
tagPrefixList = []
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 7
}
action = { type = "expire" }
}
]
})
}
Trivy and Grype in the CI Pipeline
Local scanning before pushing prevents CVE-affected images from ever reaching the registry:
# GitHub Actions workflow – container security scanning
name: Container Build & Scan
on:
push:
paths: ['Dockerfile', 'src/**']
jobs:
build-and-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker Image
run: docker build -t payment-service:${{ github.sha }} .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: payment-service:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1 # Pipeline fails on CRITICAL/HIGH
- name: Upload Trivy Results to Security Hub
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
- name: Scan with Grype (second opinion)
run: |
grype payment-service:${{ github.sha }} \
--fail-on critical \
--output json \
> grype-results.json
Base Image Strategy
-
Use distroless images – no shell, no package manager, minimal attack surface
-
Pin specific tags – never
FROM ubuntu:latest; alwaysFROM ubuntu:22.04@sha256:abc123 -
Automate base image updates – configure Renovate or Dependabot for Dockerfiles
# Good example: distroless, specific digest
FROM gcr.io/distroless/java21-debian12:nonroot@sha256:a1b2c3d4e5f6...
# Bad example: mutable tag, many packages
# FROM openjdk:latest
AMI Patching Strategy
Bake vs. In-Place
| Strategy | Approach | Recommendation |
|---|---|---|
Bake (Immutable) |
Create new AMI version with patches; update launch template; rolling update of the auto scaling group; terminate old instances |
Recommended for all production workloads; complete reproducibility; no configuration drift |
In-Place (SSM Patch Manager) |
Patch existing instances via SSM Run Command; configure patch groups; define maintenance windows |
Acceptable for workloads that do not support immutability; higher drift risk |
EC2 Image Builder for Automated AMI Creation
resource "aws_imagebuilder_image_pipeline" "payment_base" {
name = "payment-service-base-ami"
image_recipe_arn = aws_imagebuilder_image_recipe.payment_base.arn
infrastructure_configuration_arn = aws_imagebuilder_infrastructure_configuration.main.arn
schedule {
schedule_expression = "cron(0 2 ? * SUN *)" # Every Sunday at 02:00
pipeline_execution_start_condition = "EXPRESSION_MATCH_ONLY"
}
image_tests_configuration {
image_tests_enabled = true
timeout_minutes = 60
}
}
Dependency Scanning and SBOM
What Is an SBOM?
A Software Bill of Materials (SBOM) is a complete list of all software components and their versions in an artifact. When a new CVE is reported, an SBOM allows an immediate answer to the question: "Are we affected?"
Without SBOM, answering this question takes days – with SBOM, minutes.
SBOM Generation in the CI Pipeline
- name: Generate SBOM with Syft
run: |
syft payment-service:${{ github.sha }} \
-o spdx-json=sbom.spdx.json \
-o cyclonedx-json=sbom.cyclonedx.json
- name: Store SBOM as Artifact
uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: |
sbom.spdx.json
sbom.cyclonedx.json
retention-days: 365 # Compliance: retain SBOM for 1 year
- name: Attestation with Cosign (SLSA L2)
run: |
cosign attest \
--predicate sbom.spdx.json \
--type spdxjson \
payment-service:${{ github.sha }}
Dependabot and Renovate Configuration
# .github/dependabot.yml
version: 2
updates:
# Python dependencies
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
security-patches:
patterns: ["*"]
update-types: ["patch"]
# Docker base images
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
CVE Prioritization
The Prioritization Model
A CVSS score alone is not a sufficient prioritization criterion. The combination of score, exploitability, and context provides the correct picture:
| CVE Class | CVSS Score | Patch SLA | Prioritization Criteria |
|---|---|---|---|
Critical |
9.0–10.0 |
< 24 hours |
CVSS >= 9.0 OR actively exploited (CISA KEV) OR RCE on production system |
High |
7.0–8.9 |
< 7 days |
CVSS 7.0–8.9 OR reachable service exposed OR known PoC exploits |
Medium |
4.0–6.9 |
< 30 days |
CVSS 4.0–6.9; not directly reachable OR requires local access |
Low |
0.1–3.9 |
< 90 days |
Low exploitation probability; no known exploit; requires combination |
Informational |
n/a |
Best effort |
Configuration recommendations; no directly exploitable bug |
CISA Known Exploited Vulnerabilities (KEV)
The CISA KEV list contains CVEs that are actively used in attacks. CVEs on this list should be treated as critical regardless of their CVSS score:
# Check whether current CVEs are on the CISA KEV list
curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json \
| jq '.vulnerabilities[] | select(.cveID == "CVE-2021-44228")'
Monitoring and Measurement
-
Amazon Inspector: Continuous scanning of EC2, ECR, and Lambda
-
AWS Security Hub: Aggregates Inspector, GuardDuty, Macie findings
-
GitHub Security Advisories: Dependabot alerts for dependencies
-
Patch Compliance Dashboard: SSM Patch Manager compliance reports
-
Mean Time to Patch (MTTP): Metric per severity level – target: meet SLA
-
wafpass:
wafpass check --pillar security --controls WAF-SEC-070,WAF-SEC-110