This is unreleased documentation for SBOM Scanner 0.14-dev.

Configuring Kubewarden SBOM Scanner for production

This page gives configuration for SBOM Scanner in a production cluster. It covers the replicas of each component, PostgreSQL, NATS, the node scan worker, and the other chart values that matter at scale.

For the full list of chart values, see Helm Values Configuration. For how the components work together, see Architecture. For the cluster requirements, see Compatibility.

Replicas

The chart sets controller.replicas, storage.replicas, and worker.replicas to 3 by default. Each component uses its replicas in a different way.

The controller runs with leader election. The leader reconciles the custom resources and runs the two scheduled-scan runners. Every replica serves the admission webhooks. The webhooks use failurePolicy: Fail, and they cover only the SBOM Scanner custom resources. When the controller is down, a change to a Registry, a ScanJob, or a scan configuration waits. Nothing else in the cluster waits on the controller.

The storage API server has no leader. Every replica serves any request, and each replica checks the database as part of its own readiness probe. When the database is unreachable, every storage replica turns not ready at the same time. See Storage API server and kubectl.

The worker is the throughput knob. Each worker pod processes one message at a time, from one shared queue. Raise worker.replicas to scan more images at once:

worker:
  replicas: 5

Scheduling: current limits of the chart

The chart has no tolerations, nodeSelector, affinity, or priorityClassName value for the controller, the storage API server, the worker, or the MCP server. It has no PodDisruptionBudget for any of them either. With three replicas of a component and no anti-affinity, every replica of that component can land on one node.

The node scan worker, covered next, has the same gap, and it also reuses worker.resources, so you cannot size it apart from the worker Deployment.

NATS and PostgreSQL are not affected. The NATS subchart ships its own PodDisruptionBudget. The CloudNativePG operator manages its own PodDisruptionBudgets for the database pods.

These are limits of the current chart, not a setting you can change today. They are going to be addressed in future releases.

Storage API server and kubectl

The storage API server is registered as a Kubernetes aggregated API, in the group storage.sbomscanner.kubewarden.io. When no storage replica is ready, for example during a database outage, that aggregated API turns unavailable. A request for one of its kinds, such as Image or VulnerabilityReport, fails. Requests for ordinary Kubernetes resources keep working.

The controller and the worker wait for the storage API at startup, so a storage outage also delays a fresh controller or worker pod. Keep storage.replicas at 3 or more, and keep the database reachable.

PostgreSQL

PostgreSQL holds every scan result: Image, SBOM, VulnerabilityReport, WorkloadScanReport, NodeSBOM, and NodeVulnerabilityReport. The Registry, the ScanJob, and the other configuration resources live in etcd and do not depend on it. If you lose the database, SBOM Scanner rebuilds the current reports the next time it scans. It cannot rebuild the history of past reports, or the SBOM of an image that a registry no longer carries.

By default, the chart deploys PostgreSQL with the CloudNativePG operator. storage.postgres.cnpg.instances defaults to 3. These extra instances give failover. They do not add write capacity: every instance writes through one primary. See Using CloudNativePG.

storage.postgres.cnpg.storage.size starts small. You can raise it later, and CloudNativePG resizes the existing volume. You cannot lower it. Raise the value with headroom before your first large scan. Set storage.postgres.cnpg.storage.storageClass to a storage class with the performance that your scan volume needs, rather than keep the cluster default.

You install and upgrade the CloudNativePG operator yourself. The chart only creates the Cluster resource that the operator reconciles.

Back up the database

The chart does not configure a backup on its own. Use storage.postgres.cnpg.merge to add a backup configuration to the CNPG Cluster resource. This value deep-merges into the Cluster spec, so you can set any field that the chart does not expose directly. A list in merge replaces the chart-managed list; it does not append to it.

This example adds the Barman Cloud plugin:

storage:
  postgres:
    cnpg:
      merge:
        plugins:
          - name: barman-cloud.cloudnative-pg.io
            isWALArchiver: true
            parameters:
              barmanObjectName: my-object-store
        backup:
          retentionPolicy: "30d"

Before you install this configuration, install the Barman Cloud plugin next to the CloudNativePG operator. Then create the ObjectStore resource that the plugin parameter names. After the install, create a ScheduledBackup that targets the Cluster named <release>-sbomscanner-cnpg-cluster. See the CloudNativePG backup documentation for the plugin and the ObjectStore and ScheduledBackup fields.

Use your own PostgreSQL

Turn off CloudNativePG for your own backup process, your own high-availability setup, or sizing that an in-cluster database cannot give you. Point SBOM Scanner at a PostgreSQL instance that you manage instead. See Using an external PostgreSQL instance.

NATS

The chart deploys NATS as a subchart, with JetStream on. By default it runs three pods, each with its own persistent volume on the cluster default storage class. NATS carries the scan messages between the controller and the workers, and the watch events between the storage replicas. The messages that a worker has not yet processed survive a worker or a controller restart. They stay on the NATS volume until a worker acknowledges them. The subchart creates its own PodDisruptionBudget.

The nats values block configures this subchart directly. Do not edit it by hand; a value set elsewhere in the chart already shapes it to work with SBOM Scanner.

Node scan worker

With controller.nodeScan.enabled at its default of true, the chart deploys a second worker as a DaemonSet, one pod on every node. This pod reads the root file system of its node to produce a NodeSBOM and a NodeVulnerabilityReport.

Reading every file on the node needs either the UID 0 or the Linux capability DAC_READ_SEARCH. Both the baseline and the restricted Pod Security Admission levels forbid that capability, so the pod needs UID 0 regardless. The pod mounts the root file system read-only, and it adds no other capability. See Pod Security Admission for the full reasoning.

If you do not need node scanning, turn it off. The DaemonSet is removed, and every remaining pod in the release meets the restricted level:

controller:
  nodeScan:
    enabled: false

See Disabling node scanning for the difference between this value and disabling scans through the NodeScanConfiguration resource.

Admission policies and Pod Security Admission

What an admission policy accepts depends on whether node scanning is on.

With controller.nodeScan.enabled: false, the controller, the storage API server, the worker, the MCP server, and NATS all run as non-root and drop every capability. None of them use a host namespace or a hostPath volume. They meet the restricted Pod Security Admission level, and no Kubewarden Admission Controller recommended policy rejects them.

With node scanning on, the node scan worker fails both the baseline and the restricted level. In protect mode, three of the Admission Controller recommended policies reject it:

Policy Rejects the node scan worker because

do-not-share-host-paths

The root file system, /, is not under /tmp.

do-not-run-as-root

runAsUser is 0 and runAsNonRoot is false.

drop-capabilities

It adds DAC_READ_SEARCH, and the allowed list of the policy is empty.

Exclude the SBOM Scanner namespace from these policies, with the recommendedPolicies.skipAdditionalNamespaces value of the Admission Controller Helm chart. The default exclusion list of that chart already contains cattle-sbomscanner-system. If you install SBOM Scanner into a namespace with another name, such as sbomscanner from the quick start, add that name:

recommendedPolicies:
  skipAdditionalNamespaces:
    - sbomscanner

Keep the namespace at the privileged Pod Security Admission level, or use audit or warn mode only.

See Security hardening for the full list of recommended policies, and Pod Security Admission for the three levels.

Telemetry

SBOM Scanner has no bundled OpenTelemetry collector. Set otel.endpoint to the OTLP/gRPC endpoint of a collector that you already operate. The export stays off while this value is empty:

otel:
  endpoint: "https://otel-collector.observability.svc.cluster.local:4317"
  caSecretName: "sbomscanner-otel-ca"
  clientCertificateSecretName: "sbomscanner-otel-client"

Both Secrets must exist in the SBOM Scanner namespace before you set these values. See OpenTelemetry for what each value does, and OpenTelemetry for the full setup, including the exported traces and metrics. The MCP server does not export telemetry.

Scan scope and intervals

The chart has no value that limits the scan scope or the scan frequency. Each lives in a custom resource instead:

  • Registry.spec.scanInterval sets how often the registry runner rescans a Registry. Leave it unset to scan only on demand. See Scanning Registries.

  • WorkloadScanConfiguration.spec.namespaceSelector and .scanInterval set which namespaces the workload scan covers, and how often it rescans them. See Scanning Workloads.

  • NodeScanConfiguration.spec.nodeSelector, .platforms, and .scanInterval set which nodes the node scan covers, and how often. See Scanning Nodes.

A validating webhook rejects a scanInterval under one minute, on every one of these resources. SBOM Scanner runs one ScanJob per Registry at a time; split a large registry into several Registry resources to scan it in parallel.

Turn off a feature that you do not use, so that the controller does not reconcile it:

controller:
  workloadScan:
    enabled: false
  nodeScan:
    enabled: false

Air-gapped clusters

A production cluster without internet access needs its own sources for the vulnerability databases, the PostgreSQL operand image, and the default VEX feeds:

worker:
  trivyDBRepository: "my-registry.example.com/aquasecurity/trivy-db"
  trivyJavaDBRepository: "my-registry.example.com/aquasecurity/trivy-java-db"
controller:
  defaultVEXHubResources: false
storage:
  postgres:
    cnpg:
      imageName: "my-registry.example.com/postgresql:18.1"

See Air Gap Support for the full setup, including how to self-host a VEX Hub.

Example production configuration

This example combines the configuration from this page into one Helm values file:

worker:
  replicas: 5

storage:
  postgres:
    cnpg:
      storage:
        storageClass: fast-ssd
        size: 20Gi
      merge:
        plugins:
          - name: barman-cloud.cloudnative-pg.io
            isWALArchiver: true
            parameters:
              barmanObjectName: my-object-store
        backup:
          retentionPolicy: "30d"

otel:
  endpoint: "https://otel-collector.observability.svc.cluster.local:4317"
  caSecretName: "sbomscanner-otel-ca"
  clientCertificateSecretName: "sbomscanner-otel-client"

controller:
  nodeScan:
    enabled: true
mcp:
  enabled: false

Apply the file at install or upgrade time:

helm upgrade --install sbomscanner kubewarden/sbomscanner \
  --namespace sbomscanner \
  --create-namespace \
  -f production-values.yaml \
  --wait