„Benutzt Docker!“, haben sie gesagt. „Damit wird alles sicherer.“, haben sie gesagt.

…aber bezüglich einiger Bereiche, überkommt den geneigten Entwickler das ungute Gefühl: da stimmt doch etwas nicht!

TL;DR:
Na, dann schaut einfach direkt auf der Projektseite vorbei: https://github.com/hmg-dev/docker-update-scanner

Das Problem

Als vor ein paar Jahren die meisten Firmen auf den Zug aufgesprungen sind, ihre Services von Hardware in virtuelle Maschinen zu packen, da war schon das nächste große Thema im Bereich der Virtualisierung und Containerisierung am Horizont: Docker.
Seine bestechende Einfachheit ermöglichte es plötzlich auch Entwicklern, sich wohl definierte, virtuelle Umgebungen für ihre Applikationen zu erstellen. Was sie auch taten. Und zwar mit Inbrunst!
Was dabei allerdings weniger Beachtung erfuhr, war die Tatsache, dass in dem Container ja nicht nur die Applikation landet. Damit der Container überhaupt läuft ist dort ja ein vollwertiges Betriebssystem drinne. Und ggfs. noch eine Laufzeitumgebung für die Applikation (wie Python oder Java).
Und wie das bei Betriebssystemen und Laufzeitumgebungen so ist: da kommen öfters mal Updates. Was automatisch auch immer (potentielle) Sicherheitsupdates sind: https://marc.info/?l=linux-kernel&m=121616463003140

Jetzt kann man natürlich einwenden: „Aber das ist doch die Aufgabe der Leute, die das Image erstellen von dem ich einfach nur Erbe.“
Und die Feststellung ist nicht nur richtig, sondern die machen das auch – meistens.

Die Quizfrage ist auch viel mehr: wie erfahrt IHR von diesen Updates?
Und wie stellt ihr fest, welche von euren dutzenden Dockerimages betroffen sind?
Und wenn ihr das alles wisst, dann müssen diese ja immer noch neu gebaut und deployed werden!

Ein Konzept

Da Docker selbst, für das Problem keine Lösung bietet, müssen wir uns eigene Gedanken machen, wie wir das angehen können.
Zum einen benötigen wir eine Aufstellung aller eigenen Dockerimages. Dann müssen wir feststellen, von welchem Image und Tag diese jeweils erben. Vom eigenen Image und vom Eltern-Image muss dann das Datum des letzten Updates heraus gefunden werden.
Dann müssen wir die beiden Angaben nur noch vergleichen, und wenn das eigene Image älter ist, den Build anstoßen.
Klingt einfach? Tja, klingt so – hat aber ein paar aufwändige Randbedingungen!

Ein paar Beispiele gefällig?

Wo fragt ihr die Daten für eure Images ab? Direkt da wo sie laufen? Im Cluster? Was wenn dort unterschiedliche Stände laufen? Was wenn eure Cluster dynamisch hoch und runter gefahren werden?

Die Eltern-Images liegen in den meisten Fällen auf Docker-Hub, aber das bietet keine offizielle API an – wie kommt ihr an die Daten? (Und: Nein, Docker-Hub setzt _nicht_ die „Docker-Registry API V2“ ein!)

Dockerfiles unterstützen Variablen! Wie kommt ihr an das konkrete Eltern-Image, wenn euer Dockerfile so beginnt?

ARG TAG_VERSION
FROM ubuntu:$TAG_VERSION

Wie soll das Verhalten sein, wenn das Eltern-Image in eurer eigenen Docker-Registry liegt? Und wie authentifiziert ihr euch dort, wenn ihr mehrere Docker-Registries habt?

Und wenn ihr alle nötigen Informationen habt: wie startet ihr den Build? Denn vielleicht liegen die zugehörigen Jobs/Pipelines in unterschiedlichen Projekten/Instanzen zu denen ihr alles Zugriff benötigt!?

Und last but not least: wie findet ihr heraus, ob das verwendete Eltern-Image selbst überhaupt noch gepflegt wird?

Na, und nun?

Wir sehen: ist alles nicht so einfach. Aber ich schreibe diesen Beitrag hier ja nicht, nur um euch das zu sagen 😉

Die ganzen Cluster zu tracken und dort umständlich nach zu schauen erschien unpraktikabel und fehleranfällig. Wir haben uns also dafür entschieden, die git-Repositories zu checken, die als Quelle für die Images dienen. Da bei uns alle Images via Azure Build Pipeline gebaut werden, lässt sich das Problem mit den variablen Tags dadurch umgehen, dass wir die konkrete Version/Tag aus der Pipeline-Definition parsen (in jedem Repo hats dafür eine azure-pipelines.yaml)

Die API von DockerHub ist, wie erwähnt, nicht offiziell/öffentlich dokumentiert. Da hieß es, die benötigten Aufrufe im Internet zusammen zu suchen. Da wir nur ein eingeschränktes Subset davon benötigen, war der Aufwand dafür überschaubar. Allein das Sicherstellen, dass das die einzige Möglichkeit ist, hat doch etwas Zeit in Anspruch genommen.

Um das Datum der neuesten Version eines unserer Images heraus zu finden, mussten wir sowieso die eingerichteten ACRs (Azure Container Registry) abfragen. Das ließ sich direkt wieder verwenden, wenn das Eltern-Image ebenfalls eins von unseren ist.
Weiter in der Kette braucht der Check nicht zu gehen, da das Eltern-Image dann ja ebenfalls in der Liste stehen sollte und seinerseits separat überprüft wird. (Wenn die Reihenfolge nicht optimal ist, wird ein erforderliches Update für ein Child-Image ggfs. erst beim nächsten Lauf erkannt, aber das erschien uns vernachlässigbar bzw. akzeptabel!)

Für den Zugriff auf die ganzen git-Repositories braucht es einen User mit zumindest Leserechten auf alle betroffenen Repos. (Bzw. den Access-Token eines solchen Users). Ähnliches gilt auch für die Pipelines! Da niemand seinen persönlichen Account dafür hergeben wollte, und uns das Hantieren mit ServicePrincipals Kopfschmerzen bereitet, haben wir kurzerhand einen CI-User dafür eingerichtet.
Die ACRs haben eigene Credentials, über die man sie ansprechen kann.

Das abfragen/ansprechen der Pipelines und ACRs haben wir in einer frühen Version des Tools noch via Azure-CLI gemacht. Das resultierte aber in einem über 760 MB fettem Docker-Image – ca. 400 MB nur für Azure-CLI + ein paar Buildtools (wie gcc). Für ein paar popelige REST-Calls ist das ja wohl mal absolut inakzeptabel!
Beim umschreiben des Tools, um die Calls manuell zu machen, eröffnete sich dann das ganze Kraut-und-Rüben-Feld, das Azure-CLI so wunderbar vor einem versteckt. Es gibt nicht nur eine API, sondern mehrere – für zum Teil unterschiedliche Funktionen des selben Service (bspw. ACRs). Und die brauchen auch alle unterschiedliche Logins. (Also entweder tatsächlich unterschiedliche Accounts, oder der Login-Mechanismus ist unterschiedlich)
Vielleicht schreibe ich zu dem Thema noch mal einen extra Rant …ähm, Artikel.
Immerhin hat es sich gelohnt. Danach war das Image nur noch 138 MB groß.

Nachdem wir also die Basics haben, wie prüfen wir, ob das Basis-Image noch gepflegt wird? Nun, nach eingehender Recherche, sind wir zu dem Schluss gekommen: hier hilft tatsächlich nur Brute-Force! Wir gehen schlicht und ergreifend davon aus, dass ein Image (genauer: der verwendete Tag eines Image) nicht mehr gepflegt wird, wenn seit X Tagen kein Update mehr kam. Und für den Fall, dass die Annahme bei einem bestimmten Image nicht stimmt, ermöglichen wir noch, Repositories von dem Check aus zunehmen.
Da die gängigen Basis-Images, wie bspw. „ubuntu“, „alpine“, „python“ oder „openjdk“, aber regelmäßig Updates erfahren, funktioniert diese Herangehensweise erstaunlich gut.

Daraus ergibt sich nun aber wieder eine andere Anforderung! Denn ein veraltetes Parent-Image, ist nichts, was sich automatisiert lösen ließe. Da muss jemand drauf schauen und ggfs. tätig werden. Und zwar am besten jemand von dem Team, dass das Image/Repo angelegt hat.
Wir brauchen also ein Reporting. Eine dedizierte Webseite scheidet aus – da guckt eh keiner drauf! Es gilt also: simplicity is king. Und was ist das einfachste? Genau: einen HTML-Report per Mail verschicken.

Das könnte dann z.B. so aussehen:

Auch dazu nutzen wir wieder den Mail-Account eines CI-User. Es empfiehlt sich im Zweifelsfall immer, über etablierte Mail-Server zu versenden – wenn ihr einfach nur den lokalen smtpd benutzt, bleibt die Mail mit an Sicherheit grenzender Wahrscheinlichkeit im Spamfilter vom Zielserver hängen! (Falls sie überhaupt soweit kommt! Wenn der Zielserver bspw. Graylisting aktiv hat, dann müsstet ihr die Mail noch mal senden, aber der Dockercontainer mit dem Tool ist nach getaner Arbeit schon wieder beendet – alles nicht wirklich praktikabel!)

Was jetzt noch fehlt ist eine Pipeline, um das Tool regelmäßig laufen zu lassen. Die ganzen Credentials sollte man dazu in einen KeyVault packen, damit die da nicht im Klartext rum liegen!
Detailliertere Erläuterungen befinden sich in der ReadMe des Projekts: https://github.com/hmg-dev/docker-update-scanner

Und wer Ideen, Vorschläge und Verbesserungen einbringen möchte, ist herzlich dazu eingeladen.
Denn wir haben das Tool natürlich primär für unsere Situation ausgelegt. Entsprechend gibt es also ein paar

Einschränkungen und Voraussetzungen

Derzeit unterstützt das Tool nur Microsoft Azure – und halt DockerHub für Parent-Images.
Mehrere Dockerfiles und/oder Pipelines pro Projekt/Repo funktioniert auch nicht. Und wo wir gerade von Pipelines sprechen: da gibt es auch ein paar Dinge zu beachten. Aber das steht im Detail ebenfalls in der Readme.
Das ein (CI-)User mit entsprechenden Zugriffsrechten vorhanden sein muss, hatte ich ja weiter oben schon erwähnt.

Wenn ihr Dockerimages von DockerHub am Laufen habt, ohne davon ein eigenes Image zu bauen, dann wird das von diesem Tool nicht abgedeckt. Etwas entsprechendes dafür zu bauen, steht zwar auf meiner TODO-Liste – aber bis das umgesetzt ist, müsst ihr da selber noch ein Auge drauf haben 😉

Und wenn eure Build-Pipelines nicht automatisch ein Release triggern, dann muss das natürlich auch noch manuell angestoßen werden, falls erforderliche – aber das lässt sich dann ja dem Report entnehmen.

Fazit

Also was haben wir jetzt davon und welche Erkenntnisse nehmen wir mit?
Nun zu allererst natürlich: Docker-Image müssen regelmäßig geupdated werden. Bei allem Komfort nimmt uns Docker das (noch?) nicht ab!
Aber auch dutzende von eigenen Docker-Images sind kein Grund den Kopf in den Sand zu stecken. Und wenn das Tool vielleicht nur 80% eurer im Einsatz befindlichen Images abdeckt, sind das immerhin 80% die ihr nicht manuell checken müsst.

Denn: fire-and-forget ist nicht! Docker-Images müssen auch geupdated werden – damit wir damit nicht das nächste „Internet of Shit“ erleben!