Der Kern entscheidet, Plugins führen aus
Ein Dependency-Werkzeug hat zwei gefährliche Eigenschaften zugleich. Es hält einen Schreib-Token für viele Repositories. Und es führt Paketmanager aus, die Code aus genau diesen Repositories lesen.
pinup trennt deshalb scharf: Der Kern entscheidet alles, was sich ändert. Ein Task führt nur aus, ohne die Zugangsdaten der Plattform und in einem Bereich, den er nicht verlassen darf.
01 — Die Meinung in einem Satz
Ein Task erzeugt eine Lock-Datei neu oder führt einen erlaubten Befehl aus, und sonst nichts.
Welche Abhängigkeit sich ändert, welche Version kommt und welche Datei betroffen ist, steht im Plan. Der Task bekommt einen Checkout und einen Auftrag. Ein Befehl aus einer Regel sieht so aus:
packageRules:
- matchDatasources: [git-refs]
postUpgradeTasks:
commands: ["node tools/update-expected-commit.mjs {{{packageFile}}}"]
fileFilters: ["*.yaml"]
Das steht in der .pinup.yaml des Repositories. Ob der Befehl laufen darf, entscheidet aber nicht das Repository. Das entscheidet die Liste erlaubter Befehle im Runner, dem zentralen Job, der pinup über alle Repositories laufen lässt.
02 — Die Alternative: Isolation als Betriebsaufgabe
Renovate führt Post-Upgrade-Tasks in der Umgebung aus, in der es selbst läuft. Eine Liste erlaubter Befehle legt der Betreiber fest. Die Isolation überlässt Renovate weitgehend dieser Umgebung: einem Container pro Lauf, einem eigenen Runner, einem Token mit wenig Rechten.
Das ist ein vernünftiger Schnitt. Ein Werkzeug für viele Plattformen kann nicht wissen, welche Isolation dort möglich ist. Also wird sie Aufgabe des Betriebs.
In meinem Bestand läuft ein Job über viele Repositories, mit einem Token und einem Signaturschlüssel. Ein Task aus Repository A hätte die Checkouts von B und C gesehen, kurz bevor pinup sie mit seiner Signatur committet. Das wollte ich nicht dem Betrieb überlassen.
03 — Der Preis
Nur auf Linux. Die Sandbox nutzt User-Namespaces. Wo es die nicht gibt oder ein Profil sie verbietet, verweigert pinup jeden Task und hält den Branch, statt ohne Schutz zu laufen.
Caches pro Repository. Ein gemeinsamer Paket-Cache hätte es einem Repository erlaubt, den Lock-Refresh des nächsten zu vergiften. Getrennte Caches kosten bei npm einige Sekunden pro Repository.
Das Netzwerk bleibt offen. Ein Paketmanager muss Pakete laden. Was der Job erreicht, erreicht auch der Task. Das ist die bewusst benannte Lücke.
Mehr Arbeit im Kern. Die Befehle für Lock-Refreshes setzt pinup selbst zusammen, mit Schaltern, die Skripte und Plugins der Paketmanager abschalten.
04 — Der Anlass
Zwei Vorfälle aus dem eigenen Bestand.
Der Befehl, der still wegfiel. Unter Renovate stand ein Befehl nicht auf der Liste erlaubter Befehle. Renovate übersprang ihn und pushte den Branch trotzdem. So fehlten wochenlang die Lock-Refreshes für eigene Pakete, ohne roten Job. pinup hält einen solchen Branch mit dem Grund taskRefused und nennt den Befehl.
Der Token unter falschem Namen. pinup filterte die Zugangsdaten der Plattform von Anfang an nach Namen. Trotzdem erreichte der Schreib-Token eine Zeit lang die Tasks in meinem Runner. Die Runner-Konfiguration hatte ihn unter anderen Namen an Tasks weitergereicht. Seitdem prüft pinup auch den Wert: Trägt eine Variable den Token unter irgendeinem Namen, startet der Task nicht.
05 — Die Durchsetzung
- Keine Plattform-Zugangsdaten, doppelt geprüft. Ein Task sieht eine feste Grundausstattung (
PATH,LANG,LC_ALL,TMPDIR,TZ) und was der Runner ausdrücklich freigibt. Plattform-Token und Signaturschlüssel werden nach Namen und nach Wert ferngehalten. - Die Sandbox. Jeder Task läuft in eigenen Namespaces, ohne Fähigkeiten und mit verdeckten Verzeichnissen: temporäre Checkouts, Schlüssel, Agent-Sockets, pinups eigener Cache.
- Fail closed. Vor dem ersten Task prüft pinup die Sandbox von innen. Schlägt die Prüfung fehl, läuft kein Task. Eigene Tests in pinups Pipeline beweisen das in den Images, in denen Tasks in Produktion laufen.
- Der Bereich. Was ein Task ändert, vergleicht pinup mit seinen erlaubten Dateien. Ein einziger Pfad außerhalb verwirft das ganze Ergebnis.
Häufige Fragen
Wie kommt ein Task dann an private Pakete?+
Über Lese-Zugangsdaten, die der Runner ausdrücklich freigibt: für Go-Module eine .netrc im temporären Home des Tasks, für Composer und npm Lese-Token als Variablen. Den Schreib-Token der Plattform enthält keine davon, und pinup prüft das am Wert.
Warum nicht einfach ein Container pro Task?+
In der Spezifikation ist das sogar die Standardvariante, mit einem per Digest gepinnten Image pro Task. Sie braucht aber Docker im Runner. In Produktion läuft die Variante ohne Container, mit der Sandbox im Prozess, und dort ist sie bewiesen.
Kann ich die Sandbox abschalten?+
Ja, mit PINUP_TASK_ISOLATION=off. Dann laufen Tasks ohne Isolation, und pinup sagt das bei jedem Lauf. Ohne diesen Schalter läuft kein Task ohne Isolation: Fehlt die Sandbox, verweigert pinup die Tasks und hält die Branches.
Fazit
Ein Task, der nur ausführt, kann weniger. Er kann aber auch weniger kaputt machen, und genau das ist der Punkt. Der Preis sind Linux, getrennte Caches und eine Lücke im Netzwerk, die offen benannt ist.
Nächste Woche geht es um die Stellen, an denen pinup bewusst nicht anders entscheidet: Kompatibilität zu Renovate. Die Übersicht aller Teile und die Begriffe, die öfter vorkommen, stehen im Auftakt der Serie.
Was sehen die Tasks in deinem Dependency-Runner? Ich prüfe es nach.
Ein Blick auf deinen Runner: welche Zugangsdaten Tasks erreichen, welche Befehle erlaubt sind und was ein Repository über das nächste erfährt.
Über den Autor

Kai Ole Hartwig
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.
