Kubernetes SBOM Guide for Clusters, Helm, and Policy
Quick Start
Kubernetes makes SBOM management harder because the software inventory is distributed across images, workloads, namespaces, Helm charts, and admission or policy controls. A single static SBOM file is rarely enough if workloads are rebuilt often or deployed from multiple registries.
The most reliable Kubernetes SBOM workflow is to generate inventory at build time, validate it before release, and attach it to the image or deployment record before the cluster enforces policy.
The practical sequence is:
- generate SBOMs at the image stage in CI
- validate them before release with the SBOM Validator
- carry those artifacts into registry, deployment, and policy workflows
- use cluster-side checks for enforcement, not as the only generation step
What to focus on first
- image-level SBOMs for every deployable container
- Docker pipeline integration before cluster-side complexity
- CI/CD handoff so the cluster consumes already-validated artifacts
- admission or policy controls only after artifact generation is reliable
Common Kubernetes SBOM pitfalls
- assuming the cluster can reconstruct everything that should have been captured during build
- ignoring sidecars, init containers, or Helm-managed dependencies
- mixing runtime inventory with release artifact inventory
- storing SBOMs without linking them back to image digests
Introduction
Kubernetes environments make SBOM management harder because the inventory is distributed across images, workloads, namespaces, Helm charts, admission policies, and supporting platform components. A static SBOM file is not enough if workloads are rebuilt often, scaled dynamically, or pulled from multiple registries.
This guide focuses on production-oriented Kubernetes SBOM workflows: generate image-level inventories, validate the output with the SBOM Validator, connect them to Docker image pipelines, and enforce policy through CI/CD and cluster controls.Why Kubernetes SBOMs are Critical
Kubernetes environments typically contain:
- Hundreds to thousands of containers across multiple namespaces
- Dynamic workloads that scale and update frequently
- Third-party Helm charts and operators with unknown dependencies
- Multiple runtime environments (development, staging, production)
- Service mesh components adding additional layers
Effective SBOM management in Kubernetes enables:
- Real-time vulnerability tracking across all workloads
- Compliance with regulations for containerized applications
- Rapid incident response for supply chain attacks
- license compliance across distributed systems
- Resource optimization through dependency analysis
Understanding Kubernetes SBOMs
Kubernetes SBOM Scope Levels
1. Cluster Level- Kubernetes system components (API server, etcd, kubelet)
- Core networking components (CNI plugins, CoreDNS)
- Ingress controllers and load balancers
- Storage drivers and CSI plugins
- All workloads within a namespace
- ConfigMaps and Secrets containing software
- Persistent Volume contents
- Service mesh sidecars
- Individual pod containers
- Init containers and ephemeral containers
- Mounted volumes with software
- Runtime dependencies
- Base image components
- Application dependencies
- Build-time vs runtime dependencies
Kubernetes-Specific SBOM Challenges
Dynamic Nature of Workloads# Deployments can scale and update frequently
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
spec:
replicas: 3 # Can change based on HPA
strategy:
type: RollingUpdate # Gradual updates mean mixed versions# Pods often contain multiple containers with different SBOMs
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: myapp:v1.2.3
- name: sidecar
image: envoy:v1.24.0
- name: log-agent
image: fluentd:v1.15# Software can be injected via ConfigMaps
apiVersion: v1
kind: ConfigMap
metadata:
name: scripts
data:
startup.sh: |
#!/bin/bash
# Script containing dependencies
curl -L https://example.com/tool | bashCluster-Wide SBOM Generation
Comprehensive Cluster Inventory
Kubernetes Cluster SBOM Scanner#!/bin/bash
# k8s-cluster-sbom-scanner.sh
set -euo pipefail
CLUSTER_NAME="${1:-production}"
OUTPUT_DIR="${2:-./k8s-sbom-analysis}"
NAMESPACE="${3:-all}"
echo "=== Kubernetes Cluster SBOM Generation ==="
echo "Cluster: $CLUSTER_NAME"
echo "Output: $OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"/{cluster,namespaces,workloads,images,reports}
# 1. Cluster component analysis
echo "1. Analyzing cluster components..."
# Get Kubernetes version and components
kubectl version -o json > "$OUTPUT_DIR/cluster/k8s-version.json"
# System pods (kube-system namespace)
kubectl get pods -n kube-system -o json > "$OUTPUT_DIR/cluster/system-pods.json"
# Extract unique images from system namespace
kubectl get pods -n kube-system -o jsonpath="{.items[*].spec.containers[*].image}" |
tr ' ' '\n' | sort -u > "$OUTPUT_DIR/cluster/system-images.txt"
# 2. Namespace inventory
echo "2. Scanning namespaces..."
if [ "$NAMESPACE" = "all" ]; then
namespaces=$(kubectl get namespaces -o jsonpath="{.items[*].metadata.name}")
else
namespaces="$NAMESPACE"
fi
for ns in $namespaces; do
echo " Scanning namespace: $ns"
mkdir -p "$OUTPUT_DIR/namespaces/$ns"
# Get all workloads in namespace
kubectl get deployments,statefulsets,daemonsets,jobs,cronjobs -n "$ns" -o json > \
"$OUTPUT_DIR/namespaces/$ns/workloads.json"
# Extract all container images
kubectl get pods -n "$ns" -o jsonpath="{.items[*].spec.containers[*].image}" |
tr ' ' '\n' | sort -u > "$OUTPUT_DIR/namespaces/$ns/images.txt"
# Get ConfigMaps and Secrets (for potential software)
kubectl get configmaps -n "$ns" -o json > "$OUTPUT_DIR/namespaces/$ns/configmaps.json"
kubectl get secrets -n "$ns" -o json > "$OUTPUT_DIR/namespaces/$ns/secrets.json" 2>/dev/null || true
done
# 3. Generate SBOM for each unique image
echo "3. Generating SBOMs for container images..."
# Collect all unique images
find "$OUTPUT_DIR" -name "*.txt" -exec cat {} \; |
grep -E '.*:.*' | sort -u > "$OUTPUT_DIR/all-images.txt"
total_images=$(wc -l < "$OUTPUT_DIR/all-images.txt")
echo " Found $total_images unique images"
# Generate SBOM for each image
while IFS= read -r image; do
echo " Generating SBOM for: $image"
# Sanitize image name for filename
safe_name=$(echo "$image" | sed 's/[:\/]/_/g')
# Generate SBOM using [Syft](/tools/syft)
syft "$image" -o spdx-json="$OUTPUT_DIR/images/${safe_name}.spdx.json" \
-o [CycloneDX](/tools/cyclonedx-cli)-json="$OUTPUT_DIR/images/${safe_name}.cyclonedx.json" 2>/dev/null || \
echo " Failed to generate SBOM for $image" >> "$OUTPUT_DIR/failed-images.txt"
done < "$OUTPUT_DIR/all-images.txt"
# 4. Analyze Helm releases
echo "4. Analyzing Helm releases..."
if command -v helm &> /dev/null; then
helm list -A -o json > "$OUTPUT_DIR/cluster/helm-releases.json"
# Extract chart information
helm list -A -o json | jq -r '.[] | "\(.namespace) \(.name) \(.chart)"' |
while read ns release chart; do
echo " Analyzing Helm release: $release in namespace $ns"
mkdir -p "$OUTPUT_DIR/namespaces/$ns/helm"
# Get release values (may contain image references)
helm get values "$release" -n "$ns" -o json > \
"$OUTPUT_DIR/namespaces/$ns/helm/${release}-values.json" 2>/dev/null || true
# Get release manifest
helm get manifest "$release" -n "$ns" > \
"$OUTPUT_DIR/namespaces/$ns/helm/${release}-manifest.yaml" 2>/dev/null || true
done
fi
# 5. Generate cluster summary report
echo "5. Generating cluster summary report..."
python3 << 'PYTHON_SCRIPT' > "$OUTPUT_DIR/reports/cluster-summary.json"
import json
import os
import sys
from pathlib import Path
output_dir = Path(os.environ.get('OUTPUT_DIR', './k8s-sbom-analysis'))
summary = {
'cluster_name': os.environ.get('CLUSTER_NAME', 'unknown'),
'scan_timestamp': os.popen('date -u +%Y-%m-%dT%H:%M:%SZ').read().strip(),
'kubernetes_version': {},
'namespaces': {},
'images': {
'total': 0,
'with_sbom': 0,
'failed': 0
},
'workloads': {
'deployments': 0,
'statefulsets': 0,
'daemonsets': 0,
'jobs': 0,
'cronjobs': 0
},
'helm_releases': 0
}
# Parse Kubernetes version
try:
with open(output_dir / 'cluster' / 'k8s-version.json') as f:
k8s_version = json.load(f)
summary['kubernetes_version'] = {
'server': k8s_version.get('serverVersion', {}).get('gitVersion', 'unknown'),
'client': k8s_version.get('clientVersion', {}).get('gitVersion', 'unknown')
}
except:
pass
# Count namespaces and workloads
namespaces_dir = output_dir / 'namespaces'
if namespaces_dir.exists():
for ns_dir in namespaces_dir.iterdir():
if ns_dir.is_dir():
ns_name = ns_dir.name
summary['namespaces'][ns_name] = {
'images': 0,
'workloads': 0
}
# Count images
images_file = ns_dir / 'images.txt'
if images_file.exists():
with open(images_file) as f:
summary['namespaces'][ns_name]['images'] = len(f.readlines())
# Count workloads
workloads_file = ns_dir / 'workloads.json'
if workloads_file.exists():
try:
with open(workloads_file) as f:
workloads = json.load(f)
for item in workloads.get('items', []):
kind = item.get('kind', '').lower()
if 'deployment' in kind:
summary['workloads']['deployments'] += 1
elif 'statefulset' in kind:
summary['workloads']['statefulsets'] += 1
elif 'daemonset' in kind:
summary['workloads']['daemonsets'] += 1
elif 'job' in kind and 'cron' not in kind:
summary['workloads']['jobs'] += 1
elif 'cronjob' in kind:
summary['workloads']['cronjobs'] += 1
summary['namespaces'][ns_name]['workloads'] += 1
except:
pass
# Count images with SBOMs
images_dir = output_dir / 'images'
if images_dir.exists():
sbom_files = list(images_dir.glob('*.json'))
summary['images']['with_sbom'] = len(sbom_files) // 2 # Divided by 2 as we generate 2 formats
# Count total images
all_images_file = output_dir / 'all-images.txt'
if all_images_file.exists():
with open(all_images_file) as f:
summary['images']['total'] = len(f.readlines())
# Count failed images
failed_images_file = output_dir / 'failed-images.txt'
if failed_images_file.exists():
with open(failed_images_file) as f:
summary['images']['failed'] = len(f.readlines())
# Count Helm releases
helm_file = output_dir / 'cluster' / 'helm-releases.json'
if helm_file.exists():
try:
with open(helm_file) as f:
helm_releases = json.load(f)
summary['helm_releases'] = len(helm_releases)
except:
pass
print(json.dumps(summary, indent=2))
PYTHON_SCRIPT
# 6. Generate human-readable report
echo "6. Creating final report..."
cat > "$OUTPUT_DIR/reports/cluster-sbom-report.md" << EOF
# Kubernetes Cluster SBOM Report
**Cluster:** $CLUSTER_NAME
**Generated:** $(date)
**Output Directory:** $OUTPUT_DIR
## Executive Summary
$(cat "$OUTPUT_DIR/reports/cluster-summary.json" | jq -r '
"### Cluster Information\n" +
"- Kubernetes Version: " + .kubernetes_version.server + "\n" +
"- Client Version: " + .kubernetes_version.client + "\n" +
"- Total Namespaces: " + (.namespaces | length | tostring) + "\n" +
"- Total Images: " + .images.total + "\n" +
"- Images with SBOM: " + .images.with_sbom + "\n" +
"- Failed SBOM Generation: " + .images.failed + "\n" +
"\n### Workload Distribution\n" +
"- Deployments: " + .workloads.deployments + "\n" +
"- StatefulSets: " + .workloads.statefulsets + "\n" +
"- DaemonSets: " + .workloads.daemonsets + "\n" +
"- Jobs: " + .workloads.jobs + "\n" +
"- CronJobs: " + .workloads.cronjobs + "\n" +
"- Helm Releases: " + .helm_releases
')
## Namespace Analysis
$(cat "$OUTPUT_DIR/reports/cluster-summary.json" | jq -r '
.namespaces | to_entries[] |
"### " + .key + "\n" +
"- Images: " + (.value.images | tostring) + "\n" +
"- Workloads: " + (.value.workloads | tostring) + "\n"
')
## Recommendations
1. Review failed SBOM generations and investigate inaccessible images
2. Implement continuous SBOM monitoring for new deployments
3. Set up vulnerability scanning based on generated SBOMs
4. Establish SBOM storage and versioning strategy
5. Create alerts for images without SBOMs
## Files Generated
- Cluster information: \`cluster/\`
- Namespace details: \`namespaces/\`
- Image SBOMs: \`images/\`
- Summary reports: \`reports/\`
EOF
echo ""
echo "=== Cluster SBOM Generation Complete ==="
echo "Summary: $OUTPUT_DIR/reports/cluster-sbom-report.md"
echo "JSON Report: $OUTPUT_DIR/reports/cluster-summary.json"
echo ""
echo "Statistics:"
cat "$OUTPUT_DIR/reports/cluster-summary.json" | jq -r '
"Total Images: " + (.images.total | tostring) + "\n" +
"SBOMs Generated: " + (.images.with_sbom | tostring) + "\n" +
"Failed: " + (.images.failed | tostring)
'Kubernetes Node SBOM Analysis
Node-Level SBOM Generation#!/bin/bash
# k8s-node-sbom.sh - Generate SBOMs for Kubernetes nodes
NODE_NAME="${1:-}"
if [ -z "$NODE_NAME" ]; then
echo "Usage: $0 <node-name>"
echo "Available nodes:"
kubectl get nodes -o name | sed 's/node\///'
exit 1
fi
echo "=== Node SBOM Generation for $NODE_NAME ==="
# Get node information
kubectl get node "$NODE_NAME" -o json > "node-$NODE_NAME.json"
# Get pods running on this node
kubectl get pods --all-namespaces --field-selector spec.nodeName="$NODE_NAME" -o json > "node-$NODE_NAME-pods.json"
# Extract images from pods on this node
kubectl get pods --all-namespaces --field-selector spec.nodeName="$NODE_NAME" \
-o jsonpath="{.items[*].spec.containers[*].image}" | \
tr ' ' '\n' | sort -u > "node-$NODE_NAME-images.txt"
echo "Node $NODE_NAME is running $(wc -l < node-$NODE_NAME-images.txt) unique images"
# Generate SBOMs for node images
while IFS= read -r image; do
echo "Generating SBOM for: $image"
safe_name=$(echo "$image" | sed 's/[:\/]/_/g')
syft "$image" -o spdx-json="node-$NODE_NAME-sbom-$safe_name.json" 2>/dev/null || echo "Failed: $image"
done < "node-$NODE_NAME-images.txt"Pod and Container SBOM Tracking
Real-Time Pod SBOM Tracking
Kubernetes Pod SBOM Controller#!/usr/bin/env python3
"""
Kubernetes Pod SBOM Controller
Tracks and manages SBOMs for all pods in the cluster
"""
import json
import logging
import subprocess
import time
from datetime import datetime
from kubernetes import client, config, watch
from concurrent.futures import ThreadPoolExecutor
import hashlib
class PodSBOMController:
def __init__(self, namespace="default", sbom_storage="./pod-sboms"):
self.namespace = namespace
self.sbom_storage = sbom_storage
self.logger = self._setup_logging()
# Load Kubernetes config
try:
config.load_incluster_config() # In-cluster execution
except:
config.load_kube_config() # Local execution
self.v1 = client.CoreV1Api()
self.apps_v1 = client.AppsV1Api()
# Track processed pods
self.processed_pods = {}
# Thread pool for SBOM generation
self.executor = ThreadPoolExecutor(max_workers=5)
def _setup_logging(self):
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
return logging.getLogger(__name__)
def watch_pods(self):
"""Watch for pod events and generate SBOMs"""
self.logger.info(f"Starting pod watch for namespace: {self.namespace}")
w = watch.Watch()
# Set namespace to None to watch all namespaces
watch_namespace = None if self.namespace == "all" else self.namespace
for event in w.stream(self.v1.list_pod_for_all_namespaces if watch_namespace is None
else lambda: self.v1.list_namespaced_pod(self.namespace)):
event_type = event['type']
pod = event['object']
if event_type in ['ADDED', 'MODIFIED']:
self._handle_pod_event(pod)
def _handle_pod_event(self, pod):
"""Handle pod creation or modification"""
pod_name = pod.metadata.name
pod_namespace = pod.metadata.namespace
pod_uid = pod.metadata.uid
# Check if pod is ready
if pod.status.phase != 'Running':
return
# Generate unique identifier for pod state
pod_hash = self._generate_pod_hash(pod)
# Check if we've already processed this pod state
if self.processed_pods.get(pod_uid) == pod_hash:
return
self.logger.info(f"Processing pod: {pod_namespace}/{pod_name}")
# Submit SBOM generation task
self.executor.submit(self._generate_pod_sbom, pod)
# Mark as processed
self.processed_pods[pod_uid] = pod_hash
def _generate_pod_sbom(self, pod):
"""Generate SBOM for a pod"""
pod_name = pod.metadata.name
pod_namespace = pod.metadata.namespace
try:
# Create pod SBOM directory
import os
pod_dir = f"{self.sbom_storage}/{pod_namespace}/{pod_name}"
os.makedirs(pod_dir, exist_ok=True)
# Pod metadata
pod_metadata = {
'name': pod_name,
'namespace': pod_namespace,
'uid': pod.metadata.uid,
'labels': pod.metadata.labels or {},
'annotations': pod.metadata.annotations or {},
'created': pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
'node': pod.spec.node_name,
'service_account': pod.spec.service_account,
'containers': [],
'sbom_generation': {
'timestamp': datetime.utcnow().isoformat() + 'Z',
'tool': 'pod-sbom-controller'
}
}
# Process each container
for container in pod.spec.containers:
container_name = container.name
container_image = container.image
self.logger.info(f" Generating SBOM for container: {container_name} ({container_image})")
# Generate SBOM for container image
sbom_file = f"{pod_dir}/{container_name}-sbom.json"
try:
# Use Syft to generate SBOM
result = subprocess.run(
['syft', container_image, '-o', f'spdx-json={sbom_file}'],
capture_output=True,
text=True,
timeout=300
)
if result.returncode == 0:
# Parse SBOM for summary
with open(sbom_file, 'r') as f:
sbom_data = json.load(f)
container_info = {
'name': container_name,
'image': container_image,
'sbom_file': sbom_file,
'packages_count': len(sbom_data.get('packages', [])),
'sbom_format': 'spdx',
'generation_status': 'success'
}
# Add security context
if container.security_context:
container_info['security_context'] = {
'privileged': container.security_context.privileged,
'run_as_user': container.security_context.run_as_user,
'run_as_non_root': container.security_context.run_as_non_root,
'read_only_root_filesystem': container.security_context.read_only_root_filesystem
}
pod_metadata['containers'].append(container_info)
else:
self.logger.error(f" Failed to generate SBOM: {result.stderr}")
pod_metadata['containers'].append({
'name': container_name,
'image': container_image,
'generation_status': 'failed',
'error': result.stderr
})
except subprocess.TimeoutExpired:
self.logger.error(f" SBOM generation timeout for {container_image}")
pod_metadata['containers'].append({
'name': container_name,
'image': container_image,
'generation_status': 'timeout'
})
except Exception as e:
self.logger.error(f" Error generating SBOM: {e}")
pod_metadata['containers'].append({
'name': container_name,
'image': container_image,
'generation_status': 'error',
'error': str(e)
})
# Save pod metadata
metadata_file = f"{pod_dir}/pod-metadata.json"
with open(metadata_file, 'w') as f:
json.dump(pod_metadata, f, indent=2, default=str)
self.logger.info(f"✓ Pod SBOM complete: {pod_namespace}/{pod_name}")
# Send to central storage if configured
self._store_sbom(pod_metadata)
except Exception as e:
self.logger.error(f"Failed to process pod {pod_namespace}/{pod_name}: {e}")
def _generate_pod_hash(self, pod):
"""Generate hash of pod state to detect changes"""
# Hash based on container images and restart count
hash_input = ""
for container in pod.spec.containers:
hash_input += f"{container.name}:{container.image}"
if pod.status.container_statuses:
for status in pod.status.container_statuses:
hash_input += f"{status.restart_count}"
return hashlib.md5(hash_input.encode()).hexdigest()
def _store_sbom(self, pod_metadata):
"""Store SBOM in central location (e.g., S3, database)"""
# Example: Store in ConfigMap
try:
configmap_name = f"sbom-{pod_metadata['namespace']}-{pod_metadata['name']}"
configmap = client.V1ConfigMap(
metadata=client.V1ObjectMeta(
name=configmap_name,
namespace=pod_metadata['namespace'],
labels={
'app.kubernetes.io/managed-by': 'sbom-controller',
'sbom.io/pod-name': pod_metadata['name'],
'sbom.io/generated': datetime.utcnow().strftime('%Y%m%d%H%M%S')
}
),
data={
'metadata.json': json.dumps(pod_metadata, indent=2, default=str)
}
)
# Create or update ConfigMap
try:
self.v1.create_namespaced_config_map(
namespace=pod_metadata['namespace'],
body=configmap
)
self.logger.info(f" Stored SBOM in ConfigMap: {configmap_name}")
except client.exceptions.ApiException as e:
if e.status == 409: # Already exists
self.v1.patch_namespaced_config_map(
name=configmap_name,
namespace=pod_metadata['namespace'],
body=configmap
)
self.logger.info(f" Updated SBOM in ConfigMap: {configmap_name}")
else:
raise
except Exception as e:
self.logger.error(f"Failed to store SBOM: {e}")
def scan_existing_pods(self):
"""Scan all existing pods and generate SBOMs"""
self.logger.info("Scanning existing pods...")
if self.namespace == "all":
pods = self.v1.list_pod_for_all_namespaces()
else:
pods = self.v1.list_namespaced_pod(self.namespace)
for pod in pods.items:
if pod.status.phase == 'Running':
self._handle_pod_event(pod)
self.logger.info(f"Scanned {len(pods.items)} existing pods")
def run(self):
"""Main controller loop"""
self.logger.info("Starting Kubernetes Pod SBOM Controller")
# First scan existing pods
self.scan_existing_pods()
# Then watch for new events
while True:
try:
self.watch_pods()
except Exception as e:
self.logger.error(f"Watch error: {e}")
self.logger.info("Restarting watch in 10 seconds...")
time.sleep(10)
def main():
import argparse
parser = argparse.ArgumentParser(description="Kubernetes Pod SBOM Controller")
parser.add_argument("-n", "--namespace", default="default",
help="Namespace to watch (use 'all' for all namespaces)")
parser.add_argument("-s", "--storage", default="./pod-sboms",
help="Directory to store SBOMs")
args = parser.parse_args()
controller = PodSBOMController(
namespace=args.namespace,
sbom_storage=args.storage
)
try:
controller.run()
except KeyboardInterrupt:
print("\nShutting down controller...")
controller.executor.shutdown(wait=True)
if __name__ == "__main__":
main()Container Runtime SBOM Integration
Runtime SBOM Extraction#!/bin/bash
# runtime-sbom-extractor.sh
POD_NAME="$1"
NAMESPACE="${2:-default}"
CONTAINER="${3:-}"
if [ -z "$POD_NAME" ]; then
echo "Usage: $0 <pod-name> [namespace] [container]"
exit 1
fi
echo "=== Runtime SBOM Extraction ==="
echo "Pod: $POD_NAME"
echo "Namespace: $NAMESPACE"
# Get container names if not specified
if [ -z "$CONTAINER" ]; then
containers=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath="{.spec.containers[*].name}")
else
containers="$CONTAINER"
fi
for container in $containers; do
echo "Processing container: $container"
# Execute SBOM generation inside running container
kubectl exec "$POD_NAME" -n "$NAMESPACE" -c "$container" -- /bin/sh -c '
# Try to install Syft if not present
if ! command -v syft &> /dev/null; then
echo "Installing Syft in container..."
curl -sSfL https://get.anchore.io/syft | sh -s -- -b /tmp
export PATH="/tmp:$PATH"
fi
# Generate SBOM for container filesystem
syft dir:/ -o spdx-json
' > "runtime-sbom-${POD_NAME}-${container}.json" 2>/dev/null
if [ $? -eq 0 ]; then
echo " ✓ Runtime SBOM generated: runtime-sbom-${POD_NAME}-${container}.json"
else
echo " ✗ Failed to generate runtime SBOM for $container"
# Fallback: Extract package information manually
echo " Attempting manual package extraction..."
kubectl exec "$POD_NAME" -n "$NAMESPACE" -c "$container" -- /bin/sh -c '
# Try different package managers
if command -v dpkg &> /dev/null; then
echo "=== Debian packages ==="
dpkg -l
elif command -v rpm &> /dev/null; then
echo "=== RPM packages ==="
rpm -qa
elif command -v apk &> /dev/null; then
echo "=== Alpine packages ==="
apk list --installed
fi
# Language-specific packages
if command -v pip &> /dev/null; then
echo "=== Python packages ==="
pip list --format=json
fi
if command -v npm &> /dev/null; then
echo "=== Node packages ==="
npm list -g --json
fi
if [ -f /go.mod ]; then
echo "=== Go modules ==="
cat /go.mod
fi
' > "runtime-packages-${POD_NAME}-${container}.txt"
fi
doneHelm Chart SBOM Integration
Helm Chart SBOM Generation
Comprehensive Helm SBOM Generator#!/bin/bash
# helm-sbom-generator.sh
set -euo pipefail
RELEASE_NAME="${1:-}"
NAMESPACE="${2:-default}"
if [ -z "$RELEASE_NAME" ]; then
echo "Usage: $0 <release-name> [namespace]"
echo "Available releases:"
helm list -A
exit 1
fi
echo "=== Helm Release SBOM Generation ==="
echo "Release: $RELEASE_NAME"
echo "Namespace: $NAMESPACE"
OUTPUT_DIR="./helm-sbom-$RELEASE_NAME"
mkdir -p "$OUTPUT_DIR"/{manifests,images,sboms,reports}
# 1. Get release information
echo "1. Extracting release information..."
helm get values "$RELEASE_NAME" -n "$NAMESPACE" -o json > "$OUTPUT_DIR/values.json"
helm get manifest "$RELEASE_NAME" -n "$NAMESPACE" > "$OUTPUT_DIR/manifests/complete.yaml"
helm get notes "$RELEASE_NAME" -n "$NAMESPACE" > "$OUTPUT_DIR/notes.txt" 2>/dev/null || true
# 2. Extract container images from manifests
echo "2. Extracting container images..."
# Parse YAML manifests for image references
python3 << 'PYTHON_SCRIPT' > "$OUTPUT_DIR/images/extracted-images.txt"
import yaml
import sys
import os
manifest_file = f"{os.environ.get('OUTPUT_DIR', '.')}/manifests/complete.yaml"
images = set()
try:
with open(manifest_file, 'r') as f:
# Handle multiple YAML documents
for doc in yaml.safe_load_all(f):
if not doc:
continue
# Extract images from different resource types
if doc.get('kind') in ['Deployment', 'StatefulSet', 'DaemonSet', 'Job', 'CronJob']:
spec = doc.get('spec', {})
# Handle different spec structures
template = spec.get('template', spec.get('jobTemplate', {}).get('spec', {}).get('template', {}))
for container in template.get('spec', {}).get('containers', []):
if 'image' in container:
images.add(container['image'])
for container in template.get('spec', {}).get('initContainers', []):
if 'image' in container:
images.add(container['image'])
elif doc.get('kind') == 'Pod':
for container in doc.get('spec', {}).get('containers', []):
if 'image' in container:
images.add(container['image'])
for image in sorted(images):
print(image)
except Exception as e:
print(f"Error parsing manifests: {e}", file=sys.stderr)
PYTHON_SCRIPT
image_count=$(wc -l < "$OUTPUT_DIR/images/extracted-images.txt")
echo " Found $image_count unique images"
# 3. Generate SBOMs for each image
echo "3. Generating SBOMs for container images..."
while IFS= read -r image; do
echo " Processing: $image"
safe_name=$(echo "$image" | sed 's/[:\/]/_/g')
# Generate SBOM in multiple formats
syft "$image" \
-o spdx-json="$OUTPUT_DIR/sboms/${safe_name}.spdx.json" \
-o cyclonedx-json="$OUTPUT_DIR/sboms/${safe_name}.cyclonedx.json" \
2>/dev/null || echo " Failed to generate SBOM for $image"
done < "$OUTPUT_DIR/images/extracted-images.txt"
# 4. Analyze Helm chart dependencies
echo "4. Analyzing Helm chart dependencies..."
# Get chart information
helm get metadata "$RELEASE_NAME" -n "$NAMESPACE" -o json > "$OUTPUT_DIR/chart-metadata.json" 2>/dev/null ||
echo '{}' > "$OUTPUT_DIR/chart-metadata.json"
# If we have access to the chart, analyze it
CHART_NAME=$(helm get metadata "$RELEASE_NAME" -n "$NAMESPACE" -o json | jq -r '.chart // "unknown"' 2>/dev/null || echo "unknown")
if [ "$CHART_NAME" != "unknown" ]; then
echo " Chart: $CHART_NAME"
# Try to pull and analyze the chart
helm pull "$CHART_NAME" --untar --untardir "$OUTPUT_DIR" 2>/dev/null || true
# Look for Chart.yaml and requirements
find "$OUTPUT_DIR" -name "Chart.yaml" -o -name "requirements.yaml" -o -name "Chart.lock" |
while read chart_file; do
echo " Found: $chart_file"
cp "$chart_file" "$OUTPUT_DIR/" 2>/dev/null || true
done
fi
# 5. Generate comprehensive report
echo "5. Generating comprehensive report..."
python3 << 'PYTHON_SCRIPT' > "$OUTPUT_DIR/reports/helm-sbom-summary.json"
import json
import os
from pathlib import Path
import yaml
output_dir = Path(os.environ.get('OUTPUT_DIR', '.'))
summary = {
'release_name': os.environ.get('RELEASE_NAME', 'unknown'),
'namespace': os.environ.get('NAMESPACE', 'default'),
'analysis_timestamp': os.popen('date -u +%Y-%m-%dT%H:%M:%SZ').read().strip(),
'images': {
'total': 0,
'with_sbom': 0,
'list': []
},
'chart_info': {},
'resources': {
'deployments': 0,
'statefulsets': 0,
'services': 0,
'configmaps': 0,
'secrets': 0
},
'sbom_files': []
}
# Count images
images_file = output_dir / 'images' / 'extracted-images.txt'
if images_file.exists():
with open(images_file) as f:
images = f.read().strip().split('\n')
summary['images']['total'] = len(images)
summary['images']['list'] = images
# Count SBOMs
sboms_dir = output_dir / 'sboms'
if sboms_dir.exists():
sbom_files = list(sboms_dir.glob('*.json'))
summary['images']['with_sbom'] = len(sbom_files) // 2 # Divided by 2 as we generate 2 formats
summary['sbom_files'] = [str(f.relative_to(output_dir)) for f in sbom_files]
# Parse chart metadata
metadata_file = output_dir / 'chart-metadata.json'
if metadata_file.exists():
try:
with open(metadata_file) as f:
metadata = json.load(f)
summary['chart_info'] = {
'name': metadata.get('name', 'unknown'),
'version': metadata.get('version', 'unknown'),
'app_version': metadata.get('appVersion', 'unknown')
}
except:
pass
# Count resources in manifests
manifest_file = output_dir / 'manifests' / 'complete.yaml'
if manifest_file.exists():
try:
with open(manifest_file) as f:
for doc in yaml.safe_load_all(f):
if not doc:
continue
kind = doc.get('kind', '').lower()
if 'deployment' in kind:
summary['resources']['deployments'] += 1
elif 'statefulset' in kind:
summary['resources']['statefulsets'] += 1
elif kind == 'service':
summary['resources']['services'] += 1
elif kind == 'configmap':
summary['resources']['configmaps'] += 1
elif kind == 'secret':
summary['resources']['secrets'] += 1
except:
pass
print(json.dumps(summary, indent=2))
PYTHON_SCRIPT
# Create markdown report
cat > "$OUTPUT_DIR/reports/helm-sbom-report.md" << EOF
# Helm Release SBOM Report
**Release:** $RELEASE_NAME
**Namespace:** $NAMESPACE
**Generated:** $(date)
## Summary
$(cat "$OUTPUT_DIR/reports/helm-sbom-summary.json" | jq -r '
"### Chart Information\n" +
"- Name: " + .chart_info.name + "\n" +
"- Version: " + .chart_info.version + "\n" +
"- App Version: " + .chart_info.app_version + "\n" +
"\n### Images\n" +
"- Total Images: " + (.images.total | tostring) + "\n" +
"- Images with SBOM: " + (.images.with_sbom | tostring) + "\n" +
"\n### Resources\n" +
"- Deployments: " + (.resources.deployments | tostring) + "\n" +
"- StatefulSets: " + (.resources.statefulsets | tostring) + "\n" +
"- Services: " + (.resources.services | tostring) + "\n" +
"- ConfigMaps: " + (.resources.configmaps | tostring) + "\n" +
"- Secrets: " + (.resources.secrets | tostring)
')
## Container Images
$(cat "$OUTPUT_DIR/reports/helm-sbom-summary.json" | jq -r '.images.list[] | "- " + .')
## Generated SBOMs
$(ls -la "$OUTPUT_DIR/sboms/" | grep -E '\.json$' | awk '{print "- " $NF " (" $5 " bytes)"}')
## Files Generated
- Release values: \`values.json\`
- Complete manifests: \`manifests/complete.yaml\`
- Image list: \`images/extracted-images.txt\`
- SBOMs: \`sboms/\`
- Reports: \`reports/\`
EOF
echo ""
echo "=== Helm SBOM Generation Complete ==="
echo "Output directory: $OUTPUT_DIR"
echo "Report: $OUTPUT_DIR/reports/helm-sbom-report.md"
echo "Summary: $OUTPUT_DIR/reports/helm-sbom-summary.json"Helm Chart SBOM Validation
Helm SBOM Policy Validator#!/usr/bin/env python3
"""
Helm Chart SBOM Policy Validator
Validates Helm charts against SBOM policies before deployment
"""
import yaml
import json
import subprocess
import sys
from pathlib import Path
import tempfile
import logging
class HelmSBOMValidator:
def __init__(self, policy_file="helm-sbom-policy.yaml"):
self.logger = self._setup_logging()
self.policy = self._load_policy(policy_file)
def _setup_logging(self):
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
return logging.getLogger(__name__)
def _load_policy(self, policy_file):
"""Load SBOM validation policy"""
default_policy = {
'require_sbom': True,
'max_critical_vulnerabilities': 0,
'max_high_vulnerabilities': 5,
'banned_licenses': ['GPL', 'AGPL'],
'required_labels': ['sbom.generated', 'sbom.version'],
'trusted_registries': ['docker.io', 'gcr.io', 'quay.io'],
'require_signed_images': False
}
if Path(policy_file).exists():
with open(policy_file) as f:
user_policy = yaml.safe_load(f)
default_policy.update(user_policy)
return default_policy
def validate_helm_release(self, release_name, namespace="default"):
"""Validate a Helm release against SBOM policies"""
self.logger.info(f"Validating Helm release: {release_name} in namespace {namespace}")
validation_results = {
'release': release_name,
'namespace': namespace,
'valid': True,
'errors': [],
'warnings': [],
'images': []
}
# Extract images from Helm release
images = self._extract_images_from_release(release_name, namespace)
if not images:
validation_results['errors'].append("No images found in Helm release")
validation_results['valid'] = False
return validation_results
# Validate each image
for image in images:
image_validation = self._validate_image(image)
validation_results['images'].append(image_validation)
if not image_validation['valid']:
validation_results['valid'] = False
validation_results['errors'].extend(image_validation['errors'])
validation_results['warnings'].extend(image_validation.get('warnings', []))
return validation_results
def _extract_images_from_release(self, release_name, namespace):
"""Extract container images from Helm release"""
images = set()
try:
# Get Helm manifest
result = subprocess.run(
['helm', 'get', 'manifest', release_name, '-n', namespace],
capture_output=True,
text=True
)
if result.returncode != 0:
self.logger.error(f"Failed to get Helm manifest: {result.stderr}")
return list(images)
# Parse YAML manifests
for doc in yaml.safe_load_all(result.stdout):
if not doc:
continue
# Extract images from various resource types
images.update(self._extract_images_from_resource(doc))
except Exception as e:
self.logger.error(f"Error extracting images: {e}")
return list(images)
def _extract_images_from_resource(self, resource):
"""Extract images from a Kubernetes resource"""
images = set()
kind = resource.get('kind', '')
if kind in ['Deployment', 'StatefulSet', 'DaemonSet', 'Job', 'CronJob']:
spec = resource.get('spec', {})
# Handle different spec structures
if kind == 'CronJob':
template = spec.get('jobTemplate', {}).get('spec', {}).get('template', {})
else:
template = spec.get('template', {})
# Extract container images
for container in template.get('spec', {}).get('containers', []):
if 'image' in container:
images.add(container['image'])
for container in template.get('spec', {}).get('initContainers', []):
if 'image' in container:
images.add(container['image'])
elif kind == 'Pod':
for container in resource.get('spec', {}).get('containers', []):
if 'image' in container:
images.add(container['image'])
return images
def _validate_image(self, image):
"""Validate a single container image against policies"""
self.logger.info(f" Validating image: {image}")
validation = {
'image': image,
'valid': True,
'errors': [],
'warnings': [],
'sbom_generated': False,
'vulnerabilities': {},
'licenses': []
}
# Check trusted registry
if self.policy.get('trusted_registries'):
if not any(reg in image for reg in self.policy['trusted_registries']):
validation['warnings'].append(f"Image from untrusted registry: {image}")
# Generate SBOM
try:
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
sbom_file = tmp.name
result = subprocess.run(
['syft', image, '-o', f'spdx-json={sbom_file}'],
capture_output=True,
text=True,
timeout=300
)
if result.returncode == 0:
validation['sbom_generated'] = True
# Parse SBOM
with open(sbom_file) as f:
sbom = json.load(f)
# Extract licenses
for package in sbom.get('packages', []):
if 'licenseConcluded' in package:
validation['licenses'].append(package['licenseConcluded'])
# Check banned licenses
for license in validation['licenses']:
for banned in self.policy.get('banned_licenses', []):
if banned.lower() in license.lower():
validation['errors'].append(f"Banned license detected: {license}")
validation['valid'] = False
# Run vulnerability scan
vuln_validation = self._validate_vulnerabilities(image)
validation['vulnerabilities'] = vuln_validation['vulnerabilities']
if not vuln_validation['valid']:
validation['valid'] = False
validation['errors'].extend(vuln_validation['errors'])
else:
if self.policy.get('require_sbom'):
validation['errors'].append(f"Failed to generate SBOM: {result.stderr}")
validation['valid'] = False
else:
validation['warnings'].append(f"Could not generate SBOM for {image}")
# Cleanup
Path(sbom_file).unlink(missing_ok=True)
except subprocess.TimeoutExpired:
validation['errors'].append(f"SBOM generation timeout for {image}")
if self.policy.get('require_sbom'):
validation['valid'] = False
except Exception as e:
validation['errors'].append(f"Error validating image: {e}")
if self.policy.get('require_sbom'):
validation['valid'] = False
return validation
def _validate_vulnerabilities(self, image):
"""Validate image vulnerabilities against policy"""
validation = {
'valid': True,
'errors': [],
'vulnerabilities': {
'critical': 0,
'high': 0,
'medium': 0,
'low': 0
}
}
try:
# Run Grype vulnerability scan
result = subprocess.run(
['grype', image, '-o', 'json'],
capture_output=True,
text=True,
timeout=300
)
if result.returncode == 0:
vuln_data = json.loads(result.stdout)
# Count vulnerabilities by severity
for match in vuln_data.get('matches', []):
severity = match.get('vulnerability', {}).get('severity', 'unknown').lower()
if severity in validation['vulnerabilities']:
validation['vulnerabilities'][severity] += 1
# Check against policy
if validation['vulnerabilities']['critical'] > self.policy.get('max_critical_vulnerabilities', 0):
validation['errors'].append(
f"Too many critical vulnerabilities: {validation['vulnerabilities']['critical']}"
)
validation['valid'] = False
if validation['vulnerabilities']['high'] > self.policy.get('max_high_vulnerabilities', 5):
validation['errors'].append(
f"Too many high vulnerabilities: {validation['vulnerabilities']['high']}"
)
validation['valid'] = False
except Exception as e:
self.logger.error(f"Vulnerability scan failed: {e}")
return validation
def main():
import argparse
parser = argparse.ArgumentParser(description="Helm SBOM Policy Validator")
parser.add_argument("release", help="Helm release name")
parser.add_argument("-n", "--namespace", default="default", help="Kubernetes namespace")
parser.add_argument("-p", "--policy", default="helm-sbom-policy.yaml", help="Policy file")
parser.add_argument("-o", "--output", help="Output file for validation results")
args = parser.parse_args()
validator = HelmSBOMValidator(args.policy)
results = validator.validate_helm_release(args.release, args.namespace)
# Output results
if args.output:
with open(args.output, 'w') as f:
json.dump(results, f, indent=2)
else:
print(json.dumps(results, indent=2))
# Exit with error if validation failed
if not results['valid']:
print("\n❌ Validation FAILED", file=sys.stderr)
for error in results['errors']:
print(f" ERROR: {error}", file=sys.stderr)
sys.exit(1)
else:
print("\n✅ Validation PASSED")
if results['warnings']:
print("\nWarnings:")
for warning in results['warnings']:
print(f" ⚠️ {warning}")
if __name__ == "__main__":
main()Kubernetes Operators for SBOM Management
Custom SBOM Operator
SBOM Operator CRD Definition# sbom-operator-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: sboms.sbom.io
spec:
group: sbom.io
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
targetNamespace:
type: string
description: "Namespace to monitor for SBOM generation"
scanInterval:
type: string
description: "How often to scan for new images"
default: "5m"
storage:
type: object
properties:
type:
type: string
enum: ["configmap", "s3", "persistent-volume"]
config:
type: object
x-kubernetes-preserve-unknown-fields: true
policy:
type: object
properties:
requireSBOM:
type: boolean
default: true
maxVulnerabilities:
type: object
properties:
critical:
type: integer
default: 0
high:
type: integer
default: 5
status:
type: object
properties:
phase:
type: string
lastScanTime:
type: string
totalImages:
type: integer
imagesWithSBOM:
type: integer
failedImages:
type: integer
scope: Namespaced
names:
plural: sboms
singular: sbom
kind: SBOM
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: sbom-operator
namespace: sbom-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: sbom-operator
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets", "daemonsets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch"]
- apiGroups: ["sbom.io"]
resources: ["sboms"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: sbom-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: sbom-operator
subjects:
- kind: ServiceAccount
name: sbom-operator
namespace: sbom-systemCI/CD Pipeline Integration
GitLab CI/CD Kubernetes SBOM Pipeline
# .gitlab-ci.yml
stages:
- build
- sbom-generate
- security-scan
- deploy
- sbom-verify
variables:
[Docker](/guides/docker)_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
KUBECONFIG: /tmp/kubeconfig
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
generate-sbom:
stage: sbom-generate
image: anchore/syft:latest
script:
- syft $DOCKER_IMAGE -o spdx-json=sbom.spdx.json
- syft $DOCKER_IMAGE -o cyclonedx-json=sbom.cyclonedx.json
artifacts:
paths:
- sbom.spdx.json
- sbom.cyclonedx.json
expire_in: 1 week
security-scan:
stage: security-scan
image: anchore/grype:latest
script:
- grype $DOCKER_IMAGE -o json > vulnerabilities.json
- grype $DOCKER_IMAGE -o table > vulnerabilities.txt
# Fail pipeline if critical vulnerabilities found
- |
CRITICAL=$(jq '.matches[] | select(.vulnerability.severity == "Critical") | length' vulnerabilities.json)
if [ "$CRITICAL" -gt 0 ]; then
echo "Critical vulnerabilities found: $CRITICAL"
exit 1
fi
artifacts:
paths:
- vulnerabilities.json
- vulnerabilities.txt
reports:
container_scanning: vulnerabilities.json
deploy:
stage: deploy
image: bitnami/kubectl:latest
script:
- echo "$KUBE_CONFIG" | base64 -d > $KUBECONFIG
- kubectl apply -f k8s/deployment.yaml
- kubectl set image deployment/myapp myapp=$DOCKER_IMAGE
- kubectl rollout status deployment/myapp
verify-sbom:
stage: sbom-verify
image: bitnami/kubectl:latest
script:
# Verify SBOM is stored in cluster
- kubectl get configmap sbom-$CI_COMMIT_SHA -o jsonpath='{.data.sbom\.json}' | jq .
# Create SBOM resource in cluster
- |
cat <<EOF | kubectl apply -f -
apiVersion: sbom.io/v1
kind: SBOM
metadata:
name: app-sbom-$CI_COMMIT_SHA
namespace: production
spec:
targetNamespace: production
scanInterval: "10m"
storage:
type: configmap
EOFGitHub Actions Kubernetes SBOM Workflow
# .github/workflows/k8s-sbom.yml
name: Kubernetes SBOM Workflow
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-scan:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
outputs:
image-digest: ${{ steps.build.outputs.digest }}
sbom-path: ${{ steps.sbom.outputs.path }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Generate SBOM
id: sbom
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
format: spdx-json
output-file: sbom.spdx.json
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
- name: Run vulnerability scan
uses: anchore/scan-action@v3
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
fail-build: true
severity-cutoff: high
deploy-to-k8s:
needs: build-and-scan
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/k8s-set-context@v3
with:
method: kubeconfig
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- name: Download SBOM
uses: actions/download-artifact@v4
with:
name: sbom
path: ./sboms/
- name: Create SBOM ConfigMap
run: |
kubectl create configmap sbom-${{ github.sha }} \
--from-file=./sboms/sbom.spdx.json \
--dry-run=client -o yaml | kubectl apply -f -
- name: Deploy application
run: |
kubectl set image deployment/myapp \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
kubectl rollout status deployment/myapp
- name: Create SBOM tracking resource
run: |
cat <<EOF | kubectl apply -f -
apiVersion: sbom.io/v1
kind: SBOM
metadata:
name: myapp-sbom-${{ github.sha }}
namespace: default
spec:
targetNamespace: default
scanInterval: "1h"
storage:
type: configmap
policy:
requireSBOM: true
maxVulnerabilities:
critical: 0
high: 5
EOFSecurity Scanning & Policy Enforcement
Open Policy Agent (OPA) SBOM Policies
# sbom-admission-policy.rego
package kubernetes.admission
import data.kubernetes.namespaces
import data.sbom.policies
# Deny deployments without valid SBOMs
deny[msg] {
input.request.kind.kind == "Deployment"
input.request.operation == "CREATE"
# Extract container images
images := [image | image := input.request.object.spec.template.spec.containers[_].image]
# Check if all images have SBOMs
image := images[_]
not has_valid_sbom(image)
msg := sprintf("Image %v does not have a valid SBOM", [image])
}
# Deny deployments with high-risk vulnerabilities
deny[msg] {
input.request.kind.kind == "Deployment"
input.request.operation == "CREATE"
images := [image | image := input.request.object.spec.template.spec.containers[_].image]
image := images[_]
sbom := get_sbom(image)
vulnerabilities := get_vulnerabilities(sbom)
critical_count := count([v | v := vulnerabilities[_]; v.severity == "Critical"])
critical_count > policies.max_critical_vulnerabilities
msg := sprintf("Image %v has %d critical vulnerabilities (max allowed: %d)",
[image, critical_count, policies.max_critical_vulnerabilities])
}
# Deny deployments with banned licenses
deny[msg] {
input.request.kind.kind == "Deployment"
input.request.operation == "CREATE"
images := [image | image := input.request.object.spec.template.spec.containers[_].image]
image := images[_]
sbom := get_sbom(image)
licenses := get_licenses(sbom)
license := licenses[_]
license in policies.banned_licenses
msg := sprintf("Image %v contains banned license: %v", [image, license])
}
# Helper functions
has_valid_sbom(image) {
sbom := get_sbom(image)
sbom.packages
count(sbom.packages) > 0
}
get_sbom(image) := sbom {
# Look up SBOM from external data source
sbom := data.sboms[normalize_image_name(image)]
}
get_vulnerabilities(sbom) := vulnerabilities {
# Extract vulnerabilities from SBOM or external scan
vulnerabilities := data.vulnerabilities[sbom.id]
}
get_licenses(sbom) := licenses {
licenses := [license |
package := sbom.packages[_]
license := package.licenses[_]
]
}
normalize_image_name(image) := normalized {
# Normalize image name for lookup
normalized := replace(image, ":", "_")
}Gatekeeper SBOM Constraint Templates
# sbom-constraint-template.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequiresbom
spec:
crd:
spec:
names:
kind: K8sRequireSBOM
validation:
openAPIV3Schema:
type: object
properties:
exemptImages:
type: array
items:
type: string
maxCriticalVulns:
type: integer
bannedLicenses:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiresbom
violation[{"msg": msg}] {
container := input.review.object.spec.template.spec.containers[_]
image := container.image
not exempt_image(image)
not has_sbom_annotation(input.review.object)
msg := sprintf("Container image %v requires SBOM annotation", [image])
}
violation[{"msg": msg}] {
container := input.review.object.spec.template.spec.containers[_]
image := container.image
sbom_data := input.review.object.metadata.annotations["sbom.io/data"]
vulnerabilities := get_critical_vulns(sbom_data)
count(vulnerabilities) > input.parameters.maxCriticalVulns
msg := sprintf("Image %v has too many critical vulnerabilities", [image])
}
exempt_image(image) {
exempt := input.parameters.exemptImages[_]
contains(image, exempt)
}
has_sbom_annotation(obj) {
obj.metadata.annotations["sbom.io/generated"]
}
get_critical_vulns(sbom_json) := vulns {
sbom := json.unmarshal(sbom_json)
vulns := [v | v := sbom.vulnerabilities[_]; v.severity == "Critical"]
}
---
apiVersion: config.gatekeeper.sh/v1beta1
kind: K8sRequireSBOM
metadata:
name: must-have-sbom
spec:
match:
- apiGroups: ["apps"]
kinds: ["Deployment"]
namespaces: ["production"]
parameters:
exemptImages:
- "gcr.io/distroless/"
- "scratch"
maxCriticalVulns: 0
bannedLicenses:
- "GPL"
- "AGPL"SBOM Storage and Management
Kubernetes Native SBOM Storage
# sbom-storage-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: sbomstores.sbom.io
spec:
group: sbom.io
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
image:
type: string
description: "Container image reference"
digest:
type: string
description: "Image digest"
sbom:
type: object
x-kubernetes-preserve-unknown-fields: true
description: "SBOM data"
format:
type: string
enum: ["spdx", "cyclonedx", "syft"]
generatedBy:
type: string
description: "Tool that generated the SBOM"
timestamp:
type: string
format: date-time
vulnerabilities:
type: array
items:
type: object
properties:
id:
type: string
severity:
type: string
package:
type: string
scope: Namespaced
names:
plural: sbomstores
singular: sbomstore
kind: SBOMStore
---
apiVersion: v1
kind: Service
metadata:
name: sbom-api
namespace: sbom-system
spec:
selector:
app: sbom-api
ports:
- port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sbom-api
namespace: sbom-system
spec:
replicas: 2
selector:
matchLabels:
app: sbom-api
template:
metadata:
labels:
app: sbom-api
spec:
serviceAccountName: sbom-operator
containers:
- name: api
image: sbom-api:latest
ports:
- containerPort: 8080
env:
- name: STORAGE_BACKEND
value: "kubernetes"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"Best Practices
Kubernetes SBOM Best Practices Summary
1. Automated SBOM Generation- Integrate SBOM generation into CI/CD pipelines
- Use admission controllers to enforce SBOM requirements
- Implement continuous scanning for running workloads
- Store SBOMs as Kubernetes Custom Resources for integration
- Use external storage (S3, databases) for long-term retention
- Implement SBOM versioning and lifecycle management
- Link SBOMs with vulnerability scanning results
- Implement policy-based admission control
- Set up alerting for policy violations
- Isolate SBOMs by namespace
- Implement RBAC for SBOM access
- Provide tenant-specific SBOM policies
- Cache SBOM generation results
- Use incremental scanning for large clusters
- Implement asynchronous processing for SBOM tasks
Troubleshooting
Common Kubernetes SBOM Issues
Issue: SBOM Generation Fails for Private Registry Images# Solution: Configure registry authentication
kubectl create secret docker-registry regcred \
--docker-server=private-registry.com \
--docker-username=username \
--docker-password=password
# Update deployment to use secret
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
imagePullSecrets:
- name: regcred# Check operator logs
kubectl logs -n sbom-system deployment/sbom-operator
# Verify RBAC permissions
kubectl auth can-i list pods --as=system:serviceaccount:sbom-system:sbom-operator
# Check resource quotas
kubectl describe quota -n sbom-system# Optimize resource limits
apiVersion: v1
kind: LimitRange
metadata:
name: sbom-limits
namespace: sbom-system
spec:
limits:
- default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "256Mi"
type: ContainerThis comprehensive Kubernetes SBOM guide provides enterprise-ready solutions for implementing SBOM management across Kubernetes environments, ensuring security, compliance, and operational excellence in containerized deployments.