Skip to main content

Best Practices für die Containerisierung von Python-Anwendungen mit Docker

Artikel von

Daniel Campos Olivares

blog feature snyk python security

11. November 2021

0 Min. Lesezeit

Bei der Lektüre zahlreicher Blogs zu Python-Docker-Containern ist uns aufgefallen, dass die meisten Beiträge Beispiele für die Containerisierung von Python-Anwendungen unabhängig vom verwendeten Framework (Django, Flask, Falcon usw.) zeigen. Zum Beispiel könnte Ihnen Folgendes begegnen:

FROM python
WORKDIR /usr/app
COPY . .
RUN pip install -r requirements.txt
CMD [ "python", "app.py" ]

Mit diesem Dockerfile können wir eine Python-Flask-Anwendung erstellen und ausführen:

docker build -t flask-application .
docker run -p 8080:5000 flask-application

Zwei einfache Schritte und schon funktioniert alles, oder?

Dieses Beispiel ist zwar einfach und eignet sich gut für Demos und Einführungen, lässt jedoch viele wichtige Aspekte außer Acht. Deshalb gehen wir in diesem Beitrag auf diese Herausforderungen ein und stellen sechs Best Practices für die Containerisierung von Python-Anwendungen mit Docker vor. Wir zeigen, warum Sie Folgendes tun sollten:

  1. Verwenden Sie für containerisierte Python-Anwendungen explizite und deterministische Docker-Basis-Image-Tags.

  2. Trennen Sie Abhängigkeiten vom Quellcode.

  3. Verwenden Sie Python WSGI in der Produktionsumgebung.

  4. Führen Sie Container mit möglichst geringen Berechtigungen aus (und niemals als Root).

  5. Behandeln Sie fehlerhafte Zustände Ihrer Anwendung.

  6. Finden und beheben Sie Sicherheitslücken in Ihrem Python-Docker-Anwendungs-Image.

1. Verwenden Sie für containerisierte Python-Anwendungen explizite und deterministische Docker-Basis-Image-Tags

Es mag logisch erscheinen, python als Basis-Image für unsere Dockerisierte Python-Anwendung zu verwenden. Dabei bleibt jedoch die Frage offen, welche Python-Version zum Einsatz kommt.

Zum Zeitpunkt der Veröffentlichung dieses Artikels verweist das oben erwähnte Basis-Image in der Dockerfile auf ein Basis-Image mit Python 3.10. Warum? Da wir kein spezifisches Tag angegeben haben, wurde standardmäßig die Version :latest dieses Basis-Images verwendet. Auf der offiziellen Image-Seite auf Docker Hub sehen wir, dass es sich dabei um 3.10 handelt.

Da wir kontrollieren möchten, welche Python-Versionen wir containerisieren, sollten wir die Versionsangabe immer in der Dockerfile festlegen.

Wenn ich also Python Version 3.10 verwenden möchte, muss ich einfach das Tag :3.10 zu meiner Dockerfile hinzufügen, oder?

Nun ja … nicht ganz.

Das Docker-Basis-Image-Tag :3.10 steht für ein vollständiges Betriebssystem, auf dem Python 3.10 installiert ist. Es enthält zahlreiche Bibliotheken, die Sie wahrscheinlich nie verwenden werden. Die große Menge an enthaltener Software hat zudem einen Nebeneffekt: Sie vergrößert aufgrund der Sicherheitslücken in diesen Bibliotheken Ihre Angriffsfläche.

Ein großes Python-Docker-Image ist schwieriger zu warten und die Versionen all dieser Bibliotheken auf dem neuesten Stand zu halten.

Wenn wir ein Python-Basis-Image mit dem Snyk Advisor-Tool untersuchen, sehen wir, dass das Docker-Basis-Image python:3.10 12 Probleme mit hohem, 27 mit mittlerem und 132 mit niedrigem Schweregrad aufweist. Das Python-Docker-Image bringt also standardmäßig mindestens 171 Sicherheitslücken mit – und wir haben noch gar nichts hinzugefügt!

Da wir außerdem ein vollständiges Betriebssystem einführen, ist das Basis-Image für unseren Python-Anwendungsserver ziemlich groß. Das führt zu langsameren Builds und benötigt mehr Speicherplatz. Bei der Auswahl eines Basis-Images gibt es einige Regeln, doch zwei davon sind besonders wichtig.

Best Practices für die Auswahl eines Python-Docker-Images

  1. Wählen Sie das kleinste Basis-Image, das alle Ihre Anforderungen erfüllt, und bauen Sie darauf auf. Kleinere Images enthalten weniger Sicherheitslücken, verbrauchen weniger Ressourcen und kommen mit weniger unnötigen Paketen aus.

  2. Die Verwendung eines benannten Tags allein garantiert nicht, dass Sie immer dasselbe Basis-Image verwenden. Das lässt sich nur mit dem Image-Digest sicherstellen.

Mit diesem Wissen werfen wir noch einmal einen Blick auf Snyk Advisor und prüfen, welche alternativen Tags empfohlen werden. Das Tool bietet einen hervorragenden Überblick über die Sicherheitslücken und die Größe der Basis-Images und hilft uns so bei der Entscheidung.

Da wir Python 3.10 in unserer Anwendung verwenden möchten, suchen wir nach genau diesem Tag.

Snyk Advisor-Seite für das offizielle Python-Docker-Image mit dem Befehl docker pull und Empfehlungen für alternative Tags samt Angaben zu Schweregrad, Aktualisierung und Größe

Abgesehen von den bereits erwähnten Sicherheitslücken sehen wir, dass dieses Image etwa 350 MB groß ist, auf Debian 11 basiert und 427 installierte Pakete enthält. Für unsere kleine Python-Anwendung wäre dieses Image unserer Einschätzung nach etwas zu umfangreich.

Andererseits gibt es auch das Docker-Basis-Image für Python mit dem Tag :3.10-slim. Es weist 1 Problem mit hohem, 1 mit mittlerem und 35 mit niedrigem Schweregrad auf. Das Docker-Basis-Image ist 46,2 MB groß, basiert ebenfalls auf Debian 11 und enthält 106 installierte Pakete. Wenn wir dieses Docker-Basis-Image statt des Standard-Images für unseren Python-Anwendungsserver auswählen, verringern wir die Anzahl der Sicherheitslücken, den Speicherplatzbedarf und die Zahl der installierten Bibliotheken. Gleichzeitig erfüllt es unsere Anforderungen an die Python-Anwendungsversion :3.10.

Das war’s also! Ich füge einfach das Tag :3.10-slim hinzu und kann loslegen!

Fast! Die erste Regel haben wir erfüllt – wir haben ein kleines Docker-Basis-Image gewählt, das unseren Anforderungen entspricht. Doch das zweite Thema müssen wir noch angehen: Wir müssen sicherstellen, dass wir bei jedem Build unseres Python-Anwendungsservers exakt dasselbe Docker-Basis-Image verwenden.

Dafür gibt es mehrere Möglichkeiten:

  1. Den Docker-Basis-Image-Digest von Docker Hub abrufen.

  2. Das Docker-Image mit docker pull python:3.10-slim auf unseren Computer herunterladen. Dadurch wird der Digest des Docker-Images angezeigt:

3.10-slim: Pulling from library/python
7d63c13d9b9b: Pull complete
6ad2a11ca37b: Pull complete
1d79bc863ed3: Pull complete
c72b5f03bec8: Pull complete
0c3b0c5ce69b: Pull complete
Digest: sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d
Status: Downloaded newer image for python:3.10-slim
docker.io/library/python:3.10-slim

Wenn das Python-Docker-Image bereits auf unserem Computer vorhanden ist, können wir den Digest des aktuell auf der Festplatte gespeicherten Images mit dem Befehl docker images --digests | grep python abrufen:

python    3.10-slim    sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d

Sobald wir den Digest des Basis-Images haben, können wir ihn einfach zur oben genannten Dockerfile hinzufügen:

Dockerfile

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d
WORKDIR /usr/app
COPY . .
RUN pip install -r requirements.txt
CMD [ "python", "app.py" ]

So ist sichergestellt, dass beim erneuten Erstellen des Docker-Images für diese Python-Anwendung jedes Mal dasselbe zugrunde liegende Betriebssystem mit denselben Bibliotheksversionen verwendet wird. Dadurch wird der Build deterministisch.

2. Trennen Sie Abhängigkeiten vom Quellcode

Diese zweite Best Practice verhindert einen der häufigsten Fehler bei Docker-Images für Projekte mit Abhängigkeiten. Zunächst ein Beispiel für eine schlechte Vorgehensweise:

  1. Alles aus unserem Projektordner in den Image-Kontext kopieren.

  2. Die Abhängigkeiten installieren.

  3. Die Anwendung ausführen.

Das funktioniert zwar, aber es gibt einiges zu verbessern. Wenn Sie ein Projekt lokal entwickeln, installieren Sie die Abhängigkeiten doch auch nur, wenn sie sich ändern, oder? Zwingen Sie Ihr Docker-Image also nicht dazu, sie bei jeder noch so kleinen Codeänderung erneut herunterzuladen und zu installieren.

Bei dieser Best Practice geht es darum, die Ebenen eines Docker-Images zu optimieren. Wenn wir beim Docker-Build das Caching nutzen möchten, sollten wir beim Schreiben einer Dockerfile immer eines beachten: Die Ebenen sollten danach geordnet sein, wie wahrscheinlich es ist, dass sie sich ändern.

Werfen wir einen Blick auf die Dockerfile, die wir bisher verwendet haben. Bei jedem Build dieses Python-Anwendungs-Images prüft Docker die verschiedenen Ebenen und fragt: Hat sich etwas geändert, oder kann ich einfach das verwenden, was ich bereits erstellt habe?

In der aktuellen Fassung der Dockerfile führt jede Änderung in unserem Projektordner dazu, dass die Anweisung COPY erneut ausgeführt wird – und in der Folge auch alle weiteren Build-Ebenen. Das ergibt wenig Sinn und lässt viel Raum für Optimierungen und eine Beschleunigung.

Verbessern wir das mit der folgenden Dockerfile:

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d
WORKDIR /usr/app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .
CMD [ "python", "app.py" ]

Mit dieser neuen Dockerfile überspringt Docker beim nächsten Prüfen, ob Ebenen wiederverwendet werden können, direkt zur Anweisung COPY, sofern sich die Datei requirements.txt nicht geändert hat. Das dauert nur wenige Sekunden. Diese kleine Änderung beschleunigt den Build-Prozess erheblich – Sie müssen nicht mehr minutenlang zwischen Builds warten, wenn Sie etwas am Code ändern.

Wichtig ist, dass möglicherweise nicht alle Ihre Abhängigkeiten als wheels gepackt sind. In diesem Fall muss ein Compiler in Ihrem Image installiert werden.

Aber Sie haben doch gesagt, wir sollen für die Ausführung der Anwendung ein möglichst kleines Image verwenden!

Das stimmt. Deshalb zeigen wir Ihnen jetzt eine weitere großartige Docker-Funktion: Multi-Stage-Builds.

Multi-Stage-Builds

Bei einem Multi-Stage-Build verwenden wir ein Docker-Image mit zusätzlichen Tools, um die benötigten Abhängigkeiten zu kompilieren. Anschließend kopieren wir nur die erforderlichen Artefakte in das tatsächlich verwendete Python-Docker-Image.

Ein Beispiel für Anwendungen auf Basis von Node.js:

FROM node:latest AS build
ARG NPM_TOKEN
WORKDIR /usr/src/app
COPY package*.json /usr/src/app/
RUN npm install

FROM node:lts-alpine@sha256:b2da3316acdc2bec442190a1fe10dc094e7ba4121d029cb32075ff59bb27390a
WORKDIR /usr/src/app
COPY --from=build /usr/src/app/node_modules /usr/src/app/node_modules
COPY . .
CMD ["node", "server.js"]

Hinweis: Dies ist ein kurzes Beispiel, das lediglich die Möglichkeiten von Multi-Stage-Builds veranschaulicht. Wenn Sie mehr über Best Practices für die Containerisierung von Node.js-Anwendungen erfahren möchten, lesen Sie diesen Artikel von Liran Tal (kommt Ihnen der Name bekannt vor …?) und Yoni Goldberg.

Multi-Stage-Builds für Node.js lassen sich recht einfach handhaben, da sich der Ordner node_modules im selben Ordner wie das eigentliche Projekt befindet. Bei Python-Anwendungen ist das jedoch nicht der Fall.

Wenn wir einfach pip install ausführen, werden zahlreiche Dateien an unterschiedlichen Speicherorten installiert, sodass ein Multi-Stage-Build nicht möglich ist. Dafür gibt es zwei mögliche Lösungen:

  1. pip install --user verwenden

  2. Ein virtualenv verwenden

pip install --user könnte eine gute Option sein, da alle Pakete im Verzeichnis ~/.local installiert werden und sich deshalb problemlos von einer Phase in die nächste kopieren lassen. Dadurch entsteht jedoch ein anderes Problem: Sie würden alle Abhängigkeiten auf Systemebene aus dem Image, das wir zum Kompilieren der Abhängigkeiten verwendet haben, zum endgültigen Docker-Basis-Image hinzufügen. Das möchten wir vermeiden (denken Sie an die Best Practice, ein möglichst kleines Docker-Basis-Image zu verwenden).

Nachdem die erste Option wegfällt, sehen wir uns die zweite an: die Verwendung eines virtualenv. Dann würde die Dockerfile wie folgt aussehen.

FROM python:3.10-slim as build
RUN apt-get update
RUN apt-get install -y --no-install-recommends \
	      build-essential gcc 

WORKDIR /usr/app
RUN python -m venv /usr/app/venv
ENV PATH="/usr/app/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install -r requirements.txt

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d
WORKDIR /usr/app/venv
COPY --from=build /usr/app/venv ./venv
COPY . .

ENV PATH="/usr/app/venv/bin:$PATH"
CMD [ "python", "app.py" ]

Nun haben wir alle benötigten Abhängigkeiten, aber keine zusätzlichen Pakete, die zum Kompilieren erforderlich sind.

Bekannte Probleme mit Multi-Stage-Builds für containerisierte Python-Anwendungen

Derzeit gibt es ein bekanntes Problem: vorherige Phasen werden nicht im Cache gespeichert. Am einfachsten lässt es sich lösen, indem Sie BuildKit verwenden und das Argument BUILDKIT_INLINE_CACHE=1 zum Build-Prozess hinzufügen.

Beim ersten Mal erstellen wir das Image ganz normal. Für nachfolgende Builds verwenden wir dann den folgenden Befehl:

export DOCKER_BUILDKIT=1
docker build -t flask-application --cache-from flask-application --build-arg BUILDKIT_INLINE_CACHE=1 .

Sie benötigen die Umgebungsvariable DOCKER_BUILDKIT=1, damit Docker weiß, dass BuildKit für den Build-Prozess verwendet werden soll. Sie können es auch aktivieren, indem Sie die folgenden Einstellungen zur Konfigurationsdatei der Docker-Software unter /etc/docker/daemon.json hinzufügen (dann wird die Umgebungsvariable nicht benötigt):

{ "features": { "buildkit": true } }

3. Verwenden Sie Python WSGI in der Produktionsumgebung

Python-Anwendungen, die für den Produktionseinsatz bestimmt sind und in denen der Debug-Modus aktiviert bleibt, sind ein absolutes No-Go – und ein Sicherheitsvorfall ist vorprogrammiert. Leider ist das ein häufiger Fehler, den wir in zahlreichen Blogartikeln über containerisierte Python-Flask-Anwendungen und andere Python-Anwendungs-Frameworks auf Basis von WSGI beobachtet haben.

Der Debugger ermöglicht es, beliebigen Python-Code im Browser auszuführen. Das stellt ein enormes Sicherheitsrisiko dar. Dieses Risiko lässt sich zwar bis zu einem gewissen Grad mindern, bleibt aber immer eine Schwachstelle. Zur Erinnerung – der Sicherheit zuliebe: Führen Sie in einer Produktionsumgebung niemals Entwicklungsserver oder einen Debugger aus! Das gilt auch für Ihre containerisierten Python-Anwendungen.

Wir verstehen, dass es einfacher ist, Ihre Python-Flask-Anwendungen im Debug-Modus bereitzustellen, als einen WSGI-Server und den Webserver oder Proxy einzurichten. Doch einfacher ist es nur so lange, bis Sie erklären müssen, warum Ihre Anwendung gehackt wurde.

Wie lässt sich das also beheben? Entscheiden Sie zunächst, welche WSGI-Server-Implementierung Sie verwenden möchten. Für Python-Anwendungen kommen vor allem diese vier verbreiteten Optionen infrage:

  • Green Unicorn (Gunicorn) – Ein Pre-Fork-Worker-Modell, das auf dem Unicorn-Projekt aus Ruby basiert.

  • uWSGI – Eine vielseitige, leistungsstarke und ressourcenschonende WSGI-Server-Implementierung.

  • mod_wsgi — Ein Apache-Modul, das jede Python-Webanwendung hosten kann, die die Python-WSGI-Spezifikation unterstützt.

  • CherryPy — Ein Python-Framework für HTTP, das objektorientiert ist und zugleich als WSGI-Server fungiert.

In diesem Artikel verwenden wir Gunicorn als Beispiel. Lesen Sie aber gern die Dokumentation und Informationen zu allen Optionen, damit Sie die für Ihre Anforderungen am besten geeignete auswählen können. In diesem Beitrag befassen wir uns nicht mit der Konfiguration, da diese vollständig vom jeweiligen Anwendungsfall abhängt.

Für die Containerisierung müssen wir lediglich:

  1. die Abhängigkeit gunicorn in requirements.txt aufnehmen

  2. den Einstiegspunkt des Python-Anwendungscontainers ändern (verwenden Sie dazu die Anweisung CMD):

FROM python:3.10-slim as build
RUN apt-get update
RUN apt-get install -y --no-install-recommends \
	build-essential gcc 

WORKDIR /usr/app
RUN python -m venv /usr/app/venv
ENV PATH="/usr/app/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install -r requirements.txt

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d
WORKDIR /usr/app
COPY --from=build /usr/app/venv ./venv
COPY . .

ENV PATH="/usr/app/venv/bin:$PATH"
CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "manage:app" ]

Nachdem wir die Python-Anwendung auf Grundlage dieser neuen Dockerfile neu erstellt haben, können wir sie ausführen und testen, ob die Flask-Anwendung bereit ist, Anfragen zu verarbeiten:

docker run -p 8080:5000 flask-application

Hinweis: Für produktionsreife Deployments empfehlen wir, den von Gunicorn freigegebenen Port nicht direkt an den Host zu binden. Richten Sie stattdessen einen Reverse-Proxy-Server im selben Netzwerk ein, der alle HTTP-Anfragen verarbeitet und statische Dateien ausliefert.

4. Containerisierte Python-Anwendungen mit möglichst wenigen Berechtigungen ausführen (und niemals als root)

Das Prinzip der minimalen Berechtigungen ist eine bewährte Sicherheitsmaßnahme, die bis in die Anfangszeit von Unix zurückreicht – und wir sollten es stets befolgen, wenn wir unsere containerisierten Python-Anwendungen ausführen.

Das offizielle Docker-Image python enthält standardmäßig keinen privilegierten Benutzer. Daher müssen wir einen erstellen, damit der Prozess mit einem Benutzer mit minimalen Berechtigungen ausgeführt werden kann.

Dazu fügen wir die folgenden groupadd-Befehle in die Dockerfile des finalen Images ein (die zweite Phase des mehrstufigen Builds), in der der Prozess gunicorn ausgeführt wird:

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d

RUN groupadd -g 999 python && \
    useradd -r -u 999 -g python python
USER 999
WORKDIR /usr/app

COPY --from=build /usr/app/venv ./venv
COPY . .

ENV PATH="/usr/app/venv/bin:$PATH"
CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "manage:app" ]

Das Problem ist jedoch, dass nach den vorherigen Änderungen der Benutzer python den Systemprozess besitzt, den Docker ausführt ... Was ist mit den Dateiberechtigungen der kopierten Dateien oder des Verzeichnisses WORKDIR? Standardmäßig erstellt der Docker-Compiler das Verzeichnis WORKDIR, falls es noch nicht vorhanden ist. Als Eigentümer wird dabei jedoch der Systembenutzer root festgelegt. Jeder Vorgang, bei dem in dieses Verzeichnis geschrieben wird, kann daher zu einem schwerwiegenden Fehler in unserer Anwendung führen. Auch die kopierten Dateien gehören standardmäßig root, wenn wir ihr Verhalten nicht ändern – selbst wenn wir den Benutzer bereits geändert haben.

So beheben wir das:

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d

RUN groupadd -g 999 python && \
    useradd -r -u 999 -g python python

RUN mkdir /usr/app && chown python:python /usr/app
WORKDIR /usr/app

COPY --chown=python:python --from=build /usr/app/venv ./venv
COPY --chown=python:python . .

USER 999

ENV PATH="/usr/app/venv/bin:$PATH"
CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "manage:app" ]

Mit den aktualisierten COPY-Anweisungen oben vermeiden wir unerwartete Probleme mit den Dateiberechtigungen im Verzeichnis WORKDIR und stellen sicher, dass alle Dateien demselben Benutzer gehören, unter dem der Prozess ausgeführt wird.

5. Fehlerhafte Zustände Ihrer containerisierten Python-Anwendung behandeln

Beim Deployment einer Anwendung sollten Sie mögliche unbehandelte Ereignisse oder Probleme berücksichtigen, die dazu führen können, dass Ihre Anwendung in einen fehlerhaften Zustand gerät: 1) Sie funktioniert nicht mehr, aber 2) der Prozess wird nicht beendet. In diesem Fall wird Ihr Container nicht benachrichtigt, und Ihr Python-Anwendungsserver läuft weiter, ohne noch auf HTTP-Anfragen zu reagieren.

Um das zu vermeiden, sollten Sie einen Health-Check-Endpunkt implementieren. Für die Zustandsprüfung Ihrer containerisierten Python-Anwendung empfehlen wir stets, einen HTTP-Endpunkt für monitoring oder health einzurichten. So stellen Sie sicher, dass die Anwendung weiterhin Nutzeranfragen erfolgreich verarbeiten kann. Bei einigen Python-Webframeworks wie Flask ist das ganz einfach (siehe Beispiel unten). In Kombination mit der Docker-Anweisung HEALTHCHECK können Sie den Zustand Ihrer Anwendung zuverlässig überwachen.

Das folgende Snippet einer Python-Flask-Anwendung fügt einen HTTP-Endpunkt /health hinzu:

@app.route('/health', methods=['GET'])
def health():
	# Handle here any business logic for ensuring you're application is healthy (DB connections, etc...)
    return "Healthy: OK"

„Sobald Sie diesen Endpunkt in Ihrer Anwendung eingerichtet haben, müssen Sie nur noch die Anweisung HEALTHCHECK in Ihre Dockerfile aufnehmen:

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d

RUN groupadd -g 999 python && \
    useradd -r -u 999 -g python python

RUN mkdir /usr/app && chown python:python /usr/app
WORKDIR /usr/app

COPY --chown=python:python --from=build /usr/app/venv ./venv
COPY --chown=python:python . .

USER 999

ENV PATH="/usr/app/venv/bin:$PATH"
CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "manage:app" ]
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD curl -f http://localhost:5000/health

Wenn Sie auf Kubernetes deployen, sollten Sie wissen, dass Dockerfile-Anweisungen für HEALTHCHECK ignoriert werden. Stattdessen benötigen Sie in Ihrer YAML-Datei eine entsprechende Kubernetes-Liveness-, Readiness- und Startup-Probe.

So setzen Sie die obige Anweisung HEALTHCHECK für Kubernetes-Deployments um:

...
   livenessProbe:
     httpGet:
       path: /health
       port: 5000
     initialDelaySeconds: 5
     periodSeconds: 30
     timeoutSeconds: 30
     failureThreshold: 3
...

Beachten Sie, dass diese Konfiguration im Abschnitt container spec der Pod- oder Deployment-YAML-Datei stehen muss. Mit einer Neustartrichtlinie von always oder unless_stopped wird unser Python-Anwendungscontainer immer neu gestartet, wenn er in einen fehlerhaften Zustand gerät.

6. Sicherheitslücken in Ihrem Python-Docker-Anwendungsimage finden und beheben

Wir haben bereits festgestellt, dass größere Docker-Basisimages einige Probleme mit sich bringen, etwa einen umfangreichen Software-Stack, den wir warten und mit Sicherheitsupdates auf dem neuesten Stand halten müssen.

Wir haben auch untersucht, wie Sie mit Snyk Advisor die Größe und die Kennzahlen zu Schwachstellen verschiedener Basisimages ermitteln können. Doch Advisor ist nur die Spitze des Eisbergs. Snyk ist eine kostenlose Sicherheitsplattform für Entwickler. Damit können Sie alles testen – von Ihrem eigenen Python-Code über Ihre Python-Abhängigkeiten (zum Beispiel in requirements.txt) und das Python-Container-Image, in dem Ihre Anwendung ausgeführt wird, bis hin zu den Terraform- oder Kubernetes-Konfigurationen, mit denen alles orchestriert wird.

Das Beste an Snyk: Für gefundene Schwachstellen erhalten Sie empfohlene Korrekturen. Statt Ihnen nur zu zeigen, welche Sicherheitslücken bestehen, erstellt Snyk automatisch einen Pull Request mit Korrekturen oder gibt Hinweise zur Behebung. Und das alles direkt in Ihren bestehenden Tools (IDE, CLI, Docker usw.) und Workflows (Git, CI/CD usw.).

Beispiel: Unsere containerisierte Python-App mit Snyk scannen

Sehen wir uns an, wie das mit der Snyk CLI funktioniert, wenn wir das Python-Docker-Image python:3.8 verwenden, um diese Beispielanwendung mit Python Flask zu erstellen.

Wenn Sie die Snyk CLI installieren, können Sie damit Ihre Python-Projektabhängigkeiten, Ihren Python-Code und mehr scannen. Installieren wir also zunächst die Snyk CLI. Wenn Sie eine Node.js-Umgebung verwenden, können Sie dazu den Paketmanager npm wie folgt nutzen:

npm install -g snyk

Wenn Sie macOS oder Linux verwenden und Homebrew eingerichtet haben, können Sie die CLI auch wie folgt installieren:

brew tap snyk/tap
brew install snyk

Weitere Installationsmethoden finden Sie in unserer Anleitung zur Installation der Snyk CLI.

Als Nächstes müssen wir uns über die CLI authentifizieren, um ein gültiges API-Token für die Abfrage der Schwachstellendatenbank zu erhalten:

snyk auth

Anschließend können wir mit dem Basisimage python:3.8 ein lokales Docker-Image der Python-Anwendung erstellen:

❯ docker build . -t python-flask-app
FROM python:3.8 as build
[+] Building 5.2s (8/13)
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 513B
 => [internal] load .dockerignore
 => => transferring context: 2B
 => [internal] load metadata for docker.io/library/python:3.8
 => [auth] library/python:pull token for registry-1.docker.io
 => [internal] load build context
...

Scannen wir es jetzt mit Snyk, indem wir Folgendes ausführen:

snyk container test python-flask-app

Die Ausgabe zeigt die folgenden Ergebnisse (hier bewusst gekürzt, da sie sehr umfangreich ist):

Testing python-flask-app...

✗ Low severity vulnerability found in tiff/libtiff5
  Description: Out-of-bounds Read
  Info: https://snyk.io/vuln/SNYK-DEBIAN11-TIFF-514595
  Introduced through: imagemagick@8:6.9.11.60+dfsg-1.3, imagemagick/libmagickcore-dev@8:6.9.11.60+dfsg-1.3
  From: imagemagick@8:6.9.11.60+dfsg-1.3 > imagemagick/imagemagick-6.q16@8:6.9.11.60+dfsg-1.3 > imagemagick/libmagickcore-6.q16-6@8:6.9.11.60+dfsg-1.3 > tiff/libtiff5@4.2.0-1
  From: imagemagick/libmagickcore-dev@8:6.9.11.60+dfsg-1.3 > imagemagick/libmagickcore-6.q16-dev@8:6.9.11.60+dfsg-1.3 > tiff/libtiff-dev@4.2.0-1 > tiff/libtiff5@4.2.0-1
  From: imagemagick/libmagickcore-dev@8:6.9.11.60+dfsg-1.3 > imagemagick/libmagickcore-6.q16-dev@8:6.9.11.60+dfsg-1.3 > tiff/libtiff-dev@4.2.0-1 > tiff/libtiffxx5@4.2.0-1 > tiff/libtiff5@4.2.0-1
  and 3 more...

✗ High severity vulnerability found in imagemagick/imagemagick-6-common
  Description: Information Exposure
  Info: https://snyk.io/vuln/SNYK-DEBIAN11-IMAGEMAGICK-1246513
  Introduced through: imagemagick/libmagickcore-dev@8:6.9.11.60+dfsg-1.3, imagemagick/libmagickwand-dev@8:6.9.11.60+dfsg-1.3, imagemagick@8:6.9.11.60+dfsg-1.3
  From: imagemagick/libmagickcore-dev@8:6.9.11.60+dfsg-1.3 > imagemagick/imagemagick-6-common@8:6.9.11.60+dfsg-1.3
  From: imagemagick/libmagickwand-dev@8:6.9.11.60+dfsg-1.3 > imagemagick/imagemagick-6-common@8:6.9.11.60+dfsg-1.3
  From: imagemagick@8:6.9.11.60+dfsg-1.3 > imagemagick/imagemagick-6.q16@8:6.9.11.60+dfsg-1.3 > imagemagick/libmagickcore-6.q16-6@8:6.9.11.60+dfsg-1.3 > imagemagick/imagemagick-6-common@8:6.9.11.60+dfsg-1.3
  and 24 more...

✗ Critical severity vulnerability found in python3.9/libpython3.9-stdlib
  Description: Improper Input Validation
  Info: https://snyk.io/vuln/SNYK-DEBIAN11-PYTHON39-1290158
  Introduced through: mercurial@5.6.1-4
  From: mercurial@5.6.1-4 > python3-defaults/python3@3.9.2-3 > python3-defaults/libpython3-stdlib@3.9.2-3 > python3.9/libpython3.9-stdlib@3.9.2-1
  From: mercurial@5.6.1-4 > python3-defaults/python3@3.9.2-3 > python3.9@3.9.2-1 > python3.9/libpython3.9-stdlib@3.9.2-1
  From: mercurial@5.6.1-4 > python3-defaults/python3@3.9.2-3 > python3-defaults/python3-minimal@3.9.2-3 > python3.9/python3.9-minimal@3.9.2-1
  and 4 more...

✗ Critical severity vulnerability found in glibc/libc-bin
  Description: Use After Free
  Info: https://snyk.io/vuln/SNYK-DEBIAN11-GLIBC-1296898
  Introduced through: glibc/libc-bin@2.31-13+deb11u2, meta-common-packages@meta
  From: glibc/libc-bin@2.31-13+deb11u2
  From: meta-common-packages@meta > glibc/libc-dev-bin@2.31-13+deb11u2
  From: meta-common-packages@meta > glibc/libc6@2.31-13+deb11u2
  and 1 more...

Organization:      snyk-demo-567
Package manager:   deb
Project name:      docker-image|python-flask-app
Docker image:      python-flask-app
Platform:          linux/amd64
Base image:        python:3.8.12-bullseye
Licenses:          enabled

Tested 427 dependencies for known issues, found 171 issues.

Base Image              Vulnerabilities  Severity
python:3.8.12-bullseye  171              6 critical, 6 high, 27 medium, 132 low

Recommendations for base image upgrade:

Alternative image types
Base Image                   Vulnerabilities  Severity
python:3.9-slim              37               1 critical, 0 high, 1 medium, 35 low
python:3.11-rc-slim          37               1 critical, 0 high, 1 medium, 35 low
python:3.8.12-slim-bullseye  37               1 critical, 0 high, 1 medium, 35 low
python:3.10-slim-buster      70               2 critical, 9 high, 9 medium, 50 low

Damit haben wir 427 Abhängigkeiten in Form von Open-Source-Bibliotheken, die zum Betriebssysteminhalt von Python 3.8 gehören. Sie verursachen insgesamt 171 Sicherheitslücken in dieser Python-Flask-Anwendung, weil wir das Basisimage python:3.8 ausgewählt haben.

An diesem Punkt fragen Sie sich vielleicht: „Wie behebe ich das?“ Zum Glück empfiehlt Snyk andere Basisimages, auf die Sie aktualisieren oder zu denen Sie wechseln können, um die Angriffsfläche zu verringern.

Hier sehen Sie einen anschaulichen Screenshot mit den Empfehlungen für Basisimages:

Die Terminalausgabe meldet 171 Schwachstellen in python:3.8.12-bullseye und vergleicht die Anzahl der Schwachstellen in alternativen Python-Basis-Images.

Jetzt können wir eine fundierte, datengestützte Entscheidung treffen, um unsere Python-Anwendung abzusichern. Wenn wir eines der von Snyk empfohlenen alternativen Docker-Images auswählen, können wir die Angriffsfläche der in unserer Anwendung gebündelten Software erheblich verringern.

Um die Sicherheit Ihrer Anwendung noch besser im Griff zu haben, verbinden Sie Ihre Repositorys mit der Snyk-Benutzeroberfläche, um Ihren Quellcode und Ihre Dockerfile zu importieren. So können Sie diese Schwachstellen nicht nur finden, sondern auch kontinuierlich auf neue Sicherheitsprobleme überwachen. Hier sehen Sie denselben Bericht zum Docker-Basisimage in der Snyk-Benutzeroberfläche:

Details zum Snyk Container-Image mit Python 3.8 auf Debian 11 und Empfehlungen zum Upgrade des Basis-Images mit Angaben zur Anzahl der Sicherheitslücken.

Was ist besser, als Sicherheitslücken zu überwachen und zu finden? Sie zu beheben! :-)

Wenn Sie Ihre Git-Repositorys mit Snyk verbinden, können wir auch automatisch Pull Requests in Ihrem Repository erstellen, um Upgrades für Docker-Basisimages vorzuschlagen – wie hier zu sehen:

GitHub-Pull-Request mit einem Update für Warenkorb/Dockerfile von node:10 auf node:debian-buster-slim zur Behebung von Sicherheitslücken

Wenn Sie das interessiert, lesen Sie diesen Folgebeitrag zur Automatisierung der Container-Sicherheit mit Pull Requests für Dockerfiles.

Wie werden Python-Anwendungen containerisiert?

Docker ist eine Software-Virtualisierungstechnologie, mit der sich wiederverwendbare, plattformübergreifende und schnell bereitstellbare Software in Form containerisierter Python-Anwendungen erstellen lässt, die auf Python-Docker-Images basieren. Diese Anwendungen werden mithilfe von Infrastructure as Code in einer Datei namens Dockerfile definiert.

Führen Sie zum Erstellen und Verwenden einer containerisierten Python-Anwendung Folgendes aus:

docker build -t flask-application .
docker run -p 8080:5000 flask-application

Sicherheitsempfehlungen für Python-Entwickler

Mit diesen Best Practices können Sie Ihre containerisierten Python-Apps besser erstellen, verwalten und absichern. Wenn Ihnen diese Best Practices gefallen und Ihnen Anwendungssicherheit sowie der Einsatz für mehr Sicherheit am Herzen liegen, empfehlen wir Ihnen die folgenden weiterführenden Ressourcen:

Container-Sicherheit mit Fokus auf Entwickler

Snyk findet und behebt automatisch Schwachstellen in Container-Images und Kubernetes-Workloads.