Kai Ole Hartwig

Container-Images

Zwei Abbilder je Release, linux/amd64 und linux/arm64. Das erste Release mit Abbildern ist 0.35.0; die Binaries auf der Release-Seite reichen weiter zurück.

  • ghcr.io/ohartwig/pinup: pinup, git, gpg samt Agent, ssh-keygen, ein CA-Bundle
  • ghcr.io/ohartwig/pinup-toolchain: dasselbe, dazu composer, php, node, npm, yarn und go
docker run --rm -v "$PWD:/workspace" ghcr.io/ohartwig/pinup:0 \
  pinup whatif --repo . --config .pinup.yaml --report plan.json

Welches von beiden

Das schlanke. Einen Paketmanager braucht pinup nur, wenn ein Update eine Lock-Datei neu schreiben muss — eine composer.lock, eine package-lock.json, eine go.sum — oder wenn die Konfiguration einen postUpgradeTask ausführt. Alles andere ist das Binary und git.

Das Toolchain-Abbild ist rund fünfmal so groß, und jeder Interpreter darin kann eine CVE in die Pipeline tragen. Wo ein Update einen braucht, sagt pinup es statt zu raten: der Branch wird mit dem Grund pluginRequired gehalten und steht so im Plan.

Das schlanke Abbild hält sich selbst schlank. Der Bau scheitert, wenn node oder php im Pfad auftaucht. Das ist keine Zierde, sondern die Invariante, für die es dieses Abbild gibt.

Tags

Nur ein Digest (ghcr.io/ohartwig/pinup@sha256:…) kann sich nicht unter dir ändern. In einer Pipeline also Tag und Digest pinnen und beides von einem Bot bewegen lassen — wofür es pinup schließlich gibt.

Woher das Binary kommt

Das Abbild kompiliert pinup nicht. Der Workflow lädt die Binaries des Releases, prüft sie gegen dessen SHA256SUMS und kopiert eines hinein. Gebaut und signiert werden sie einmal, davor, und dieselben Dateien liegen auf der Release-Seite.

Diese Trennung ist Absicht. Ein früherer Workflow hat die Binaries beim Eintreffen des Tags neu gebaut: ein zweites Artefakt unter dem Namen des Releases, mit niemandes Signatur darauf. Ein signiertes Binary zu verpacken ist etwas anderes, als eines zu bauen, und nur das Erste lässt sich gegen das Veröffentlichte prüfen.

Die Rezepte liegen im Repository unter packaging/. Die Basis ist Chainguards wolfi-base, per Digest gepinnt. Das Toolchain-Abbild pinnt jede Sprachlaufzeit auf eine exakte Wolfi-Paketversion, damit ein Bump ein geprüfter Diff ist und nicht das, was der Spiegel an dem Morgen ausgeliefert hat.

Prüfen, was ankommt

Beide Abbilder sind keyless signiert. Die Signatur entsteht gegen die OIDC-Identität des Workflows und lässt sich ohne einen Schlüssel von uns prüfen.

 

cosign verify ghcr.io/ohartwig/pinup:0.35.0 \
  --certificate-identity-regexp '^https://github.com/ohartwig/pinup/' \
  --certificate-oidc-issuer token.actions.githubusercontent.com

 

Dazu trägt jedes Abbild eine SBOM und die Herkunft seines Baus, als Attestation im Index; docker buildx imagetools inspect holt beides. Der Scan läuft vor der Signatur: ein behebbarer CRITICAL- oder HIGH-Fund lässt den Bau scheitern, und ein Abbild, das nicht besteht, wird nie signiert. Eine fehlende Signatur ist damit eine Aussage und kein Versehen.

Betrieb

Das Abbild läuft als uid 1000 in /workspace. Es hat keinen Entrypoint, pinup gehört also zum Kommando: docker run … pinup whatif …, nicht docker run … whatif …. Das ist Absicht. Ein CI-Runner startet das Job-Skript mit sh im Container, und ein Entrypoint pinup würde darauf mit „unknown command sh“ antworten.

 

docker run --rm \
  -v "$PWD:/workspace" \
  -e PINUP_GITLAB_URL -e PINUP_GITLAB_TOKEN \
  ghcr.io/ohartwig/pinup:0 \
  pinup run --project gruppe/projekt --config 'local>gruppe/runner' --report plan.json

Erreichen die Runner ghcr.io nicht, spiegle das Abbild in die eigene Registry und pinne den gespiegelten Digest. Ein kopiertes Abbild bleibt prüfbar, solange der Digest mitreist.

Weiter

Einstieg

Vom Binary zum geplanten Job: der erste Plan, der erste Lauf, ein CI-Job.

Lesen →
Einstieg

Zurück zu pinup

Die Übersicht: Problem, Fit-Check, Quickstart, alle Kapitel.

pinup →
Zurück zu pinup