Practical Kubernetes SBOM guide covering image inventory, cluster workflows, validation, storage, Helm charts, and policy enforcement.

Updated:

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:

  1. generate SBOMs at the image stage in CI
  2. validate them before release with the SBOM Validator
  3. carry those artifacts into registry, deployment, and policy workflows
  4. 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
2. Namespace Level
  • All workloads within a namespace
  • ConfigMaps and Secrets containing software
  • Persistent Volume contents
  • Service mesh sidecars
3. Workload Level
  • Individual pod containers
  • Init containers and ephemeral containers
  • Mounted volumes with software
  • Runtime dependencies
4. Image Level
  • 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
Multi-Container Pods
# 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
ConfigMap and Secret Software
# 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 | bash

Cluster-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
done

Helm 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-system

CI/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
      EOF

GitHub 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
        EOF

Security 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
2. Storage Strategy
  • Store SBOMs as Kubernetes Custom Resources for integration
  • Use external storage (S3, databases) for long-term retention
  • Implement SBOM versioning and lifecycle management
3. Security Integration
  • Link SBOMs with vulnerability scanning results
  • Implement policy-based admission control
  • Set up alerting for policy violations
4. Multi-Tenancy
  • Isolate SBOMs by namespace
  • Implement RBAC for SBOM access
  • Provide tenant-specific SBOM policies
5. Performance Optimization
  • 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
Issue: Operator Not Processing All Namespaces
# 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
Issue: High Resource Usage During SBOM Generation
# 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: Container

This comprehensive Kubernetes SBOM guide provides enterprise-ready solutions for implementing SBOM management across Kubernetes environments, ensuring security, compliance, and operational excellence in containerized deployments.