Bill of Delivery — oder: Wessen Problem ist das eigentlich?

Eine SBOM beschreibt, was in einem Container steckt. Aber welche Container in welchen Versionen ein Release ausmachen — das ist eine andere Frage. Über Composition, das Open Component Model, NixOS+Podman und die Frage, warum die Bill-of-Delivery-Diskussion stark aus dem K8s-Ökosystem heraus geprägt ist.

Vor wenigen Tagen ist auf appetizers.io ein Artikel von Matthias Bruns erschienen — Software Bills of Delivery: Beyond SBOMs with Component Models —, der das Open Component Model (OCM) als nächste Evolutionsstufe nach klassischen SBOMs positioniert. Der Post ist lesenswert, weil er ein reales Problem adressiert: Eine SBOM beschreibt, was in einem einzelnen Container steckt, sagt aber nichts darüber, welche Container als Set zu einem Release gehören.

Ich komme bei der vorgeschlagenen Lösung zu einer anderen Schlussfolgerung als der Autor — weniger, weil ich OCM für falsch halte, sondern weil ich die Frage anders stelle: nicht „wie löse ich das Problem“, sondern „wo habe ich es überhaupt?“. Mein Eindruck ist, dass die ganze Bill-of-Delivery-Diskussion stark aus dem K8s-Ökosystem heraus geprägt ist und sich verändert, sobald man eine Schicht tiefer schaut.

Diesen Punkt finde ich interessant genug für einen Post, weil er etwas Größeres beleuchtet: Wie viele unserer „DevSecOps-Tooling-Notwendigkeiten“ sind eigentlich Antworten auf Probleme, die wir uns durch unsere Architekturwahl selbst geschaffen haben?

High-Level: Worum geht's überhaupt?

Stell dir vor, du lieferst Software aus. Diese Software besteht aus mehreren Teilen — eine Anwendung, ein Reverse-Proxy davor, vielleicht ein Scheduler, ein Key-Value-Store, ein paar Konfigurationsdateien. Beim Audit oder beim Sicherheitsvorfall willst du beantworten können:

  1. Was steckt in jedem einzelnen Teil? Das ist die klassische SBOM — Software Bill of Materials. Liste aller Bibliotheken, Lizenzen, Versionen.
  2. Wer hat es wie gebaut? Das ist Provenance, Stichwort SLSA. Welcher Commit, welcher Runner, welche Inputs.
  3. Welche Teile in welcher Version gehören zu Release 2.4.0, atomar als Set? Das ist die Composition — und genau hier setzt die Bill-of-Delivery-Diskussion an.

Punkt 3 ist der spannende. Eine SBOM beschreibt, was in einem Container steckt. Sie sagt aber nichts darüber, welche Container in welchen Versionen ein bestimmtes Release ausmachen. Genau diese Lücke versuchen Konzepte wie das Open Component Model (OCM) zu schließen.

Nur: Diese Lücke existiert nicht überall gleich. Sie ist primär eine Folge der Designentscheidungen von K8s — kein Bug, sondern Konsequenz aus den Anforderungen, für die K8s optimiert wurde: dynamische Systeme, unabhängig versionierte Deployments, dezentrale Composition als bewusst gewählte Architektur.

Warum K8s die Composition-Lücke strukturell hat

Ein typischer K8s-Deploy ist konzeptionell zerrissen — und das aus guten Gründen. K8s ist für dynamische Systeme mit unabhängig versionierten Komponenten gebaut; die Komposition ist bewusst dezentral angelegt, damit sich Operator-Lifecycles, Service-Updates und Cluster-Plattform-Bestandteile nicht synchron bewegen müssen.

Konkret bedeutet das:

Es gibt schlicht keinen einzelnen Ort, der verbindlich sagt: Das ist Release X, hier sind alle seine Bestandteile mit ihren Digests, signiert als atomare Einheit. Helm versucht das, deckt aber nur seine Subcharts ab. Flux/Argo arbeiten auf GitOps-Ebene, nicht auf Artefakt-Ebene. Tags driften, ImagePullPolicy variiert, Operatoren werden außerhalb der App-Releases versioniert.

Aus dieser bewussten Dezentralisierung entsteht ein echter Bedarf nach einem Bundle-Layer, der „all das atomar zusammenbindet“, wenn man Releases als geschlossene Einheiten ausrollen oder zwischen Umgebungen promoten will. OCM positioniert sich genau dort. Es ist mehr als nur ein Transport-Format — es definiert einen Descriptor und ein Referenzmodell dafür, wie Komponenten ihre Artefakte, Provenance und Cross-Component-Referenzen ausdrücken. Konzeptionell sauber gemacht. Der Bedarf ist legitim, die Lösung trägt.

Was NixOS + Podman strukturell anders macht

Wir kennen beide Welten. Bei Kunden betreiben wir K8s-Cluster und arbeiten dort in genau der Tooling-Landschaft, die ich oben skizziert habe — die Composition-Fragmentierung ist für uns kein theoretisches Problem, sondern Alltagsgeschäft. Für unsere bei deutschen Hostern betriebene Hosting-Infrastruktur haben wir uns dagegen bewusst für mehrere NixOS-VPS mit Podman entschieden. Das ist keine Wertung der Plattformen, sondern eine andere Architekturwahl mit anderen Tradeoffs. Was dabei interessant ist: Was im K8s-Kontext als separates Tooling-Problem auftaucht, ist im NixOS-Kontext Nebeneffekt der Konfiguration.

Eine NixOS-Container-Definition sieht in etwa so aus:

 

virtualisation.oci-containers.containers.app = {
  image = "registry.example.com/app@sha256:abc123...";
  ports = [ "127.0.0.1:8080:8080" ];
  environment = { ... };
  dependsOn = [ "kv-store" "scheduler" ];
};

virtualisation.oci-containers.containers.proxy = {
  image = "registry.example.com/proxy@sha256:def456...";
  # ...
};

 

Drei Eigenschaften fallen direkt ins Auge:

Digest-Pinning ist Default. Du kannst Tags nutzen, aber für Production schreibst du Digests. Tag-Drift ist damit konstruktiv ausgeschlossen, nicht durch Disziplin oder einen Renovate-Bot.

Composition lebt in derselben Datei wie alles andere. Konfiguration, Firewall-Regeln, systemd-Units, Reverse-Proxy, Backup-Timer, TLS-Zertifikate — alles in einer Sprache, einem Evaluation-Schritt. Kein „abstimmen zwischen drei Tools“.

nixos-rebuild ist atomar. Entweder die ganze neue Generation aktiviert sich, oder die alte bleibt. Rollback ist ein Boot-Menü-Eintrag, keine Hilfekonstruktion mit Sternchen.

Mit einer Einschränkung, die fair zu nennen ist: Nix beschreibt den gewünschten Zustand, nicht den tatsächlichen Runtime-Zustand. Externe Abhängigkeiten — SaaS-APIs, drittpartei-gemanagte Services, Datenbanken außerhalb der Konfiguration — fallen nicht ins Modell. Was Nix gibt, ist eine sehr saubere, deterministische Beschreibung der Build- und Deploy-Schicht. Die Runtime-Wahrheit muss man weiterhin über Observability erschließen — das gilt aber für jeden Stack.

In unserem Kontext ist das Tupel aus Release-Modul (mit der Container-Set-Konfiguration) und flake.lock, in Git versioniert und signiert, die vollständigere Form der Composition — weil es zugleich die Deployment-Wahrheit ist und nicht nur ihre Beschreibung. Was OCM zusätzlich anbietet — Cross-Registry-Distribution, Airgap-Transport, Multi-Cluster-Promotion — brauchen wir an dieser Stelle nicht. In einem K8s-Kontext mit verteilten Targets würde die Rechnung anders ausgehen.

Was du trotzdem brauchst — egal ob K8s oder NixOS

Hier wird oft falsch abstrahiert: „Wenn ich Nix habe, brauche ich kein DevSecOps mehr.“ Falsch. Nix löst die Composition-Schicht. Die anderen Schichten bleiben unverändert relevant.

SLSA-Provenance pro Image

Pro Container-Build erzeugt deine Pipeline ein in-toto-Statement vom Predicate-Type slsa.dev/provenance/v1. Es dokumentiert:

Wichtig: Das Base-Image gehört in diese Inputs — als Digest, nicht als Tag. Damit entsteht eine Verifikationskette, die du im Audit zurückverfolgen kannst:

 

App-Provenance ─ baseImage: php-base@sha256:xyz
                                       │
                                       └── eigene Provenance ─ baseImage: wolfi-base@sha256:...
                                                                              │
                                                                              └── ...

 

Anhängen ans Image via cosign attest --type slsaprovenance. Das ist die untere Schicht der Lieferkette, und sie ist orthogonal zu deinem Deployment-Modell.

Image-Signing mit Cosign

Bei selbst-gehostetem GitLab ist keyless Sigstore aktuell keine Option (der Sigstore-Public-Good-Trust-Root erwartet eine OIDC-Identität, die für gitlab.com sauber funktioniert, für eigene Instanzen aber nicht ohne weiteres Trust etabliert). Also key-based Signing mit kurzlebigen Build-Keys aus Vault. Jedes Image, das in die Production-Registry geht, ist signiert.

OpenVEX für Vulnerability-Hygiene

Wolfi-Images sind extrem schlank, aber Trivy/Grype melden trotzdem regelmäßig CVEs in transitiven Libraries, deren verwundbare Code-Pfade gar nicht erreicht werden. Statt das im Confluence-Doc oder im Ticket-System zu dokumentieren, schreibst du OpenVEX-Statements: signiert, maschinenlesbar, im OCI-Registry als Attestation neben dem Image. Pro CVE eine klare Aussage — affected, not_affected mit Justification, fixed, under_investigation.

Das reduziert Audit-Lärm massiv und ist genau der Mechanismus, der bei reiner SBOM-Prüfung fehlt. Für ISO-27001 oder TISAX ist das gold wert, weil ein Auditor nicht mehr fragen muss „warum ist diese High-CVE seit drei Wochen offen“, sondern liest „nicht betroffen, weil X“ mit Signatur und Datum.

System-SBOM aus dem Nix-Closure

Eine Nix-Derivation ist faktisch eine sehr präzise Komponentenbeschreibung — content-addressed, deterministisch, mit vollständigem Closure-Graph. Tools wie genealogos exportieren das nach CycloneDX. Für unsere NixOS-Hosts heißt das: System-SBOM fällt fast als Nebenprodukt ab — und die Reproduzierbarkeit ist echt, nicht behauptet.

Mit einer Einschränkung: Eine Nix-Closure-SBOM ist nicht eins-zu-eins gleichwertig zu einer klassisch gepflegten SBOM. Lizenz-Metadaten, Maintainer-Felder und einige andere Standard-Attribute kommen nicht automatisch mit; je nach Audit-Anforderung muss man sie aus den Nix-Paketdefinitionen ergänzen oder über zusätzliche Tools nachreichen. Das ist machbar, aber kein Selbstläufer.

Verify-on-Deploy

Der wichtigste Schritt, der oft vergessen wird: Was nicht verifiziert wird, ist nicht signiert. Im NixOS-Stack bietet sich dafür der nixos-rebuild-Pfad an — ein cosign verify als Build-Schritt sorgt dafür, dass eine Konfiguration, die ein Image referenziert, das nicht gegen den Signaturschlüssel validiert, beim Build fehlschlägt. Production-Hosts bekommen damit nur Configs, die diesen Verify-Schritt bereits bestanden haben.

Im K8s-Kontext liegt die äquivalente Stelle bei Kyverno oder dem Sigstore Policy Controller — Admission Webhooks, die unsignierte Images beim Scheduling abweisen. Mechanisch unterschiedlich, semantisch identisch: Was nicht signiert ist, läuft nicht.

Die ehrliche Abwägung: Wann lohnt OCM?

Ich halte OCM nicht für Marketing-Geschwurbel. Es löst ein reales Problem — nur eben nicht meines. Konkret lohnt es sich, wenn:

Es lohnt sich nicht, wenn:

Konkret heißt das für uns zwei verschiedene Antworten je nach Kontext: In den K8s-Setups bei Kunden ist OCM ein durchaus prüfenswerter Baustein — gerade dann, wenn Multi-Artifact-Promotion zwischen Umgebungen oder Cross-Registry-Transfer ein Thema werden. Für unsere eigene NixOS-Infrastruktur lösen wir die Composition-Eigenschaft auf andere Weise, ohne ein zusätzliches Bundle-Format einführen zu müssen. Beide Antworten sind in ihrem jeweiligen Kontext sauber.

Was ich aus dem Ausgangsartikel mitnehme

Begriffshygiene zählt. „Software Bill of Delivery“ ist kein etablierter Standard — weder NTIA, noch CISA, noch OWASP, noch CNCF nutzen den Begriff. Das ist kein Vorwurf an einen Autor, der ihn als beschreibenden Sammelbegriff verwendet. Aber wer ihn in formaler Außenkommunikation oder gegenüber Auditoren übernimmt, hat es bei der Anschlussfähigkeit unnötig schwer. Die etablierten Akronyme sind SBOM, SLSA, in-toto, VEX, Sigstore. Auditoren erkennen sie. Ich verwende sie deswegen auch in eigener Dokumentation.

Das mentale Modell trotzdem mitnehmen. Atomic Composition mit Provenance-Kette ist die richtige mentale Brille — egal mit welchem Tooling man sie umsetzt. Bei NixOS sind es die Release-Module plus flake.lock plus signierte Images mit Provenance. Bei K8s sind es OCM oder ein selbstgebautes Manifest plus Sigstore. Die Eigenschaften sind dieselben, die Werkzeuge unterschiedlich.

DevSecOps-Architektur ist Architektur. Viele Tooling-Notwendigkeiten verschwinden, wenn man die Architektur eine Schicht tiefer richtig wählt. Das ist kein Argument gegen K8s — es gibt gute Gründe für Cluster-Plattformen, und Skalierungsbedarf ist einer davon. Aber es ist ein Argument dagegen, K8s zu wählen, weil „alle es nutzen“, und sich dann mit der zugehörigen Tooling-Landschaft auseinanderzusetzen, ohne dass die Anforderungen das tatsächlich erfordern.

Take-away

Die Frage ist nicht „brauche ich eine Bill of Delivery?“. Die Frage ist: „Wo bringt mein Stack die Composition-Eigenschaft schon mit, und wo muss ich sie nachrüsten?“

Bei K8s nachrüsten — über Bundle-Formate, Sigstore-Policies, GitOps-Disziplin. Bei NixOS-Hosts mit Podman ist sie als Nebeneffekt vorhanden — bleibt nur, sie sichtbar und signiert zu machen.

Provenance pro Image, OpenVEX für Audit-Hygiene und Verify-on-Deploy bleiben in beiden Welten Pflicht. Der Rest hängt davon ab, welches Composition-Problem dein Stack dir gestellt hat — oder dir erspart.

Beide Wege sind legitim. K8s mit OCM oder einem äquivalenten Bundle-Layer ist eine konsistente Antwort auf ein konsistentes Problem. NixOS mit Podman ist eine andere — nicht überlegene — Antwort, die das Problem an einer anderen Stelle auflöst. Was zählt, ist nicht die Wahl an sich, sondern dass die Eigenschaften am Ende sauber gegeben sind: atomare Komposition, nachvollziehbare Provenance, Signatur, Verifikation. Welcher Pfad dorthin führt, hängt vom Stack ab, vom Team, vom Kunden, von Skalierungsanforderungen.

Genau deswegen finde ich Auseinandersetzungen wie die mit dem Appetizers-Post wertvoll: nicht, um Recht zu haben, sondern um die Eigenschaften, die wir alle erreichen wollen, klarer zu benennen — und zu sehen, auf wie unterschiedlichen Pfaden man dort ankommt.