Docker für Java-Entwickler: 5 Dinge, die Sie für die Sicherheit wissen müssen
20. November 2020
0 Min. LesezeitDocker ist die am weitesten verbreitete Methode, Anwendungen in Containern bereitzustellen. Mit Docker Hub lassen sich vorgefertigte Images einfach erstellen und herunterladen. Das ist sehr praktisch: Sie können diese Images von Docker Hub verwenden, um schnell ein Image für Ihre Java-Anwendung zu erstellen. Doch die unüberlegte Erstellung eigener Docker-Images für Java-Anwendungen birgt zahlreiche Sicherheitsrisiken. Wie wird Sicherheit also zu einem unverzichtbaren Bestandteil von Docker für Java-Entwickler?
Bevor wir uns ansehen, wie Sie ein hervorragendes Docker-Java-Image für Ihre Anwendung erstellen, werfen wir einen Blick auf einige häufig gestellte Fragen zu diesem Thema.
Wie containerisiere ich Java-Anwendungen?
Sie können Ihre Java-Anwendung ganz einfach in einem Docker-Container ausführen, indem Sie die Datei .jar oder .war direkt in ein JRE-Basis-Image kopieren. Dabei gibt es jedoch einige Punkte zu beachten. Die richtigen JVM-Argumente und passende Einstellungen für die Container-Laufzeitumgebung sind nur die halbe Miete. Die Wahl des Basis-Images ist aus Sicherheitssicht äußerst wichtig, denn eine schlechte Wahl kann zu Schwachstellen führen.
Dieser Artikel hilft Ihnen, die Auswirkungen der Wahl des Basis-Images besser zu verstehen und das sicherste verfügbare Image für Ihre Anwendung zu finden.
Wie ist Docker für Java-Entwickler hilfreich?
Wenn Sie Ihre Java-Anwendung in einem Container verpacken, können Sie die gesamte Anwendung definieren – einschließlich JRE, Konfigurationseinstellungen, Abhängigkeiten auf Betriebssystemebene und Build-Artefakten. Das Ergebnis sind eigenständige, bereitstellbare Artefakte, sogenannte Container-Images. Diese Images werden als Software definiert. Dadurch lassen sie sich jederzeit reproduzierbar erstellen und bieten Entwicklern die Möglichkeit, in allen Umgebungen dieselbe Plattform auszuführen. Außerdem können Entwickler mit Containern neue Plattformversionen oder andere Änderungen leichter direkt auf ihrem Desktop ausprobieren, ohne besondere Berechtigungen zu benötigen.
Wählen Sie das passende Docker-Basis-Image für Ihre Java-Anwendung
Beim Erstellen eines Docker-Images verwenden wir ein Image, das wir von Docker Hub herunterladen, als Grundlage. Dieses nennen wir Basis-Image. Es bildet das Fundament des neuen Images, das Sie für Ihre Java-Anwendung erstellen. Das gewählte Basis-Image ist entscheidend, denn Sie können alles nutzen, was darin enthalten ist. Das hat jedoch seinen Preis: Enthält ein Basis-Image eine Schwachstelle, übernehmen Sie diese in Ihr neu erstelltes Image.
Bei Basis-Images finden sich viele Schwachstellen in der Betriebssystemebene (OS), die das Image verwendet. In unserer früheren Studie von 2019, Shifting Docker security left, haben wir bereits gezeigt, dass die durch die OS-Ebene eingebrachten Schwachstellen je nach gewählter Variante stark variieren können.

Bericht 2019 – Shifting Docker security left
Sehen wir uns eine beliebte Reihe von Docker-Java-Basis-Images von Adoptopenjdk an: openjdk11. Mit dem Standard-Tag basiert dieses Image auf einer Ubuntu-Distribution. Sie können aber auch Tags für bestimmte Versionen auswählen, die beispielsweise auf Debian, Centos oder Alpine basieren. (Hinweis: Alpine basiert nicht auf glibc und ist möglicherweise nicht mit Anwendungen kompatibel, die native JNI-Aufrufe ausführen.)

Die Wahl des passenden Basis-Images ist aus Sicherheitssicht entscheidend. Wahrscheinlich benötigen Sie nicht alle Binärdateien, die mit einem vollständigen Betriebssystem geliefert werden. Erstellen Sie Ihr neues Docker-Java-Image für die Anwendung am besten auf Grundlage eines minimalen Basis-Images. Was Sie nicht haben, kann Ihnen nicht schaden.
Neben den Sicherheitsvorteilen verringert ein minimales Basis-Image auch die Größe Ihres neu erstellten Images. Ein kleineres Docker-Image bedeutet außerdem einen geringeren Platzbedarf und wahrscheinlich eine schnellere Startzeit. Eine weitere Möglichkeit ist der Build mit jib, wodurch ein minimales Java-Image erstellt wird, für das keine Dockerfile erforderlich ist.
Verwenden Sie eine JRE statt einer JDK
Beim Erstellen eines Docker-Images sollten wir nur die Ressourcen zuweisen, die für den ordnungsgemäßen Betrieb erforderlich sind. Beginnen Sie daher mit einer passenden Java-Laufzeitumgebung (JRE) für Ihr Produktions-Image statt mit dem vollständigen Java Development Kit (JDK). Außerdem sollte Ihr Produktions-Image kein Build-System wie Maven oder Gradle enthalten. Das Ergebnis des Builds, beispielsweise Ihre JAR-Datei, sollte ausreichen.
Auch wenn Sie Ihre Anwendung in einem Docker-Container erstellen möchten, können Sie das Build-Image mit einem mehrstufigen Build ganz einfach vom Produktions-Image trennen.
Beispiel:Ich möchte ein Docker-Java-Image für meine Anwendung java-code-workshop erstellen. Sie basiert auf Spring Boot, wurde mit Maven erstellt und benötigt Java Version 8.
Eine unüberlegte Vorgehensweise beim Erstellen dieses Docker-Java-Images könnte so aussehen:

Ich habe ein Basis-Image mit Maven und openjdk8 ausgewählt, meinen Quellcode in das Image kopiert und Maven aufgerufen, um meine Anwendung zu erstellen und auszuführen. Dieses Beispiel funktioniert einwandfrei. Meine Anwendung startet und läuft problemlos. Das erstellte Docker-Image ist jedoch 631 MB groß.
Ändern wir diese Dockerfile und verwenden einen mehrstufigen Build:

Jetzt verwende ich weiterhin das Image maven-openjdk8, um mein Projekt zu erstellen. Dieses Image wird jedoch nicht das Ergebnis sein. Stattdessen erstelle ich ein neues Image auf Grundlage eines deutlich kleineren Java-8-JRE-Images und kopiere nur die ausführbare Spring-Boot-JAR-Datei hinein. Jetzt muss ich nur noch die jar-file ausführen – und fertig! Das resultierende Docker-Image enthält weder das JDK noch Maven, sondern nur die JRE. Die Image-Größe sinkt drastisch auf 132 MB.
Kleinere Images lassen sich nicht nur einfacher hochladen und sparen Startzeit, sondern sind auch deutlich sicherer. Stellen Sie sich vor, was passieren könnte, wenn ein Angreifer Zugriff auf einen laufenden Container erhält, in dem das JDK, Ihr Quellcode und ein Build-Tool verfügbar sind.
Das ist auch nützlich, wenn Sie Secrets für den Zugriff auf ein privates Repository einbinden müssen. Solche Secrets sollen nicht im Cache Ihres Produktions-Images landen. Da Sie das Build-Image nicht in der Produktion einsetzen, können Sie die Secrets dort problemlos verwenden. Mit dieser Technik können Sie benötigte Komponenten aus anderen Images auswählen und ein Docker-Produktions-Image erstellen, das nur die erforderlichen Ressourcen enthält.
Führen Sie Ihren Docker-Container nicht als Root aus
Ein Docker-Container wird standardmäßig als Root ausgeführt. Das ist zwar bei der Entwicklung praktisch, sollte aber in Ihren Produktions-Images vermieden werden. Angenommen, ein Angreifer erhält aus irgendeinem Grund Zugriff auf ein Terminal oder kann Code ausführen. Dann verfügt er über umfangreiche Berechtigungen im laufenden Container und kann möglicherweise über Dateisystem-Bind-Mounts mit unangemessen weitreichenden Zugriffsrechten auch auf Host-Dateisysteme zugreifen.
Am einfachsten verhindern Sie das, indem Sie einen eigenen Benutzer erstellen, etwa so:

In der dritten Zeile erstelle ich eine neue Gruppe und füge einen Benutzer hinzu. Dieser Benutzer ist ein Systembenutzer (-r) ohne Passwort und Home-Verzeichnis. Außerdem füge ich ihn der neu erstellten Gruppe hinzu.
Anschließend erteile ich dem Benutzer in Zeile 6 die Berechtigung für den Anwendungsordner. Vergessen Sie Zeile 7 nicht. Dort lege ich fest, welcher Benutzer verwendet werden soll. So führt der neu erstellte, eingeschränkte Benutzer den Befehl in der letzten Zeile aus.
Scannen Sie Ihr Docker-Image und Ihre Java-Anwendung während der Entwicklung
Beim Erstellen eines Docker-Images aus einer Dockerfile und sogar beim erneuten Erstellen eines Images können neue Schwachstellen in Ihr System gelangen. Das Scannen Ihrer Docker-Images während der Entwicklung sollte zu Ihrem Workflow gehören, damit Sie Schwachstellen möglichst früh erkennen.
Mit der Snyk CLI können Sie Ihr Docker-Image ganz einfach scannen. Verwenden Sie sie auf Ihrem lokalen Rechner, als Teil Ihrer Pipeline oder beides. Nachdem Sie die Snyk CLI installiert und authentifiziert haben, müssen Sie zum Scannen eines Images nur Folgendes ausführen:
Wenn ich wie im ersten Abschnitt erwähnt ein adoptopenjdk-Image scannen möchte, sehen die Befehle so aus:
Ausgabe:

Sie können das Docker-Image sowohl testen als auch überwachen. Für die Überwachung verwenden Sie snyk container monitor <image>. Dabei wird eine Momentaufnahme erstellt und im Zeitverlauf geprüft, ob neue Schwachstellen oder Korrekturen für Ihr Image verfügbar sind.
Wenn Sie ein Image scannen und über die Dockerfile verfügen (weil Sie ein neues Docker-Java-Image erstellt haben), sollten Sie sowohl bei snyk container test als auch bei snyk container monitor das Flag --Dockerfile=<dockerfile> hinzufügen. So erhalten Sie bessere Empfehlungen zur Behebung. Wenn beispielsweise ein Basis-Image verfügbar ist, mit dem sich die Anzahl der Schwachstellen verringern lässt, erfahren Sie davon.
Beispiel:
Scannen Sie Ihre Java-Anwendung
Das Docker-Java-Image, das Sie erstellen, enthält auch Ihre Anwendung. Auch sie kann natürlich angegriffen werden. Damit Docker für Java-Entwickler von Anfang an eine sichere Entscheidung ist, müssen Sie sicherstellen, dass Ihre Java-Anwendung frei von Sicherheitsschwachstellen ist. Stellen Sie sich vor, Ihre Anwendung enthält eine Bibliothek, die bei einem Aufruf eines REST-Endpunkts die Ausführung von Remote-Code ermöglicht. Selbst wenn der Rest Ihres Images keine Schwachstellen aufweist, könnte das katastrophale Folgen haben.
Der Großteil der Java-Binärdateien in Ihrem Docker-Image besteht wahrscheinlich aus importiertem Code. Die Bibliotheken und Frameworks Ihrer Anwendung können Sie als Abhängigkeiten betrachten. Mit der Snyk CLI lassen sich diese Abhängigkeiten ganz einfach prüfen. Es ist dieselbe CLI, mit der wir zuvor unser Image gescannt haben. Führen Sie im Stammverzeichnis snyk test oder snyk monitor aus, um Ihre Anwendung auf Sicherheitsschwachstellen in den Bibliotheken zu scannen oder zu überwachen.
Für den selbst geschriebenen Code empfiehlt sich ein Code-Analyse-Tool oder Linter wie SonarLint, PMD oder spotbugs. Diese Tools sind zwar für allgemeine Verbesserungen der Codequalität gedacht, helfen Ihnen aber auch dabei, offensichtliche Sicherheitsfehler zu vermeiden.
Erstellen, um neu erstellen zu können
Erstellen Sie Ihre Java-Anwendung für das Docker-Image so, dass Sie das Image jederzeit verwerfen und neu erstellen können. Angenommen, Sie stellen fest, dass mit Ihrem laufenden Container etwas nicht stimmt. Dann wäre es praktisch, ihn einfach zu beenden und eine neue Instanz zu starten. Dafür müssen Sie zustandslose Java-Anwendungen entwickeln, bei denen die Daten außerhalb des Containers gespeichert werden. Beachten Sie dabei Folgendes:
Betreiben Sie keinen Datenspeicher oder keine Datenbank in Ihrem Container.
Speichern Sie keine (Protokoll-)Dateien in Ihrem Container.
Stellen Sie sicher, dass sich Ihr Cache bei Bedarf automatisch wiederherstellt.
Wenn Sie Ihre Anwendung so erstellen, dass Sie sie jederzeit verwerfen und eine neue Instanz starten können, können Sie auch Ihr gesamtes Docker-Image problemlos neu erstellen. Wussten Sie, dass sich bei 20 % der anfälligen Docker-Images ein oder mehrere Sicherheitsprobleme allein durch das erneute Erstellen des Images beheben lassen? Docker-Images basieren häufig auf dem „latest“-Tag eines Basis-Images. Diese „latest“-Versionen ändern sich im Laufe der Zeit und werden durch neuere, verbesserte Versionen ersetzt. Dasselbe gilt für wichtige Binärdateien, die mithilfe von Paketmanagern wie apt oder yum in Ihrem Container installiert werden. Die jeweils neueste Version zu verwenden, ist aus Sicherheitssicht natürlich sinnvoll, da Sie so automatisch die neuesten Sicherheitskorrekturen erhalten. Sie müssen dies jedoch gegen den Umstand abwägen, dass sich Ihr Basis-Image im Laufe der Zeit ändert und es dadurch schwieriger wird, Ihr Image zu einem bestimmten Zeitpunkt exakt nachzubilden.
Erstellen Sie Ihr Docker-Image regelmäßig neu, auch wenn sich Ihre Anwendung nicht geändert hat – möglicherweise mit einem neueren oder aktuellen Tag der Basis-Image-Version. Verbesserungen in den zugrunde liegenden Ebenen, etwa der Betriebssystemebene, können die Qualität Ihres Images verbessern und Sicherheitsschwachstellen reduzieren.
https://www.youtube.com/watch?v=v2SkWn-ZRDg
Zusammengefasst: Wenn Sie sich über bewährte Sicherheitsverfahren für die Erstellung optimaler Docker-Images im Allgemeinen oder für Java-Anwendungen informieren möchten:
10 Best Practices zum Erstellen eines Java-Containers mit Docker – eine ausführliche Schritt-für-Schritt-Anleitung zum Erstellen sicherer und leistungsfähiger Docker-Images für Ihre Java-Anwendungen
10 bewährte Sicherheitsverfahren für Java – Sicherheitsmaßnahmen, die Sie beim Erstellen von Java-Anwendungen für jede Umgebung beachten sollten.
Starten Sie mit Capture the Flag
Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.
