Profile
Back to NewsBack
GitHub Trending 13 min
Reader Mode
ckotzbauer/sbom-operator: Catalogue all images of a Kubernetes cluster to multiple targets with Syft

ckotzbauer/sbom-operator: Catalogue all images of a Kubernetes cluster to multiple targets with Syft

sbom-operator

Catalogue all images of a Kubernetes cluster to multiple targets with Syft.

test</a>

Overview

This operator maintains a central place to track all packages and software used in all those images in a Kubernetes cluster. For this a Software Bill of Materials (SBOM) is generated from each image with Syft. They are all stored in one or more targets. Currently Git, Dependency Track, OCI-Registry and ConfigMaps are supported. With this it is possible to do further analysis, vulnerability scans and much more in a single place. To prevent scans of images that have already been analyzed pods are annotated with the imageID of the already processed image.

Kubernetes Compatibility

The image contains versions of k8s.io/client-go. Kubernetes aims to provide forwards & backwards compatibility of one minor version between client and server:

| sbom-operator | k8s.io/{api,apimachinery,client-go} | expected kubernetes compatibility | |-----------------|-------------------------------------|-----------------------------------| | main | v0.34.1 | 1.33.x, 1.34.x, 1.35.x | | 0.38.0 | v0.34.1 | 1.33.x, 1.34.x, 1.35.x | | 0.33.0 | v0.30.0 | 1.29.x, 1.30.x, 1.31.x | | 0.31.0 | v0.29.3 | 1.28.x, 1.29.x, 1.30.x | | 0.30.0 | v0.28.4 | 1.27.x, 1.28.x, 1.29.x | | 0.28.0 | v0.27.4 | 1.26.x, 1.27.x, 1.28.x | | 0.26.0 | v0.26.3 | 1.25.x, 1.26.x, 1.27.x | | 0.22.0 | v0.25.4 | 1.24.x, 1.25.x, 1.26.x | | 0.15.0 | v0.24.4 | 1.23.x, 1.24.x, 1.25.x | | 0.10.0 | v0.23.6 | 1.22.x, 1.23.x, 1.24.x |

However, the operator will work with more versions of Kubernetes in general.

Container Registry Support

The operator relies on the syft-internal mechanism to download images from OCI-compliant registries.

Installation

Manifests

kubectl apply -f deploy/standard/

Helm-Chart

Create a YAML file first with the required configurations or use helm-flags instead.

helm repo add ckotzbauer https://ckotzbauer.github.io/helm-charts
helm install ckotzbauer/sbom-operator -f your-values.yaml

Configuration

All parameters are cli-flags. The flags can be configured as args or as environment-variables prefixed with SBOM_ to inject sensitive configs as secret values.

Common parameters

| Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | verbosity | false | info | Log-level (debug, info, warn, error, fatal, panic) | | cron | false | "" | Backround-Service interval (CRON). See Trigger for details. | | ignore-annotations | false | false | Force analyzing of all images, including those from annotated pods. | | format | false | json | SBOM-Format. (One of json, syftjson, cyclonedxjson, spdxjson, github, githubjson, cyclonedx, cyclone, cyclonedxxml, spdx, spdxtv, spdxtagvalue, text, table) | | format-version | false | (see below) | SBOM-Format-Version. Overrides the Syft encoder specification version for version-aware families (cyclonedxjson, cyclonedxxml, spdxjson, spdxtagvalue). Flag can be set via SBOM_FORMAT_VERSION env var or formatVersion in YAML. See Format Versions for the complete supported-matrix. | | targets | false | git | Comma-delimited list of targets to sent the generated SBOMs to. Possible targets git, dtrack, oci, configmap. Ignored with a job-image | | pod-label-selector | false | "" | Kubernetes Label-Selector for pods. | | namespace-label-selector | false | "" | Kubernetes Label-Selector for namespaces. | | fallback-pull-secret | false | "" | Kubernetes Pull-Secret Name to load as a fallback when all others fail (must be in the same namespace as the sbom-operator) | | registry-proxy | false | [] | Proxy-Registry-Hosts to use. Flag can be used multiple times. Value-Mapping e.g. docker.io=ghcr.io | | delete-orphan-images | false | true | Delete orphan images automatically | | strip-cpes | false | false | Strip CPE attributes from all components in generated SBOMs (Syft-JSON cpes, CycloneDX cpe). Disables Syft CPE generation and removes cataloger-sourced CPEs. Flag can be set via SBOM_STRIP_CPES env var or stripCpes in YAML. |

Format Versions

The --format-version flag (env: SBOM_FORMAT_VERSION, YAML: formatVersion) pins the CycloneDX or SPDX specification version used by the embedded Syft encoder. It only applies when --format selects a version-aware family; for non-version-aware formats (json, syftjson, text, table, github) the flag is silently accepted and Syft defaults are used.

Family / Alias Matrix

| Family | Canonical alias | Aliases | Version-aware? | Default source | |--------|----------------|---------|----------------|----------------| | CycloneDX JSON | cyclonedxjson | — | Yes | Bundled Syft | | CycloneDX XML | cyclonedxxml | cyclonedx, cyclone | Yes | Bundled Syft | | SPDX JSON | spdxjson | — | Yes | Bundled Syft | | SPDX Tag-Value | spdxtagvalue | spdx, spdxtv | Yes | Bundled Syft | | Syft JSON | json, syftjson | — | No | — | | Plain output | text, table | — | No | — | | GitHub | github, githubjson | — | No | — |

Exact supported revisions and defaults come from the Syft version bundled in each operator release. Run sbom-operator --help to see the authoritative values for the installed binary.

Usage Examples

CLI flag (explicit CycloneDX 1.3 JSON):

sbom-operator --format=cyclonedxjson --format-version=1.3

Environment variable (SPDX 2.3):

export SBOM_FORMAT_VERSION=2.3
sbom-operator --format=spdxjson

Helm YAML:

args:
  format: cyclonedxxml
  formatVersion: "1.5"

Invalid Combinations

Starting the operator with a version not supported by the selected family produces a hard Fatal error before any Kubernetes runtime initialization. The error includes the requested family, rejected version, and the versions supported by the bundled Syft release.

Unknown Format Fallback

An unknown format name (not in the alias matrix) with an omitted or whitespace-only format-version delegates to Syft's default JSON encoder, preserving backward compatibility with custom format strings:

sbom-operator --format=custom-format --format-version=""

When an unknown format is paired with a version-like string (e.g. --format-version=1.2), the version is resolved to its family (CycloneDX 1.2 → cyclonedxjson) and the operator logs the concrete effective version at startup.

Example Helm-Config

args:
  targets: git
  git-author-email: XXX
  git-author-name: XXX
  git-repository: https://github.com/XXX/XXX
  git-path: dev-cluster/sboms
  verbosity: debug
  cron: "0 30    "

envVars: - name: SBOM_GIT_ACCESS_TOKEN valueFrom: secretKeyRef: name: "sbom-operator" key: "accessToken"

Analysis-Trigger

Cron

With the cron flag set, the operator runs with a specified interval and checks for changed images in your cluster. All options from github.com/robfig/cron are allowed as cron-syntax.

Real-Time

When you omit the cron flag, the operator uses a Cache-Informer to process changed pods immediately. In this mode there's also a one-time analysis at startup to sync the targets with the actual cluster-state. If you configured a job-image there's no initial startup sync.

AWS ECR (IRSA / Pod Identity)

When images are hosted in private AWS Elastic Container Registry (.dkr.ecr..amazonaws.com) and no pull secrets are configured on the pod, sbom-operator automatically obtains a registry token via the AWS SDK default credentials chain. IRSA, EKS Pod Identity, and EC2 instance roles are all supported transparently. The required IAM permission for the operator's role is ecr:GetAuthorizationToken. For successful image layer pulls, the role also needs ecr:BatchGetImage and ecr:GetDownloadUrlForLayer on the relevant repositories.

ECR Public (public.ecr.aws/*) and cross-account ECR via assume-role are not supported by this auto-detection. Use --fallback-pull-secret for those scenarios.

Targets

It is possible to store the generated SBOMs to different targets (even multple at once). All targets are using Syft as analyzer. If you want to use another tool to analyze your images, then have a look at the Job image section. Images which are not present in the cluster anymore are removed from the configured targets (except for the OCI-Target).

Dependency Track

Dependency Track Parameter

| Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | dtrack-base-url | true when dtrack target is used | "" | Dependency-Track base URL, e.g. 'https://dtrack.example.com' | | dtrack-api-key | true when dtrack target is used | "" | Dependency-Track API key | | dtrack-label-tag-matcher | false | "" | Dependency-Track Pod-Label-Tag matcher regex | | dtrack-ca-cert-file | false | "" | CA-Certificate filepath when using mTLS to connect to dtrack | | dtrack-client-cert-file | true when dtrack-ca-cert-file is provided | "" | Client-Certificate filepath when using mTLS to connect to dtrack | | dtrack-client-key-file | true when dtrack-ca-cert-file is provided | "" | Client-Key filepath when using mTLS to connect to dtrack | | dtrack-default-parent-project | false | "" | The default parent project UUID that should be used. This is overwritten in case a parent project annotation is set and found | | dtrack-use-short-name | false | "" | Use short image name (without registry) for project names | | dtrack-kubernetes-cluster-id-mode| false| "tag" | Mode for using the kubernetes-cluster-id. Options: tag (adds cluster ID as a project tag) or prefix (prefixes the project name with the cluster ID). | | dtrack-parent-project-annotation-key | false | "" | Kubernetes pod annotation key to set parent project automatically, e.g. "my.pod.annotation" | | dtrack-project-name-annotation-key | false | "" | Kubernetes pod annotation key to set custom dtrack project name automatically, e.g. "my.pod.annotation" | | dtrack-manage-project-active-status | true when enabled without parent config | false | Manage project active/isLatest status based on running pods. Orphans are deactivated instead of deleted. Requires a parent project. | | kubernetes-cluster-id | false | "default" | Kubernetes Cluster ID (to be used in Dependency-Track or Job-Images) |

Each image in the cluster is created as project with the full-image name (registry and image-path without tag) and the image-tag as project-version. When there's no image-tag, but a digest, the digest is used as project-version. The autoCreate option of DT is used. You have to set the --format flag to cyclonedx with this target.

[!IMPORTANT]
Dependency-Track 5.0.x rejects CycloneDX 1.7, which is the default emitted by the Syft version currently bundled with sbom-operator. Configure --format-version=1.6 (or SBOM_FORMAT_VERSION=1.6 / formatVersion: "1.6") when using the dtrack target. If the version is omitted, the operator logs a startup warning but preserves Syft's default.

Custom dtrack project name:

The key at kubernetes has to be suffixed with the container name the project is for. e.g. my.project.name/my-nginx.

[!IMPORTANT]
The suffix regarding container name must not be added to the config value and must not include /. e.g. my.project.name

The value for a custom project name in dtrack by annotation at the specific Pod is written in the format of project:version or just project where version defaults to latest. E.g. MyParentProject or MyParentProject:1.0


Setting parent project at Dependency Track automatically:

The key at kubernetes has to be suffixed with the container name the parent project is for. e.g. my.parent.project/my-nginx. The value for the parent project annotation at the specific Pod is written in the format of project:version or just project. The version is optional: collection projects (parents that only aggregate children) usually do not carry a version, so a bare name like MyParentProject is sufficient. If a version is provided, e.g. MyParentProject:1.0, the parent project is matched by name and version.

[!IMPORTANT]
The suffix regarding container name must not be added to the config value and must not include /. e.g. my.parent.project

Project active/inactive lifecycle management

When --dtrack-manage-project-active-status is enabled, the operator manages project active/isLatest state in Dependency Track:

  • Running version: set Active=true, IsLatest=true
  • Sibling versions under the same parent (matched by project name): set Active=false, IsLatest=false
  • Orphaned projects (no longer running in any cluster): deactivated instead of deleted, preserving version history. The global --delete-orphan-images flag is bypassed for dtrack when this is active.
  • Rollback: when a pod rolls back to a previously deployed image with a matching digest, the inactive project is reactivated without re-uploading the BOM.
A parent project is required — either --dtrack-parent-project-annotation-key or --dtrack-default-parent-project. In multi-cluster tag mode, the current cluster's tag is removed first and the project is only deactivated if no other clusters are still using it.

Requires Dependency Track v4.12.0+ for the isLatest field.


Example Pod Annotation:

apiVersion: v1
kind: Pod
metadata:
  annotations:
    my.parent.project/my-nginx: MyParentProject
    my.project.name/my-nginx: MyNginxProject:1.0
    my.parent.project/my-sidecar: MyOtherParentProject
    my.project.name/my-sidecar: MySidecarProject:1.0.1
spec:
  containers:
    - image: nginx:latest
      name: my-nginx
    ...
    - image: some-other-image:latest
      name: my-sidecar
    ...
...

sbom-operator config:

--dtrack-parent-project-annotation-key=my.parent.project
--dtrack-project-name-annotation-key=my.project.name

Git

Git Parameter

| Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | git-workingtree | false | /work | Directory to place the git-repo. | | git-repository | true when git target is used. | "" | Git-Repository-URL (HTTPS). | | git-branch | false | main | Git-Branch to checkout. | | git-path | false | "" | Folder-Path inside the Git-Repository. | | git-author-name | true when git target is used. | "" | Author name to use for Git-Commits. | | git-author-email | true when git target is used. | "" | Author email to use for Git-Commits. | | git-access-token | false | "" | Git-Personal-Access-Token with write-permissions. | | git-username | false | "" | Git-Username | | git-password | false | "" | Git-Password | | github-app-id | false | "" | GitHub App-ID. | | github-app-installation-id | false | "" | GitHub App-Installation-ID. |

The operator will save all files with a specific folder structure as described below. When a git-path is configured, all folders above this path are not touched from the application. Assuming that git-path is set to dev-cluster/sboms. When no git-path is given, the structure below is directly in the repository-root. The structure is basically ////sbom.json (see example below). The file-extension may differ when another output-format is configured. You can use a token-based authentication (e.g. a PAT for GitHub) with --git-access-token, BasicAuth with username and password (--git-username, --git-password) or Github App Authentication (--github-app-id, --github-app-installation-id, env: SBOM_GITHUB_APP_PRIVATE_KEY) The private-key has to be Base64 encoded.

Note: It is required, that the specified branch of the repo is fully initialized. There's no logic which creates a non-existent branch. Just commit a README.md or something similar, to make things work.

dev-cluster
│
└───sboms
    │
    └───docker.io
    |   │
    |   └───library
    |       │
    |       └───busybox
    |           │
    |           └───sha256_ae39a6f5...
    |               │   sbom.json
    |
    └───ghcr.io
        │
        └───kyverno
            │
            └───kyverno
            |   │
            |   └───sha256_9e3f14e5...
            |       │   sbom.json
            |
            └───kyvernopre
                │
                └───sha256_e48f87fd...
                    │   sbom.json
            |
            └───policy-reporter
                │
                └───sha256_b70caa7a...
                    │   sbom.json

OCI-Registry

OCI-Registry Parameter

| Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | oci-registry | true when oci target is used | "" | OCI-Registry | | oci-user | true when oci target is used | "" | OCI-User | | oci-token | true when oci target is used | "" | OCI-Token |

In this mode the operator will generate a SBOM and store it into an OCI-Registry. The SBOM then can be processed by cosign, Kyverno or any other tool. E.g.:

COSIGN_REPOSITORY=<yourregistry> cosign download sbom <your full image digest>

The operator needs the Registry-URL, a user and a token as password to authenticate to the registry. Write-permissions are needed.

ConfigMap

This target stores the SBOM as Kubernetes-ConfigMap. They are placed in the same namespace as the corresponding pod and the name consists of the pod- and container-name. The configmap is labeled with ckotzbauer.sbom-operator.io=true and annotated with ckotzbauer.sbom-operator.io/image-id=. The content is stored as brotli-compressed binary-data with the configmap-key sbom.

Job-Images

Job-Image Parameter

| Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | job-image | false | "" | Job-Image to process images with instead of Syft | | job-image-pull-secret | false | "" | Pre-existing pull-secret-name for private job-images | | job-timeout | false | 3600 | Job-Timeout in seconds (activeDeadlineSeconds) | | kubernetes-cluster-id | false | "default" | Kubernetes Cluster ID (to be used in Dependency-Track or Job-Images) |

If you don't want to use Syft to analyze your images, you can give the Job-Image feature a try. The operator creates a Kubernetes-Job which does the analysis with any possible tool inside. There's no target-handling done by the operator, the tool from the job has to process the SBOMs on its own. Currently there are two possible integrations:

| Tool | Description | | ---- | ----------- | | Codenotary VCN | The VCN-Tool from Codenotary can notarize your images in the Codenotary Cloud. (chargeable) |

This feature is built as generic approach. Any image which follows these specs can be used as job-image.

e.g. Manifest (deploy/job-image):

--job-image=ghcr.io/ckotzbauer/sbom-operator/vcn:<TAG>

e.g. Helm:

jobImageMode: true

envVars: - name: SBOM_JOB_VCN_LC_API_KEY value: "<KEY>"

All operator-environment variables prefixed with SBOM_JOB_ are passed to the Kubernetes job.

Security

The docker-image is based on a scratch-image to reduce the attack-surface and keep the image small. Furthermore the image and release-artifacts are signed with cosign and attested with provenance-files. The release-process satisfies SLSA Level 2. All of those "metadata files" are also stored in a dedicated repository ghcr.io/ckotzbauer/sbom-operator-metadata. When discovering security issues please refer to the Security process.

Signature verification

COSIGN_EXPERIMENTAL=1 COSIGN_REPOSITORY=ghcr.io/ckotzbauer/sbom-operator-metadata cosign verify ghcr.io/ckotzbauer/sbom-operator:<tag-to-verify> --certificate-github-workflow-name create-release --certificate-github-workflow-repository ckotzbauer/sbom-operator

Attestation verification

COSIGN_EXPERIMENTAL=1 COSIGN_REPOSITORY=ghcr.io/ckotzbauer/sbom-operator-metadata cosign verify-attestation ghcr.io/ckotzbauer/sbom-operator:<tag-to-verify> --certificate-github-workflow-name create-release --certificate-github-workflow-repository ckotzbauer/sbom-operator

Download attestation

COSIGN_REPOSITORY=ghcr.io/ckotzbauer/sbom-operator-metadata cosign download attestation ghcr.io/ckotzbauer/sbom-operator:<tag-to-verify> | jq -r '.payload' | base64 -d

Download SBOM

COSIGN_REPOSITORY=ghcr.io/ckotzbauer/sbom-operator-metadata cosign download sbom ghcr.io/ckotzbauer/sbom-operator:<tag-to-verify> | jq -r '.payload' | base64 -d

License -------- Changelog --------

Contributing

Please refer to the Contribution guildelines.

Code of conduct

Please refer to the Conduct guildelines.

Chat with me