Reproducible Builds bedeuten eine simple Zusage: Wer denselben Quellcode zweimal baut, bekommt zweimal exakt dasselbe Artefakt, Byte für Byte identisch. Klingt selbstverständlich, ist es aber in der Praxis fast nie. Die meisten Build-Prozesse produzieren bei jedem Lauf ein leicht anderes Ergebnis, ohne dass sich am Code etwas geändert hat. Für die Sicherheit der Lieferkette ist das ein Problem, denn ohne Reproduzierbarkeit kann niemand unabhängig prüfen, ob ein veröffentlichtes Binary wirklich aus dem Code stammt, der offen einsehbar ist.
Warum das für die Lieferkette zählt
Der übliche Vertrauensweg ist: Ich lade ein Binary herunter, vertraue darauf, dass es zum Quellcode passt, und führe es aus. Zwischen Quellcode und Binary steht aber der Build-Server, und der ist ein interessantes Ziel. Wer die Build-Pipeline kompromittiert, kann Schadcode einschleusen, der im offenen Repository nicht auftaucht. Der berühmte Fall ist der Angriff auf SolarWinds, bei dem genau an dieser Stelle manipuliert wurde.
Reproduzierbarkeit dreht das um. Wenn zwei unabhängige Parteien denselben Code bauen und Bit für Bit dasselbe Ergebnis erhalten, muss das Binary aus dem Code stammen. Der Build-Server verliert seine Sonderstellung als Vertrauensanker. Genau deshalb setzen Projekte wie Debian, Tor und Bitcoin seit Jahren auf reproduzierbare Builds.
Was Reproduzierbarkeit zerstört
Die Ursachen für nichtdeterministische Builds sind meistens dieselben. An erster Stelle stehen Zeitstempel. Viele Werkzeuge betten die aktuelle Uhrzeit in das Artefakt ein, in Archive, in Metadaten, in kompilierte Objekte. Zwei Builds zu unterschiedlichen Zeiten erzeugen dann unterschiedliche Bytes.
Weitere Klassiker sind absolute Pfade des Build-Verzeichnisses, die Reihenfolge von Dateien beim Einlesen eines Ordners, eingebettete Hostnamen oder Benutzernamen und nicht festgenagelte Abhängigkeiten. Auch parallele Kompilierung kann die Reihenfolge im Ergebnis verändern, wenn das Build-System nicht sorgfältig sortiert.
SOURCE_DATE_EPOCH: der wichtigste erste Schritt
Für das Zeitstempel-Problem gibt es einen etablierten Standard. Die Umgebungsvariable SOURCE_DATE_EPOCH enthält einen festen Unix-Zeitstempel, den Build-Werkzeuge statt der aktuellen Uhrzeit verwenden. Viele Compiler, Archivierer und Paket-Tools respektieren sie bereits. Ein sinnvoller Wert ist der Zeitpunkt des letzten Commits:
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)echo $SOURCE_DATE_EPOCH
Bei Container-Images lohnt sich derselbe Ansatz. Buildkit unterstützt eine Option, die alle Zeitstempel in den Layern auf einen festen Wert setzt:
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) docker buildx build --output type=docker,rewrite-timestamp=true -t meinapp:repro .
Reproduzierbarkeit tatsächlich prüfen
Die einzige verlässliche Prüfung ist der Doppelbau. Man baut zweimal, idealerweise auf zwei verschiedenen Maschinen zu zwei verschiedenen Zeiten, und vergleicht die Ergebnisse. Bei einem einzelnen Artefakt reicht ein Hash:
sha256sum build1/app.tarsha256sum build2/app.tar
Stimmen die Hashes nicht überein, will man wissen, woran es liegt. Dafür ist diffoscope das richtige Werkzeug. Es packt Archive, Images und Binaries rekursiv aus und zeigt den Unterschied auf Byte-Ebene, oft mit einem lesbaren Hinweis auf die Ursache wie einen abweichenden Zeitstempel:
diffoscope build1/app.tar build2/app.tar
In der Regel arbeitet man sich so Schritt für Schritt durch die Fehlerquellen. Erst der Zeitstempel, dann die Pfade, dann die Sortierung. Nach ein paar Runden bleibt der Diff leer, und ab dann ist der Build reproduzierbar.
Wo sich der Aufwand lohnt
Reproduzierbarkeit ist kein Selbstzweck. Wer nur intern deployt und keine externe Verifikation braucht, hat andere Prioritäten. Interessant wird es, sobald man Artefakte veröffentlicht, die andere ausführen, oder sobald man belegen will, dass ein ausgeliefertes Binary genau dem geprüften Code entspricht. Zusammen mit einer signierten Provenance und einer Stückliste der Abhängigkeiten entsteht daraus eine überprüfbare Kette vom Commit bis zum laufenden Container.
Der pragmatische Einstieg ist klein: SOURCE_DATE_EPOCH setzen, zweimal bauen, mit diffoscope die verbleibenden Unterschiede aufräumen. Das deckt schon einen Großteil der Fälle ab. Wie sich reproduzierbare Builds in eine größere Absicherung der Lieferkette einfügen, habe ich im Umfeld von digital-business.blog ausführlicher beschrieben.
Hinterlasse einen Kommentar