
„Software ist wie Obst“ sagte mal ein guter Kollege zu mir – und damit hatte er recht. Die meiste moderne Software wird regelmäßig aktualisiert, und wenn dies nicht mehr der Fall ist, schaut man sich nach Alternativen um. Denn veraltete Software birgt das Risiko, bereits bekannte Sicherheitslücken zu enthalten, die ohne Updates weiter ausnutzbar bleiben. Seine eigene Software jederzeit auf dem aktuellen Stand zu halten, ist dabei keine triviale Aufgabe, denn moderne Software bringt in der Regel Unmengen an Abhängigen mit sich.
Kosten und Nutzen von Abhängigkeiten abwägen
Bei meinem Passwortmanager habe ich Wert darauf gelegt, auf unnötige Abhängigkeiten zu verzichten. Dennoch komme ich auf 5 Laufzeit-Abhängigkeiten, 9 Test-Abhängigkeiten und 8 Gradle-Plugins, die zur Kompilierzeit wirken können. Ich nutzen 8 GitHub-Actions und 2 Docker-Images und installiere in einem davon 3 NPM-Pakete. Mit transitiven Abhängigkeiten wachsen die 14 Java-Dependencies auf insgesamt 47 Abhängigkeiten.
Im Vergleich zu der Software, mit der ich beruflich zu tun habe, ist das noch harmlos, und dennoch verdeutlicht es schon ganz gut, dass eine komplett manuelle Pflege dieser Abhängigkeiten das Potential hat, Lücken zu hinterlassen. Die Modernisierung eines über Jahre nicht auf dem Stand gehaltenen Produktes kann sehr teuer sein. In einem Fall, den ich erlebt habe, hat es sogar maßgeblich zur Komplettablösung durch eine Neuentwicklung beigetragen. Man sollte also sein Dependency-Management nicht vernachlässigen.
Abhängigkeitenpflege, aber wie?
Doch wie tut man es richtig? Nutzt man Werkzeuge wie den OWASP Dependency Check, um im Nachhinein auf Sicherheitslücken zu reagieren und in dem Zuge Abhängigkeiten auf den aktuellen Stand zu bringen? Überprüft man seine Abhängigkeiten im Rahmen eines regelmäßigen Patch Days? Geht man dabei alle Dependencies einzeln durch und vergleicht diese gegen Maven Central, NPM, GitHub und Co? Verwendet man entsprechende Plugins seines Dependency Managers, um sich aktualisierbare Abhängigkeiten direkt ausgeben zu lassen?
Eine Antwort auf all diese Fragen könnte Renovate sein. Ich habe Renovate kürzlich für meinen Passwortmanager eingerichtet und werde in diesem Blogbeitrag erklären, was Renovate ist, welches Potential es hat und wie ich es für mich aufgesetzt habe.
Moderne Dependency-Pflege mit Renovate
Wer schon mal etwas vom Dependabot gehört hat, dem kann ich Renovate in einem Satz beschreiben: Es ist so ähnlich, aber mächtiger. Dies trifft zumindest zum Zeitpunkt dieses Beitrags zu. Doch nun ein paar mehr Details.
Renovate ist ein Bot für Git-basierte Repositories, der verschiedenste Abhängigkeiten automatisch erkennt und aktualisiert. Renovate unterstützt dabei eine Vielzahl an Plattformen und Paketmanagern, kann Updates gruppieren, selbstständig mergen, umfangreich konfiguriert werden und das Issue-Management der Plattform nutzen, um den Status Quo in einem interaktiven Dashboard abzubilden.
Wie ich Renovate aufgesetzt habe
Wahl der Plattform
Für meinen Passwortmanager verwende ich GitLab und GitHub. Auf beiden Plattformen habe ich ein inhaltlich identisches Git-Repository laufen, das mit meinem lokalen Repository verknüpft ist. Der eigentliche Build-Prozess läuft über GitLab CI, ich nutze aber auch GitHub Actions für ein schnelles Feedback. Renovate unterstützt beide Plattformen und noch einige mehr.
Da Renovate in seiner Dokumentation zur GitLab bot security Bedenken hinsichtlich des aktuellen GitLab-Security-Modells äußert und GitLab einige Features wie Project Access Tokens nur in der Bezahlversion anbietet, habe ich mich für eine Integration in GitHub entschieden.
GitHub bietet seit kurzem fine-grained Personal Access Tokens an, die wie der Name erahnen lässt Berechtigungen feingranularer vergeben lassen. Leider unterstützt Renovate diese zum Zeitpunkt des Beitrags nicht und erfordert einen classic Personal Access Token, der sich nicht auf einzelne Repositories einschränken lässt. Ich habe deshalb die Laufzeit des Tokens auf 90 Tag begrenzt und werde danach evaluieren, ob es für mich sicherere Alternativen gibt.
Grundsätzlich stufe ich Renovate als vertrauenswürdig ein, bin aber auch ein großer Verfechter des Principles of Least Privilege und möchte dem Bot nicht unnötig viele Rechte einräumen. Alternativ zum Token gibt es Renovate auch als GitHub App. Für die meisten Anwender und Organisationen ist das vermutlich eine gute Wahl. Da mir Sicherheit und Transparenz sehr wichtig sind, habe ich mich für einen Eigenbetrieb mit Token entschieden.
Installation als Self-Hosted Bot
Der Renovate-Bot kann direkt via NPM installiert oder als Docker-Image betrieben werden. Für GitHub ist der Bot auch als GitHub Action verfügbar. In der Dokumentation wird auch die Installation in einem Kubernetes-Cluster beschrieben. Ich habe mich für den Einsatz des Docker-Images entschieden, da ich die Reproduzierbarkeit von Docker sehr schätze.
Das Setup gestaltet sich sehr einfach. Der Renovate-Bot kann wie ein Skript verwendet werden und benötigt dazu lediglich eine Konfigurationsdatei. Diese wird als CommonJS-Modul bereitgestellt:
1 2 3 4 5 6 7 8 | module.exports = { token: process.env.RENOVATE_TOKEN, platform: 'github' , onboardingConfig: { extends : [ 'config:best-practices' ], }, repositories: [ 'christianpflugradt/passbird' ], } |
Diese sehr minimalistische Konfiguration legt folgendes fest:
- der Personal Access Token soll aus der Umgebungsvariablen
RENOVATE_TOKEN
gelesen werden - die Plattform ist GitHub
- der Bot soll lediglich das Repository
christianpflugradt/passbird
verwalten - die Konfigurationsvoreinstellung
config:best-practices
soll verwendet werden
Der unterste Punkt spielt in meinem Fall keine große Rolle, ist aber interessant, wenn eine Vielzahl von Projekten über Renovate verwaltet wird, etwa auf Organisationsebene. In einem solchen Fall kann statt einer festen Liste an Repositories die Autodiscover-Option gesetzt werden, so dass neue Projekte automatisch von Renovate erkannt und bespielt werden.
Existiert in einem Projekt noch keine renovate.json
-Konfigurationsdatei, erstellt Renovate einen auf das Repository abgestimmten Pull Request mit einer initialen Konfiguration. Nachdem dieser gemerged ist, fängt Renovate an, Abhängigkeiten auf Aktualisierungen zu überprüfen.
Den eigentlichen Aufruf des Bots habe ich in ein kleines Shellskript ausgelagert, das über einen Cronjob regelmäßig aufgerufen wird:
1 2 3 4 5 6 7 8 | TAG=37-full TOKEN= "ghp_secrettoken" docker pull renovate /renovate :$TAG docker run -- rm -e "RENOVATE_TOKEN=$TOKEN" \ -e "LOG_LEVEL=debug" \ - v "/opt/renovate/config.js:/usr/src/app/config.js" \ renovate /renovate :$TAG > log.txt |
Wie man sieht, setze ich die Version des Renovate-Docker-Images manuell, um plötzliche Fehler durch Breaking Changes möglichst zu vermeiden. Eine bessere Lösung habe ich zum Zeitpunkt dieses Beitrags noch nicht, möchte aber für die aktuelle Version auf die offizielle Dokumentation verweisen, da ich diesen Artikel nicht aktualisieren werde und die hier abgebildete Version irgendwann veraltet sein wird.
In dem Shellskript setze ich auch den GitHub-Token, den ich hier natürlich nicht im Original angebe. Außerdem habe ich für mich den Loglevel auf Debug gesetzt und leite das Log in eine Datei weiter, in der ich immer das Ergebnis des letzten Laufs einsehen kann. Da ich Renovate aktuell noch kennenlerne, finde ich das sehr praktisch. Allgemein würde ich nicht empfehlen, in eine lokale Datei zu schreiben, sondern sich an der jeweiligen Umgebung, etwa einer Container-Orchestrierungsplattform, zu orientieren.
Für meine persönlichen Zwecke reicht es aus, Renovate einmal die Stunde laufen zu lassen. Das Setup ist aber individuell und ich kann hier keine allgemeingültigen Empfehlungen geben. Mein Crontab-Eintrag für Renovate sieht wie folgt aus:
0 * * * * cd /opt/renovate ; /bin/bash run.sh |
Clientseitige Konfiguration – das Repository
Da ich eine klare Vorstellung meiner Renovate-Konfiguration habe, habe ich nach dem ersten Lauf den Onboarding-Pull-Request geschlossen und direkt eine renovate.json
im Repository angelegt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | { "dependencyDashboard" : true , "dependencyDashboardOSVVulnerabilitySummary" : "all" , "separateMultipleMajor" : true , "prCreation" : "status-success" , "packageRules" : [ { "matchFileNames" : [ ".github/**" ], "groupName" : "github-actions" , "separateMajorMinor" : false }, { "matchFileNames" : [ "build.gradle.kts" ], "matchUpdateTypes" : [ "patch" , "minor" ], "groupName" : "non-major" } ] } |
In dieser Konfigurationsdatei wird folgendes definiert:
- das Dependency-Dashboard soll verwendet werden
- auf dem Dashboard sollen identifizerte Sicherheitslücken aus der OSV-Datenbank (Open Source Vulnerabilities) aufgeführt werden
- für jedes verfügbare Update auf eine neue Major-Version, also die erste Stelle nach semantischer Versionierung x.y.z, soll ein separater Pull Request erzeugt werden
- Pull Requests werden erst erzeugt, wenn der Build-Workflow erfolgreich durchgelaufen ist
- Updates von GitHub-Actions werden auch bei Major-Updates gebündelt in einem Pull Request angeboten
- Minor- und Patch-Level-Updates von Gradle-Dependencies werden in einem Pull Request zusammengefasst
An dieser Stelle möchte ich kurz erwähnen, dass ich die Konfigurationsmöglichkeiten enorm umfangreich und die Dokumentation dazu sehr gelungen finde. Im Dokumentationsindex der Repository-Konfiguration befinden sich weit über hundert Einträge. Gleichzeitig ist das Onboarding denkbar einfach, da Renovate nach dem Prinzip Convention over Configuration vorgeht und globale Presets (Voreinstellungen), wie das von mir verwendete config:best-practices
anbietet.
Ein paar Beispiele gefällig? Renovate legt für jeden Pull Request einen eigenen Branch an. Die Anzahl parallel existierender Branches kann konfigurativ begrenzt werden, um Anwender mit sehr komplexen Projekten nicht mit einer Vielzahl von Branches und Pull Requests zu erschlagen. Manche Projekte verwenden eine Vielzahl an Branches für den Release-Prozess. Renovate verwendet standardmäßig nur den Default Branch eines Projektes, kann aber – auch über reguläre Ausdrücke – beliebig viele Base Branches verwalten. Renovate verwendet eine konfigurierbare Rebase-Strategie, wenn nach Erstellung eines Branches ein Base Branch aktualisiert wird. Ebenfalls existiert eine Recreate-Strategie für erstellte Pull Requests. Die Ausgestaltung des Dashboards und von Pull Requests kann umfangreich konfiguriert werden. Auch die in meiner Konfiguration verwendeten Package Rules sind ein sehr mächtiges Werkzeug. Es ist beispielsweise möglich, je nach Registry unterschiedliche Regeln anzuwenden oder zusammengehörende Packages nur gruppiert zu aktualisieren, am Beispiel meines Passwortmanagers etwa alle JUnit-Abhängigkeiten, die über die GroupId org.junit.jupiter
erkennbar sind.
Automatische Erkennung vorhandener Abhängigkeiten
Renovate unterstützt eine Vielzahl an Managern, darunter unter anderem ansible, bazel, cargo, conan, cpan, docker, gitlabci, gomod, gradle, helm, homebrew, jenkins, maven, npm, nuget, osgi pip, puppet, sbt, swift und terraform. Wie entsprechende Abhängigkeiten gefunden werden, veranschaulicht das Debug-Log sehr transparent, hier ein kleiner Ausschnitt:
1 2 3 4 5 6 7 8 9 10 | DEBUG: Using file match: (^|/)tasks/[^/]+\.ya?ml$ for manager ansible (repository=christianpflugradt/passbird) DEBUG: Using file match: (^|/)requirements\.ya?ml$ for manager ansible-galaxy (repository=christianpflugradt/passbird) DEBUG: Using file match: (^|/)galaxy\.ya?ml$ for manager ansible-galaxy (repository=christianpflugradt/passbird) DEBUG: Using file match: (^|/)\.tool-versions$ for manager asdf (repository=christianpflugradt/passbird) DEBUG: Using file match: azure.*pipelines?.*\.ya?ml$ for manager azure-pipelines (repository=christianpflugradt/passbird) DEBUG: Using file match: (^|/)batect(-bundle)?\.ya?ml$ for manager batect (repository=christianpflugradt/passbird) DEBUG: Using file match: (^|/)batect$ for manager batect-wrapper (repository=christianpflugradt/passbird) DEBUG: Using file match: (^|/)WORKSPACE(|\.bazel)$ for manager bazel (repository=christianpflugradt/passbird) DEBUG: Using file match: \.bzl$ for manager bazel (repository=christianpflugradt/passbird) DEBUG: Using file match: (^|/)MODULE\.bazel$ for manager bazel-module (repository=christianpflugradt/passbird) |
Im Fall meines Passwortmanagers werden 6 Dateien erkannt, die unterstützte Abhängigkeiten enthalten:
1 2 3 4 5 6 7 8 9 10 11 | DEBUG: Found 6 package file(s) (repository=christianpflugradt/passbird) INFO: Dependency extraction complete (repository=christianpflugradt/passbird, baseBranch=main) "stats": { "managers": { "github-actions": {"fileCount": 3, "depCount": 56}, "gitlabci": {"fileCount": 1, "depCount": 2}, "gradle": {"fileCount": 1, "depCount": 18}, "gradle-wrapper": {"fileCount": 1, "depCount": 1} }, "total": {"fileCount": 6, "depCount": 77} } |
Im Debug-Log ist dann weiterhin ersichtlich, wie alle identifizierten Abhängigkeiten online auf vorhandene Releases überprüft und entsprechend der Konfiguration Branches und Pull Requests erzeugt werden. Hier ein paar Ausschnitte:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | DEBUG: Found 9 new releases for io.strikt:strikt-core in repository https://repo.maven.apache.org/maven2/ (repository=christianpflugradt/passbird) DEBUG: Found 20 new releases for org.awaitility:awaitility in repository https://repo.maven.apache.org/maven2/ (repository=christianpf lugradt/passbird) DEBUG: Found 37 new releases for com.tngtech.archunit:archunit in repository https://repo.maven.apache.org/maven2/ (repository=christi anpflugradt/passbird) [...] DEBUG: PR concurrent limit remaining: 10 (repository=christianpflugradt/passbird) DEBUG: Calculated maximum PRs remaining this run: 2 (repository=christianpflugradt/passbird) DEBUG: PullRequests limit = 2 (repository=christianpflugradt/passbird) DEBUG: Calculating hourly PRs remaining (repository=christianpflugradt/passbird) DEBUG: PR hourly limit remaining: 2 (repository=christianpflugradt/passbird) DEBUG: Calculating branchConcurrentLimit (10) (repository=christianpflugradt/passbird) DEBUG: 1 already existing branches found: renovate/github-actions (repository=christianpflugradt/passbird) DEBUG: Branch concurrent limit remaining: 9 (repository=christianpflugradt/passbird) DEBUG: Calculated maximum branches remaining this run: 2 (repository=christianpflugradt/passbird) [...] DEBUG: Package lookup durations (repository=christianpflugradt/passbird) "gradle-version": {"count": 1, "averageMs": 515, "totalMs": 515, "maximumMs": 515}, "github-tags": {"count": 48, "averageMs": 1062, "totalMs": 50973, "maximumMs": 5692}, "maven": {"count": 18, "averageMs": 3075, "totalMs": 55354, "maximumMs": 8749}, "docker": {"count": 2, "averageMs": 6315, "totalMs": 12629, "maximumMs": 8251} DEBUG: dns cache (repository=christianpflugradt/passbird) "hosts": [] INFO: Repository finished (repository=christianpflugradt/passbird) "cloned": true, "durationMs": 24607 DEBUG: Checking file package cache for expired items DEBUG: Deleted 0 of 87 file cached entries in 100ms DEBUG: Renovate exiting |
Renovate-Dashboard
Das Renovate-Dashboard bringt Transparenz in den Update-Prozess, denn nicht jeder Entwickler hat Zugriff auf das Debug-Log. Zur Veranschaulichung habe ich in zwei Schritten mehrere Abhängigkeiten auf eine ältere Version zurückgedreht. Gemäß meiner Konfiguration erstellt Renovate zunächst einen Branch und wartet darauf, dass eine entsprechend eingerichtete Pipeline dadurch angestoßen und durchlaufen wird. Sollte die Pipeline erfolgreich durchlaufen, wird beim nächsten Lauf des Bots ein Pull Request angelegt.

Auf diesem Screenshot des Dependency-Dashboards wird transparent, wie Renovate die einzelnen Abhängigkeiten gruppiert. Verschiedene Minor- und Patch-Level-Updates der Gradle-Dependencies werden in einem non-major-Branch zusammengefasst. Die Major-Updates der Dependencies ArchUnit und Jacocolog werden in getrennten Branches hochgezogen. Die Docker-Images in der GitLab CI werden aufgrund der Major-Updates ebenfalls isoliert angeboten. Da bereits im vorherigen Lauf des Bots die verfügbaren Updates für Gradle und mehrere GitHub Actions identifiziert wurden, sind für diese im aktuellen Schritt bereits Pull Requests angelegt worden, wobei die GitHub Actions trotz Major-Updates aufgrund meiner konfigurierten Package Rule gebündelt bereitgestellt werden.
Das Dashboard bietet über Checkboxen Interaktionsmöglichkeiten, um z.B. Pull Requests früher zu erstellen. Der Abschnitt Detected dependencies schlüsselt auf, welche Abhängigkeiten in welchen Versionen im Projekt erkannt wurden und verwaltet werden. Der nachfolgende Screenshot zeigt diesen Bereich im teilweise aufgeklappten Zustand.

Debugging
Kurz nach erstmaliger Konfiguration von Renovate war ich etwas verwundert: Für verschiedene GitHub Actions standen offenbar Updates bereit, im Dashboard erschien aber abwechselnd der Hinweis auf Pending Status Checks und geltende Rate Limits. Tatsächlich hatte sich ein kleiner Bug eingeschlichen. Laut Dokumentation muss für den Personal Access Token lediglich der Geltungsbereich Repository vergeben werden. Tatsächlich erlaubt GitHub dann aber nicht die Anpassung der GitHub-Workflows. Leider wurde dieser Umstand auf dem Dashboard nicht richtig abgebildet und es erschien auch kein Error-Eintrag im Log. Dank des sehr transparenten Debug-Loglevels konnte ich trotzdem einen eindeutigen Hinweis auf diesen Fehler finden:
1 2 3 4 5 6 | To https://github.com/christianpflugradt/passbird.git refs/heads/renovate/github-actions [remote rejected] (refusing to allow a Personal Access Token to create or update workflow `.github/workflows/build.yml` without `workflow` scope) |
Sollte sich also Renovate nicht erwartungskonform verhalten, kann ich neben der Dokumentation das Debug-Log sehr empfehlen.
Fazit
Renovate macht einen hervorragenden ersten Eindruck und scheint für mich persönlich eine nachhaltige Lösung für das Problem Dependency-Pflege zu sein. Sowohl die Konfiguration als auch die Dokumentation sind auf einem hohen Niveau bei einem sehr geringen Einrichtungsaufwand. Renovate kann vielfältig eingesetzt und betrieben werden, sei es als App, als GitHub Action oder Standalone als NPM-Bibliothek oder im Docker-Image verpackt.
Der einzige Punkt, bei dem ich persönlich Nachbesserungsbedarf sehe, sind die notwendigen Berechtigungen. Diesen Punkt kann man aber nicht Renovate allein anlasten, da die Gründe, warum z.B. der fine-grained Personal Access Token von GitHub zurzeit nicht unterstützt wird, in der Dokumentation transparent gemacht werden. Ich hoffe dennoch, dass sich da in Zukunft etwas tut. Zur Überbrückung werde ich wahrscheinlich einen dedizierten User nur für Renovate anlegen, der dann nur die notwendigen Rechte auf die entsprechenden Repositories erhält und über dessen Personal Access Token Renovate dann legitimiert sein wird.
Trotz des sehr guten Eindrucks sollte man nicht außer Acht lassen, dass Renovate viele, aber unter Umständen nicht alle Abhängigkeiten unterstützt. Hinterlegt man beispielsweise direkt im Java-Code Docker-Images für Testcontainers-Tests, so weiß Renovate damit nichts anzufangen. Neben der fehlerbehafteten händischen Pflege kann ein möglicher Umgang damit so aussehen, dass man diese Abhängigkeiten sofern möglich in unterstützte Formate auslagert. Eine nicht ganz ideale Lösung für diesen spezifischen Fall: Dockerfiles für die Testcontainers-Images schreiben, die nur die FROM
-Anweisung enthalten und das Image als latest getagged in eine private Registry hochladen.
Renovate ist im Kern Open Source, bietet aber auch mehrere kostenpflichtige Angebote für Enterprise-Kunden. Ich hoffe sehr, dass Renovate damit profitabel bleibt und eine hochwertige Weiterentwicklung des Open-Source-Kernes auch in Zukunft finanzieren kann.
- Renovate-Dokumentation - Renovate-Dashboard meines Passwortmanagers