A KBOM, or Kubernetes bill of materials, is an inventory of one running cluster: the control plane version, every node's operating system, kernel, kubelet and container runtime, the add-ons, custom resource definitions, Helm releases and every container image by digest. No standard defines a KBOM, so most teams build one from CycloneDX parts.
Neither CycloneDX nor SPDX has a document type called KBOM, but both have the building blocks: platform, container and operating system components, services, links to other BOMs and a dependency graph. This guide covers where the name came from, what a cluster inventory should contain, a schema valid CycloneDX example, and a step by step build with kubectl, Helm, cdxgen and the CycloneDX CLI.
Key takeaways
- KBOM is a use case, not a format. The term was introduced by an open source project whose first tagged release is dated 25 April 2023. It described its own schema as "a rough specification". CycloneDX has no KBOM capability page and SPDX 3.0.1 has no Kubernetes profile.
- A useful KBOM has layers. Control plane version, per node facts (OS image, kernel, kubelet, container runtime, architecture), add-ons, CRDs, admission webhooks, Helm releases, workloads, and images pinned by digest.
- CycloneDX already carries it. Platform, container and operating-system components,
pkg:ocipurls for images,servicesfor endpoints, andbomexternal references to per image SBOMs and per node OBOMs. - Record versions per node. Kubelets may run up to three minor versions behind the API server, so one cluster can run four kubelet minors at once.
- Kubernetes publishes the inputs. A signed SPDX 2.3 SBOM for every release at
sbom.k8s.ioand an official CVE feed in JSON. A KBOM joins them to what you run. - Inventory and enforcement are different jobs. A KBOM records what is running. Admission control with signature and attestation checks decides what may run. You want both.
Where did the term KBOM come from?
The phrase "Kubernetes Bill of Materials" and the acronym KBOM were introduced by an open source project published in spring 2023. Its earliest tagged release, v0.0.1, is dated 25 April 2023, followed by a run of small releases that week and v0.1.0 on 27 April 2023. The project is Apache 2.0 licensed and written in Go. Its most recent tag we could see when checking on 23 October 2026 was v0.3.4, dated 22 May 2026, so it is still maintained, if slowly.
The project's README is candid about its maturity. It calls itself a standard but describes its schema as a first draft, "a rough specification", that should "fall in line with other Bill of Materials (BOM) standards". Its high level object model has three parts:
| Section | Fields the project defines |
|---|---|
| Instances (nodes) | Name, hostname, cloud type, creation timestamp, capacity, allocatable resources, OS version, kernel version, architecture, CRI version, kubelet version, kube-proxy version |
| Images | Name, full name, version, digest |
| KubeObjects | Kind, API version, count, details |
The tool reads a cluster through your kubeconfig and writes its own JSON or YAML, or CycloneDX JSON or XML. Even the project that coined the term treats CycloneDX as the interchange format. Its README says it was tested on Kubernetes versions newer than 1.19 and on the main managed services, with some metadata fields empty on some clusters.
What the term is not:
- It is not a CycloneDX BOM type. The CycloneDX capabilities index lists SBOM, SaaSBOM, CBOM, HBOM, ML-BOM, OBOM, MBOM, VDR, VEX, BOV, attestations (CDXA), BOM-Link and a common release notes format. A request for a
kbomcapability page on cyclonedx.org returned 404 on 23 October 2026. - It is not an SPDX profile. SPDX 3.0.1 defines ten profile identifiers: core, software, security, simpleLicensing, expandedLicensing, dataset, ai, build, lite and extension. None is specific to Kubernetes.
- It is not a regulatory term. None of the CISA SBOM material we checked uses it. When a contract or regulator asks for an SBOM of something you deploy on Kubernetes, the answer is still an SBOM, possibly with an OBOM and a SaaSBOM next to it.
So when someone asks for "a KBOM", ask which question they need answered: which Kubernetes and node versions run where, which images are deployed where, or which clusters an advisory affects. All three are inventory questions that CycloneDX answers without a new document type.
New to BOMs? Start with what a software bill of materials is and the six SBOM lifecycle types; a KBOM is closest to a deployed or runtime SBOM for one cluster.
Is a KBOM a standard? How the options compare
Three ways to write the same inventory exist today. This table compares them on the points that decide whether other tools can read your file.
| KBOM project schema | CycloneDX 1.6 or 1.7 | SPDX 3.0.1 | |
|---|---|---|---|
| Who maintains it | One open source project | OWASP CycloneDX, standardised by Ecma TC54 as ECMA-424 | The SPDX project at the Linux Foundation |
| Formal status | Self described draft | Published specification with JSON, XML and Protocol Buffers schemas | Published specification; SPDX 2.2.1 is ISO/IEC 5962:2021 |
| Node facts | First class fields | operating-system component plus properties, or a linked OBOM | software_Package with an operating system purpose plus annotations or properties |
| Images | Name, version, digest | container component, pkg:oci purl, SHA-256 hash | software_Package with primaryPurpose: container |
| Kubernetes objects | Kind, API version, count | Components, services and properties; no native "Kubernetes object" type | software_File (the SPDX file purpose explicitly mentions a Kubernetes deployment) or configuration |
| Links to image SBOMs | Not defined | externalReferences of type bom, BOM-Link URNs | External maps and relationships across SPDX documents |
| Vulnerability data | Not defined | vulnerabilities array, VEX, VDR | Security profile with VEX relationships |
| Consumer tool support | The project's own tool | Wide: SBOM platforms, Dependency-Track style analysers, CycloneDX CLI | Growing for 3.0; most tools still read SPDX 2.3 |
The practical recommendation: if you already run the KBOM project's tool, ask it for cyclonedx-json output and treat that as your file of record. Otherwise build the inventory in CycloneDX directly, or in SPDX 3 if that is your organisation's standard, expecting fewer consumers today.
What should a cluster inventory actually list?
A KBOM is only as useful as the questions it can answer during an advisory or an audit. Work backwards from those questions. Each row below is a layer that has its own version, its own release cadence and its own advisories.
| Layer | What to record | Where it comes from | Why it matters |
|---|---|---|---|
| Control plane | Version of kube-apiserver, kube-controller-manager, kube-scheduler, cloud-controller-manager, etcd | kubectl version, static pod manifests or the managed service's version field | Core Kubernetes advisories name these components and fixed patch versions |
| Nodes | OS image, kernel version, kubelet version, kube-proxy version where reported, container runtime and version, architecture | status.nodeInfo on each Node object | Kernel and runtime advisories hit nodes, and kubelet versions can differ per node |
| Node operating system packages | Installed OS packages | An OBOM generated on the node or from the node image build | Most node CVEs are in OS packages, not in Kubernetes |
| Add-ons | CNI plugin, CSI drivers, CoreDNS, ingress controller, metrics, service mesh, autoscalers | Helm releases, manifests in kube-system and add-on namespaces | The Kubernetes official CVE feed includes add-on issues such as ingress controllers and CSI drivers, not just core |
| Custom resource definitions | Name, API group, served versions, owning operator | kubectl get crd | CRDs tell you which operators run with cluster wide permissions |
| Admission webhooks | Validating and mutating webhook configurations and the services they call | kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations | Webhooks sit in the request path for every object they match |
| Helm releases | Release name, namespace, chart name and version, app version, revision | helm list -A -o json | Most add-ons and many applications arrive as charts |
| Workloads | Deployments, StatefulSets, DaemonSets, Jobs and CronJobs per namespace | kubectl get with JSON output | Links each image to an owner and a namespace |
| Images | Registry, repository, tag, and above all digest | Pod status.containerStatuses[].imageID and pod specs | Tags move; a digest names exactly one image |
| Services and endpoints | Service names, ports, ingress hosts, external endpoints the workloads call | Service and Ingress objects, application config | This is the SaaSBOM slice of the cluster |
Why each node gets its own versions
The Kubernetes version skew policy is the reason a single "cluster version" field is not enough. As of the policy page we fetched on 23 October 2026:
- In highly available clusters, the newest and oldest kube-apiserver instances must be within one minor version.
- The kubelet must not be newer than kube-apiserver and may be up to three minor versions older. Kubelets older than 1.25 may only be two minors older.
- kube-proxy follows the same three minor rule against the API server and may also be up to three minors older or newer than the kubelet beside it.
- kube-controller-manager, kube-scheduler and cloud-controller-manager must not be newer than the API server and may be one minor older.
- kubectl is supported within one minor version, older or newer, of kube-apiserver.
So a cluster whose API server runs 1.37 can legitimately have nodes on kubelet 1.37, 1.36, 1.35 and 1.34. If an advisory affects kubelet below a certain patch release, you need the version of every node, not the version on the cluster's settings page.
Why support dates belong in the KBOM
The Kubernetes project maintains release branches for the three most recent minor releases, and 1.19 and newer get roughly one year of patch support. On the releases page we fetched on 23 October 2026, the maintained branches were 1.37, 1.36 and 1.35, and 1.34 was still listed with an end of life date of 27 October 2026. Recording the end of life date next to each version in your KBOM turns a static inventory into a list of clusters that will stop receiving fixes soon. Managed services publish their own support calendars, which can differ from upstream, so record the provider's date where it applies.
Why digests, not tags
A tag such as v1.37.1 is a pointer that can be moved. A digest such as sha256:e190f914... is the hash of the image index or manifest and names exactly one artifact. The Kubernetes documentation on verifying signed artifacts makes the same point for control plane images: once you have verified an image, reference it by digest in your Pod manifests. In a KBOM, record the digest the node actually pulled, which the kubelet reports in each container's imageID, and keep the tag as a secondary label.
How to map a cluster onto CycloneDX
CycloneDX 1.6 defines thirteen component types: application, framework, library, container, platform, operating-system, device, device-driver, firmware, file, machine-learning-model, data and cryptographic-asset. The schema describes platform as "a runtime environment which interprets or executes software", which fits a Kubernetes cluster and its control plane well. container covers images, and operating-system covers node operating systems "without regard to deployment model".
Here is a mapping that keeps the file readable and lets standard tools work with it.
| Cluster element | CycloneDX construct | Notes |
|---|---|---|
| The cluster itself | metadata.component with type: platform | The subject of the BOM. Add properties for provider, region, node count |
| Control plane release | components[] with type: platform, name kubernetes, version such as 1.37.1 | Link the upstream release SBOM with an externalReferences entry of type bom |
| Control plane images | components[] with type: container and a pkg:oci purl | Include the digest in the purl version and in hashes |
| Each node | components[] with type: operating-system | Put kernel, kubelet, runtime and architecture in properties; link the node's OBOM by BOM-Link |
| Add-ons and Helm releases | components[] with type: application | Chart, namespace and revision as properties; images as dependencies |
| CRDs | Properties on the owning add-on, or type: data components | Keep it simple unless you need per CRD policy |
| Services and endpoints | services[] with endpoints, authenticated and x-trust-boundary | This is where the KBOM overlaps with a SaaSBOM |
| Relationships | dependencies[] | Cluster depends on control plane, nodes and releases; releases depend on images |
| Image contents | A separate SBOM per image digest, linked by externalReferences | Do not inline thousands of packages into the KBOM |
Two choices in that table deserve a word.
Link, do not inline. A single application image can carry hundreds of packages, and a node OBOM can carry thousands. Put each one in its own BOM and reference it with a BOM-Link URN (urn:cdx:<serialNumber>/<version>) or a URL. The KBOM stays small enough to diff, and each child BOM can be regenerated on its own schedule. This also matches how most teams already produce one SBOM per image digest.
Use your own property namespace. CycloneDX properties are free form. The example uses a kbom: prefix; pick one your team owns and document it so consumers do not mistake it for a registered CycloneDX taxonomy name.
If you work in SPDX 3.0.1 instead, the equivalent is a software_Package per image with software_primaryPurpose set to container, packages with the operating system purpose for nodes, and relationships between them. The SPDX software purpose vocabulary even lists "Kubernetes deployment" as an example of a file element, which tells you where manifests belong.
A minimal KBOM in CycloneDX JSON
The document below describes one cluster with its control plane release, the kube-apiserver image, one worker node, one Helm release and one service. It validates against the CycloneDX 1.6 JSON schema (bom-1.6.schema.json from the CycloneDX specification repository, checked on 23 October 2026 with a Draft 7 JSON Schema validator).
The kube-apiserver digest is the real image index digest that registry.k8s.io returned for kube-apiserver:v1.37.1 on 23 October 2026, and the upstream SBOM URL is the real one for that release. The node values, the Helm release and the service are illustrative.
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:4f0c8a3e-6d1b-4b7e-9a52-2f3c1d9e7b10",
"version": 1,
"metadata": {
"timestamp": "2026-10-23T09:00:00Z",
"component": {
"type": "platform",
"bom-ref": "cluster/prod-eu-1",
"name": "prod-eu-1",
"description": "Production Kubernetes cluster, inventory taken from the live API",
"properties": [
{ "name": "kbom:cluster:provider", "value": "self-managed" },
{ "name": "kbom:cluster:nodeCount", "value": "3" }
]
}
},
"components": [
{
"type": "platform",
"bom-ref": "k8s/control-plane",
"name": "kubernetes",
"version": "1.37.1",
"purl": "pkg:github/kubernetes/kubernetes@v1.37.1",
"externalReferences": [
{ "type": "bom", "url": "https://sbom.k8s.io/v1.37.1/release", "comment": "Upstream SPDX 2.3 release SBOM" }
]
},
{
"type": "container",
"bom-ref": "img/kube-apiserver",
"name": "kube-apiserver",
"version": "sha256:e190f914a6cc21cab9268485d1ae7282fdeab1315ac64b668aa42d732fd1061d",
"purl": "pkg:oci/kube-apiserver@sha256%3Ae190f914a6cc21cab9268485d1ae7282fdeab1315ac64b668aa42d732fd1061d?repository_url=registry.k8s.io%2Fkube-apiserver&tag=v1.37.1",
"hashes": [
{ "alg": "SHA-256", "content": "e190f914a6cc21cab9268485d1ae7282fdeab1315ac64b668aa42d732fd1061d" }
]
},
{
"type": "operating-system",
"bom-ref": "node/worker-a-1",
"name": "worker-a-1",
"description": "Node OS as reported in status.nodeInfo",
"properties": [
{ "name": "kbom:node:osImage", "value": "Ubuntu 24.04.3 LTS" },
{ "name": "kbom:node:kernelVersion", "value": "6.8.0-85-generic" },
{ "name": "kbom:node:kubeletVersion", "value": "v1.36.5" },
{ "name": "kbom:node:containerRuntimeVersion", "value": "containerd://2.1.4" },
{ "name": "kbom:node:architecture", "value": "amd64" }
],
"externalReferences": [
{ "type": "bom", "url": "urn:cdx:7c1e2d4a-0b9f-4e61-8f3a-5d2c6b8e9a01/1", "comment": "OBOM generated on this node" }
]
},
{
"type": "application",
"bom-ref": "helm/payments/payments-api",
"name": "payments-api",
"version": "3.4.0",
"description": "Helm release payments-api in namespace payments",
"properties": [
{ "name": "kbom:helm:chart", "value": "payments-api-3.4.0" },
{ "name": "kbom:helm:namespace", "value": "payments" },
{ "name": "kbom:helm:revision", "value": "12" }
]
}
],
"services": [
{
"bom-ref": "svc/payments/payments-api",
"name": "payments-api",
"endpoints": [ "https://payments-api.payments.svc.cluster.local:8443" ],
"authenticated": true,
"x-trust-boundary": false
}
],
"dependencies": [
{ "ref": "cluster/prod-eu-1", "dependsOn": [ "k8s/control-plane", "node/worker-a-1", "helm/payments/payments-api" ] },
{ "ref": "k8s/control-plane", "dependsOn": [ "img/kube-apiserver" ] },
{ "ref": "helm/payments/payments-api", "dependsOn": [ "svc/payments/payments-api" ] }
]
}
A few details worth copying:
- The purl encodes the digest. In a
pkg:ocipackage URL the digest is the version, with the colon percent encoded as%3A, and the registry path goes in therepository_urlqualifier. The tag is kept as a qualifier for humans. - The kubelet is behind the control plane on purpose. Kubelet 1.36.5 under an API server at 1.37.1 is inside the skew policy. Your KBOM should show that skew rather than hide it.
- The node links out. Its operating system packages live in a separate OBOM referenced by a BOM-Link URN. The KBOM records the node facts; the OBOM records what is installed.
Step by step: build a KBOM with kubectl, Helm, cdxgen and the CycloneDX CLI
This walkthrough uses only read access to the cluster. Run it from a workstation or a CI runner with a kubeconfig bound to a read only role. Use a kubectl within one minor version of your API server, as the skew policy requires.
1. Install the tools
cdxgen's npm package was renamed in version 13: v13 and later publish as @cdxgen/cdxgen, while @cyclonedx/cdxgen stays on the v12 line. The command name is unchanged. The project recommends its standalone binaries or container images for CI; the npm route is shown here because it is short.
npm install -g @cdxgen/cdxgen --ignore-scripts
cdxgen --version
# CycloneDX CLI: download a release binary for your platform from
# https://github.com/CycloneDX/cyclonedx-cli/releases, then:
cyclonedx --version
kubectl version --client
helm version
cdxgen defaults to CycloneDX 1.7 output and accepts --spec-version 1.6, 1.7 or 2.0. The commands below pin 1.6 because more consumers read it today; drop the flag if your tools accept 1.7.
2. Snapshot the control plane and node versions
mkdir -p kbom/{raw,manifests,sboms,oboms}
cd kbom
# Client and server versions
kubectl version -o json > raw/version.json
jq -r '.serverVersion.gitVersion' raw/version.json
# One line per node: name, kubelet, OS image, kernel, runtime, architecture
kubectl get nodes -o json > raw/nodes.json
jq -r '.items[] | [
.metadata.name,
.status.nodeInfo.kubeletVersion,
.status.nodeInfo.osImage,
.status.nodeInfo.kernelVersion,
.status.nodeInfo.containerRuntimeVersion,
.status.nodeInfo.architecture
] | @tsv' raw/nodes.json
The Node status.nodeInfo block is published by each kubelet and, per the Kubernetes node status documentation, describes the kernel version, Kubernetes version, container runtime and operating system of the node. On self managed control planes, also record the control plane images from the static pod manifests (/etc/kubernetes/manifests on kubeadm clusters) or from the kube-system pods.
3. List every image by digest
The Kubernetes task page on listing running images uses the pod spec. That gives you what was requested, usually a tag. For a KBOM you also want what was pulled, which the kubelet reports per container in imageID.
# Requested images, from the pod specs (init and regular containers)
kubectl get pods -A -o jsonpath="{.items[*].spec['initContainers','containers'][*].image}" \
| tr -s '[:space:]' '\n' | sort -u > raw/images-requested.txt
# Resolved images, from container status, with namespace and pod
kubectl get pods -A -o json \
| jq -r '.items[] | .metadata.namespace as $ns | .metadata.name as $pod
| (.status.initContainerStatuses // []) + (.status.containerStatuses // [])
| .[] | [$ns, $pod, .image, .imageID] | @tsv' \
| sort -u > raw/images-resolved.tsv
The format of imageID depends on the container runtime, but on common runtimes it is a repository@sha256:... reference. Where a pod spec uses a tag and the resolved digest differs between nodes, you have found drift worth investigating: two nodes pulled different images under the same tag.
4. Capture Helm releases, CRDs and admission webhooks
# Every Helm release in every namespace
helm list -A -o json > raw/helm-releases.json
# Rendered manifests for each release, one file per release
jq -r '.[] | "\(.namespace) \(.name)"' raw/helm-releases.json | while read -r ns name; do
helm get manifest "$name" -n "$ns" > "manifests/${ns}__${name}.yaml"
done
# CRDs with group and served versions
kubectl get crd -o json \
| jq -r '.items[] | [.metadata.name, .spec.group, ([.spec.versions[] | select(.served) | .name] | join(","))] | @tsv' \
> raw/crds.tsv
# Admission webhooks and the services they call
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations -o json > raw/webhooks.json
# Workloads not managed by Helm
kubectl get deploy,statefulset,daemonset,cronjob -A -o yaml > manifests/live-workloads.yaml
helm get manifest returns the YAML Kubernetes resources generated from a release's chart, including resources from dependent charts. helm list accepts -A for all namespaces and -o json for machine readable output.
5. Turn manifests into a CycloneDX inventory with cdxgen
cdxgen lists kubernetes, openshift, kustomize, operator, skaffold, tekton, docker-compose and yaml-manifest among its container file project types, which share one handler for YAML manifests. helm and charts have their own handler, and universal collects "as many components and services as possible" across package, container and Kubernetes manifests.
cdxgen -t kubernetes --spec-version 1.6 -o kbom-manifests.json ./manifests
Reading the cdxgen source for this handler shows what to expect. It parses every .yaml and .yml file in the directory, extracts image references, and records each image as a container component with a pkg:oci purl, a repository_url qualifier and a tag qualifier. It adds properties pointing back to the source file (internal:SrcFile) and the original image string (oci:SrcImage). Where an image reference is pinned by digest, the digest becomes the version; where it is not, the version is recorded as latest, which is a useful signal on its own. For images it can follow, it also attempts an image scan and adds the packages it finds, so expect large output and run it where the registry is reachable.
For chart sources rather than rendered output, run cdxgen -t helm against the chart directory. Rendered manifests are closer to the truth, because values files change image tags.
6. Generate one SBOM per image digest
cut -f4 raw/images-resolved.tsv | sed 's#^docker-pullable://##' | sort -u | grep '@sha256:' > raw/digests.txt
while read -r ref; do
safe=$(echo "$ref" | tr '/:@' '___')
cdxgen -t docker "$ref" --spec-version 1.6 -o "sboms/${safe}.json"
done < raw/digests.txt
Scanning by digest means the SBOM describes exactly what the nodes run. For more on image enumeration, see our guide to listing every image in a Docker Compose project; the same thinking applies to pods.
7. Generate node OBOMs
cdxgen's obom command is an alias for cdxgen -t os and inventories a live Linux, Windows or macOS system through osquery. Run it on the node itself, or better, in the pipeline that builds your node image, so the OBOM is tied to an image version rather than to one machine.
# On the node, or in the node image build
obom -o oboms/worker-a-1.json --spec-version 1.6
On a managed service where you cannot log into nodes, record the node image name and version from status.nodeInfo.osImage, and note in the KBOM which nodes have an OBOM and which do not.
8. Fetch and verify the upstream Kubernetes release SBOM
Kubernetes publishes a signed SBOM for each release. The release engineering team produces it in SPDX 2.3 tag value format; the v1.37.1 document we fetched lists "Kubernetes Release Engineering" as creator and was generated by the project's bom tool. The Kubernetes documentation gives these verification steps.
VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt)
curl -Ls "https://sbom.k8s.io/$VERSION/release" -o "$VERSION.spdx"
echo "$(curl -Ls "https://sbom.k8s.io/$VERSION/release.sha512") $VERSION.spdx" | sha512sum --check
curl -Ls "https://sbom.k8s.io/$VERSION/release.sig" -o "$VERSION.spdx.sig"
curl -Ls "https://sbom.k8s.io/$VERSION/release.cert" -o "$VERSION.spdx.cert"
cosign verify-blob \
--certificate "$VERSION.spdx.cert" \
--signature "$VERSION.spdx.sig" \
--certificate-identity krel-staging@k8s-releng-prod.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com \
"$VERSION.spdx"
Replace stable.txt with your cluster's exact version from step 2, so the SBOM matches what you run rather than the newest release. Our post on verifying Kubernetes control plane images and the SBOM covers the image side of this in detail, so we will not repeat it here.
9. Assemble, validate and diff
Build the top level KBOM (the cluster, control plane, nodes and releases) from the raw JSON with a short script, or start from the example above. Then merge in the manifest inventory and validate.
cyclonedx merge \
--input-files kbom-core.json kbom-manifests.json \
--hierarchical --name prod-eu-1 --version 2026-10-23 \
--output-version v1_6 --output-format json \
--output-file kbom.json
cyclonedx validate --input-file kbom.json --input-version v1_6 --fail-on-errors
# Compare with yesterday's inventory
cyclonedx diff kbom-2026-10-22.json kbom.json --component-versions
A hierarchical merge needs each input BOM to describe its subject in metadata.component, plus --name and --version. Keep per image SBOMs and node OBOMs as separate files linked by reference rather than merging them in, or the KBOM stops being readable.
10. Run it on a schedule
A KBOM taken once goes stale on the next deploy. A nightly job is a reasonable default, plus a run after every cluster upgrade.
name: nightly-kbom
on:
schedule:
- cron: "17 2 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
kbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install tools
run: |
npm install -g @cdxgen/cdxgen --ignore-scripts
# install kubectl, helm and the CycloneDX CLI from their official releases here
- name: Write read-only kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KBOM_READONLY_KUBECONFIG }}" > ~/.kube/config
chmod 600 ~/.kube/config
- name: Build KBOM
run: ./scripts/build-kbom.sh # steps 2 to 9 above
- uses: actions/upload-artifact@v4
with:
name: kbom-${{ github.run_id }}
path: kbom/
Bind the kubeconfig to a role that can get and list the resources above and nothing else. A KBOM job does not need to create, patch or exec into anything.
How a KBOM supports CVE response for control plane components
The value of a KBOM shows up on the day an advisory lands. Kubernetes gives you a structured starting point: the official CVE feed, a community maintained list of CVEs announced by the Kubernetes Security Response Committee, published as JSON and RSS. The page has carried a beta feature state since Kubernetes 1.27. When we fetched it on 23 October 2026, recent entries covered core components, kubectl, ingress-nginx and the CSI drivers for SMB and NFS. That mix is the argument for recording add-ons, not just the control plane.
A response workflow with a KBOM looks like this:
- Read the advisory's affected component and fixed versions. Each feed entry links to a GitHub issue with the details.
- Query the KBOM, not the clusters. If your KBOMs are stored per cluster per day, a
jqquery across them answers "which clusters run kube-apiserver below the fixed patch" or "which clusters have this CSI driver installed" in seconds, without touching production.
curl -Ls https://k8s.io/docs/reference/issues-security/official-cve-feed/index.json -o k8s-cves.json
jq -r '.items[] | [.id, .summary] | @tsv' k8s-cves.json | head
# Which clusters run a given add-on, by Helm chart name?
for f in kboms/*/kbom.json; do
jq -r --arg f "$f" '.components[] | select(.properties[]? | select(.name=="kbom:helm:chart" and (.value | startswith("csi-driver-smb")))) | [$f, .name, .version] | @tsv' "$f"
done
# Which nodes run a kubelet older than a fixed version?
jq -r '.components[] | select(.type=="operating-system") | [.name, (.properties[] | select(.name=="kbom:node:kubeletVersion") | .value)] | @tsv' kboms/prod-eu-1/kbom.json
- Plan the upgrade order from the skew policy. Upgrade kube-apiserver first, then the controller manager, scheduler and cloud controller manager, then kubelets after draining each node, then kube-proxy. The policy says kube-apiserver must not skip minor versions, even on single instance clusters, so a cluster several minors behind needs several steps. The KBOM tells you how far each cluster has to go.
- Record the outcome. When a component is present but not affected, for example because the vulnerable feature is disabled, write a VEX statement rather than a note in a ticket. Our VEX guide covers the status values and justifications.
- Diff after the fix. Regenerate the KBOM and diff it against the one taken before the change. The diff is the evidence that the patch reached every node.
For the wider playbook of finding affected products fast, see SBOM driven incident response. A KBOM is the cluster shaped version of the same idea.
If a cluster is on a minor version that has left support, the KBOM shows that no backport is coming, which turns a patch task into an upgrade project.
How admission control and image attestations complement a KBOM
A KBOM is a record. It tells you what was running when it was taken. It does not stop anything from running. Admission control is the other half: it checks each object as it is created and can refuse images that are unsigned, unattested or from an unexpected registry.
The pieces fit together like this:
| Question | Answered by | When |
|---|---|---|
| What is running in this cluster right now? | KBOM | On a schedule and after changes |
| Is this image signed by the publisher I expect? | Signature verification at admission, for example the Sigstore policy-controller | At deploy time |
| Was this image built by the pipeline I expect, from the source I expect? | SLSA provenance attestation, verified at admission | At deploy time |
| Does this image have an SBOM, and what is in it? | SBOM attestation attached to the image, plus the per image SBOM in the KBOM | At build time and deploy time |
| Which running images would fail today's policy? | KBOM compared against policy | On a schedule |
The Sigstore policy-controller is one open source option. Its documentation describes a ClusterImagePolicy resource that names the images it covers and the authorities that must have signed them, including keyless authorities, and an attestations block that can require, for example, a predicate of type https://slsa.dev/provenance/v1. By default it only enforces in namespaces labelled policy.sigstore.dev/include=true, which makes a staged rollout straightforward. The Kubernetes project itself suggests this controller for verifying signatures on non control plane images at deploy time.
Where the two meet: if admission policy requires an SBOM attestation on every image, the KBOM job can fetch those attestations instead of generating SBOMs, and any image without one shows up as a gap. Our post on whether Kubernetes admission control can require an SBOM attestation covers the enforcement options.
One caution. Admission control only sees objects created after the policy is in place. Pods that were already running before you turned enforcement on are not re-checked, and static pods are started by the kubelet from local files whatever the policy says. A KBOM is how you find them.
KBOM, SBOM, OBOM or SaaSBOM: which document answers what?
These documents overlap, which is why people reach for a new acronym. This decision table maps common questions to the document that answers them best.
| You need to know | Best document | Format |
|---|---|---|
| Which open source packages are inside an image | SBOM per image digest | CycloneDX or SPDX |
| Which OS packages and kernel a node runs | OBOM per node or per node image | CycloneDX (OBOM is a CycloneDX capability) |
| Which Kubernetes version, add-ons, CRDs and images a cluster runs | KBOM | CycloneDX with platform, container and operating-system components |
| Which external services and endpoints workloads call | SaaSBOM | CycloneDX services |
| Which known vulnerabilities affect a component, and which do not | VEX or VDR | CycloneDX VEX, CSAF VEX, OpenVEX, SPDX 3 security profile |
| How an image was built | Provenance attestation | SLSA provenance in an in-toto statement |
CycloneDX describes OBOM as a "full-stack inventory of runtime environments, configurations, and additional dependencies". A fair reading is that a KBOM is an OBOM scoped to one cluster, with per image SBOMs hanging off it. For the services slice, see what a SaaSBOM is and whether your service inventory needs one.
Who should own the KBOM
The platform team usually owns the KBOM job, the read only role, node image OBOMs and the control plane and add-on rows. Application teams own the per image SBOMs, ideally produced at build time and attached to each image as an attestation. The security team owns the queries: CVE feed matching, end of life checks and policy comparisons. Keep at least ninety days of history, because most of the value comes from diffs.
Where Safeguard fits
The fit here is indirect, so to be plain: Safeguard does not generate KBOMs, OBOMs or SaaSBOMs, and it does not take a live inventory of your clusters. The steps above use open source tools for that.
Where Safeguard helps is on the parts of the KBOM that are software. SBOM generation and analysis is generally available: Safeguard scans repositories and container images (safeguard scan --image <name>), reads existing SBOM files (safeguard scan --sbom <file>), and works with SPDX in JSON, XML and RDF and CycloneDX in JSON and XML, with more than 30 export formats including CSV, Excel and PDF. You can feed it the per image SBOMs from step 6, match them against vulnerability databases with reachability analysis, and see which projects contain a given component and version. CSAF and VEX upload is generally available too, so the VEX statements from your CVE response sit next to the findings they explain.
On the enforcement side, Safeguard's attestation and signing capability is generally available and covers SLSA, Sigstore keyless signing with Fulcio and Rekor, in-toto and X.509, with Kubernetes admission control. Policies and gates, also generally available, let you block or warn on images that fail a rule at build, CI or release. None of that replaces the KBOM job; it acts on what the KBOM finds.
Common mistakes
- Recording one version for the whole cluster. Skew is allowed and common. Record kubelet, kube-proxy where reported, kernel and runtime per node.
- Recording tags instead of digests. A tag can move between yesterday's KBOM and today's. Use
imageIDfrom container status. - Inlining every package. A KBOM with fifty thousand components is unreadable and undiffable. Link per image SBOMs and per node OBOMs by reference.
- Forgetting add-ons. CNI plugins, CSI drivers, DNS and ingress controllers appear in the official Kubernetes CVE feed. If the KBOM lists only the control plane, it misses them.
- Missing static pods and init containers. Static pods are started by the kubelet from files on the node and appear in the API only as mirror pods, and init containers are easy to skip in jsonpath queries. Check both.
- Treating the KBOM as enforcement. It records what is running. Pair it with admission control if you need to stop things.
- Giving the KBOM job broad credentials. It needs
getandlist. It does not need write access, secrets access or exec. - Assuming managed means covered. A managed control plane still has a version, a support calendar and advisories, and the node images are often yours to upgrade. Record them.
Fix it without waiting for upstream
A KBOM tells you which clusters, nodes and images carry a vulnerable component, but it does not ship the fixed build. The images and charts you build yourself still need someone to upgrade the dependency, rebuild, and roll the new digest out, and that is where most of the waiting happens.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
For Kubernetes specifically, that limit covers the control plane binaries and, on managed services, the node images: those fixes come from the Kubernetes project or your provider, and the KBOM is how you track that they arrived.
FAQ
What does KBOM stand for?
KBOM stands for Kubernetes bill of materials. It is an inventory of a Kubernetes cluster: the control plane version, node operating systems and kubelet versions, add-ons, custom resource definitions, Helm releases, and the container images running in it, ideally by digest.
Is KBOM an official standard?
No. The term comes from an open source project first released in April 2023 that described its own schema as a rough first draft. CycloneDX has no KBOM type and SPDX 3.0.1 has no Kubernetes profile. Most teams express a KBOM as a CycloneDX document using existing component types.
Does CycloneDX support a Kubernetes bill of materials?
Yes, through existing building blocks rather than a dedicated type. Use a platform component for the cluster and control plane, container components with pkg:oci purls for images, operating-system components for nodes, services for endpoints, and externalReferences of type bom to link per image SBOMs and node OBOMs. The CycloneDX OBOM capability covers runtime environment inventories of this kind.
What is the difference between a KBOM and an SBOM?
An SBOM lists the software components inside one artifact, such as a container image. A KBOM lists what a whole cluster runs: Kubernetes versions, nodes, add-ons and images. A good KBOM links to one SBOM per image digest instead of copying their contents.
How do I list every container image running in my Kubernetes cluster?
Use kubectl get pods -A -o jsonpath over spec.initContainers and spec.containers to get requested images, as the Kubernetes documentation shows. For the exact image each node pulled, read status.containerStatuses[].imageID and status.initContainerStatuses[].imageID with kubectl get pods -A -o json and jq.
Does Kubernetes publish its own SBOM?
Yes. Each release has a signed SPDX SBOM at https://sbom.k8s.io/<version>/release, with SHA-256 and SHA-512 checksums and a Sigstore signature and certificate. The v1.37.1 SBOM is SPDX 2.3 in tag value format. That SBOM describes the release artifacts, not your cluster, so it is an input to a KBOM rather than a replacement for one.
Can a KBOM cover a managed Kubernetes control plane?
Partly. You can record the control plane version from the API and the provider's support dates, and you can record node facts from status.nodeInfo. You usually cannot inventory the provider's control plane hosts, so note that boundary in the KBOM and rely on the provider's own advisories for it.
Can Safeguard generate a KBOM?
No. Safeguard does not generate KBOMs or OBOMs or take a live inventory of clusters. It generates and analyses SBOMs for repositories and container images, reads SBOM files you supply, accepts CSAF and VEX uploads, and provides attestation, signing and Kubernetes admission control, all generally available.
See how Safeguard would help
These demos show Safeguard on general supply chain work, not on a Kubernetes cluster. The per image SBOMs and fixes that follow a KBOM run through the same flows.
This first demo is a real CLI session: install, run safeguard to open the interactive terminal, and ask it to fix the lodash CVE in a repository. It reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, and pushes nothing. When a KBOM query points at an application image with a vulnerable dependency, this is the same loop for the repository that builds that image.
Click through it at your own pace, or open it full screen.
The second demo sets up the runner on your own machine, approves it with its device code, and then scans from the terminal. A runner on a machine inside your network is a natural place to scan the image digests from step 6 of the walkthrough.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. If your team already asks an assistant "which of our projects contain this component", this is how it gets read access to the answer.
Click through it at your own pace, or open it full screen.
To try it on your own images, download Safeguard and follow the CLI documentation.
Quick check
Your API server runs Kubernetes 1.37. Which kubelet minor versions are inside the supported skew?
1.37, 1.36, 1.35 and 1.34. The kubelet must not be newer than kube-apiserver and may be up to three minor versions older, which is why a KBOM records the kubelet version per node.
Which CycloneDX component type fits a container image in a KBOM, and how do you identify the exact image?
Use type: container with a pkg:oci package URL whose version is the image digest (with the colon encoded as %3A), and add the same SHA-256 value under hashes. Take the digest from the container's imageID in pod status, not from the tag.
Sources
- CycloneDX Operations Bill of Materials (OBOM) capability page
- CycloneDX Software Bill of Materials (SBOM) capability page
- CycloneDX 1.6 JSON schema, CycloneDX specification repository
- cdxgen README, CycloneDX GitHub organisation
- cdxgen project types (PROJECT_TYPES.md)
- cdxgen source, container specification handler (lib/cli/index.js)
- CycloneDX CLI README (merge, validate, diff)
- SPDX 3.0.1 ProfileIdentifierType vocabulary
- SPDX 3.0.1 SoftwarePurpose vocabulary
- Kubernetes Release History
- Kubernetes Version Skew Policy
- Kubernetes Patch Releases
- Kubernetes: Verify Signed Kubernetes Artifacts
- Kubernetes v1.37.1 release SBOM
- Kubernetes Official CVE Feed
- Kubernetes Node Status reference
- Kubernetes: List All Container Images Running in a Cluster
- Helm: helm list
- Helm: helm get manifest
- Sigstore policy-controller overview
- OpenSSF: Sigstore project page
- SLSA v1.1 provenance specification
- CISA Software Bill of Materials resources
Last reviewed: 23 October 2026. Kubernetes support dates and tool flags change often; check the linked documentation for the latest.