BLOG | NGINX

NGINX-Tutorial: So verwalten Sie Geheimnisse in Containern sicher

NGINX-Teil-von-F5-horiz-schwarz-Typ-RGB
Robert Haynes Miniaturbild
Robert Haynes
Veröffentlicht am 14. März 2023

Dieser Beitrag ist eines von vier Tutorials, die Ihnen helfen, Konzepte aus Microservices März 2023 in die Praxis umzusetzen: Beginnen Sie mit der Bereitstellung von Microservices :

Viele Ihrer Microservices benötigen Geheimnisse , um sicher zu funktionieren. Beispiele für Geheimnisse sind der private Schlüssel für ein SSL/TLS-Zertifikat, ein API-Schlüssel zur Authentifizierung bei einem anderen Dienst oder ein SSH-Schlüssel für die Remote-Anmeldung. Für eine ordnungsgemäße Verwaltung von Geheimnissen ist es erforderlich, die Kontexte, in denen Geheimnisse verwendet werden, strikt auf die Orte zu beschränken, an denen sie benötigt werden, und zu verhindern, dass auf Geheimnisse nur bei Bedarf zugegriffen wird. In der Eile bei der Anwendungsentwicklung wird dieser Vorgang jedoch häufig übersprungen. Das Ergebnis? Unsachgemäße Verwaltung von Geheimnissen ist eine häufige Ursache für Informationslecks und Exploits.

Tutorialübersicht

In diesem Tutorial zeigen wir, wie Sie ein JSON Web Token (JWT), das ein Client-Container für den Zugriff auf einen Dienst verwendet, sicher verteilen und verwenden. In den vier Herausforderungen dieses Tutorials experimentieren Sie mit vier verschiedenen Methoden zum Verwalten von Geheimnissen, um nicht nur zu lernen, wie Sie Geheimnisse in Ihren Containern richtig verwalten, sondern auch, welche Methoden unzureichend sind:

Obwohl in diesem Tutorial ein JWT als Beispielgeheimnis verwendet wird, gelten die Techniken für alle Container, die Sie geheim halten müssen, z. B. Datenbankanmeldeinformationen, private SSL-Schlüssel und andere API-Schlüssel.

Das Tutorial nutzt zwei Hauptsoftwarekomponenten:

  • API-Server – Ein Container, auf dem NGINX Open Source und grundlegender NGINX-JavaScript-Code ausgeführt werden, der einen Anspruch aus dem JWT extrahiert und einen Wert aus einem der Ansprüche oder, wenn kein Anspruch vorhanden ist, eine Fehlermeldung zurückgibt.
  • API-Client – Ein Container, der sehr einfachen Python-Code ausführt, der einfach eine GET- Anfrage an den API-Server sendet.

Sehen Sie sich dieses Video an, um eine Demo des Tutorials in Aktion zu sehen.

Am einfachsten können Sie dieses Tutorial absolvieren, indem Sie sich für Microservices March registrieren und das bereitgestellte browserbasierte Labor verwenden. Dieser Beitrag enthält Anweisungen zum Ausführen des Tutorials in Ihrer eigenen Umgebung.

Voraussetzungen und Einrichtung

Voraussetzungen

Um das Tutorial in Ihrer eigenen Umgebung abzuschließen, benötigen Sie:

  • Eine Linux/Unix‑kompatible Umgebung
  • Grundlegende Kenntnisse der Linux-Befehlszeile
  • Ein Texteditor wie Nano oder Vim
  • Docker (einschließlich Docker Compose und Docker Engine Swarm )
  • curl (auf den meisten Systemen bereits installiert)
  • git (auf den meisten Systemen bereits installiert)

Hinweise:

  • Das Tutorial verwendet einen Testserver, der auf Port 80 lauscht. Wenn Sie bereits Port 80 verwenden, legen Sie mit dem Flag „-p“ einen anderen Wert für den Testserver fest, wenn Sie ihn mit dem Befehl „Docker Run starten. Fügen Sie dann die :<Portnummer> Suffix an lokaler Host im Locke Befehle.
  • Im gesamten Tutorial wird die Eingabeaufforderung in der Linux-Befehlszeile weggelassen, um das Ausschneiden und Einfügen der Befehle in Ihr Terminal zu erleichtern. Die Tilde ( ~ ) steht für Ihr Home-Verzeichnis.

Aufstellen

In diesem Abschnitt klonen Sie das Tutorial-Repository , starten den Authentifizierungsserver und senden Testanfragen mit und ohne Token.

Klonen Sie das Tutorial-Repo

  1. Erstellen Sie in Ihrem Home-Verzeichnis das Verzeichnis „Microservices-March“ und klonen Sie das GitHub-Repository hinein. (Sie können auch einen anderen Verzeichnisnamen verwenden und die Anweisungen entsprechend anpassen.) Das Repo enthält Konfigurationsdateien und separate Versionen der API-Clientanwendung, die unterschiedliche Methoden zum Abrufen von Geheimnissen verwenden.

    mkdir ~/microservices-marchcd ~/microservices-march
    git-Klon https://github.com/microservices-march/auth.git
  2. Zeigen Sie das Geheimnis. Es handelt sich um ein signiertes JWT, das häufig zur Authentifizierung von API-Clients gegenüber Servern verwendet wird.

    Katze ~/microservices-march/auth/apiclient/token1.jwt "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2Nz UyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

Es gibt zwar mehrere Möglichkeiten, dieses Token zur Authentifizierung zu verwenden, in diesem Tutorial übergibt die API-Client-App es jedoch mithilfe des OAuth 2.0 Bearer Token Authorization-Frameworks an den Authentifizierungsserver. Dazu muss dem JWT die Autorisierung vorangestellt werden: Inhaber wie in diesem Beispiel:

„Autorisierung: Inhaber eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

Erstellen und Starten des Authentifizierungsservers

  1. Wechseln Sie in das Verzeichnis des Authentifizierungsservers:

    cd apiserver
  2. Erstellen Sie das Docker-Image für den Authentifizierungsserver (beachten Sie den letzten Punkt):

    Docker-Build -t APIServer.
  3. Starten Sie den Authentifizierungsserver und bestätigen Sie, dass er ausgeführt wird (die Ausgabe ist zur besseren Lesbarkeit auf mehrere Zeilen verteilt):

    docker run -d -p 80:80 apiserver docker ps CONTAINER-ID IMAGE-BEFEHL …
    2b001f77c5cb APIServer „nginx -g ‚Daemon von …‘“ … … ERSTELLTER STATUS ... ... vor 26 Sekunden. 26 Sekunden hoch ... ... ANSCHLÜSSE ... ... 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp ... ... NAMEN ... entspannt_proskuriakova

Testen des Authentifizierungsservers

  1. Überprüfen Sie, ob der Authentifizierungsserver eine Anforderung ablehnt, die das JWT nicht enthält, und gibt Folgendes zurück: 401Autorisierung erforderlich :

    curl -X GET http://localhost<html>
    <head><title>401-Autorisierung erforderlich</title></head>
    <body>
    <center><h1>401-Autorisierung erforderlich</h1></center>
    <hr><center>nginx/1.23.3</center>
    </body>
    </html>
  2. Stellen Sie das JWT mithilfe des Autorisierungsheaders bereit. Der200 Der Rückgabecode „OK“ zeigt an, dass die Authentifizierung der API-Client-App erfolgreich war.

    curl -i -X GET -H "Autorisierung: Träger `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhost HTTP/1.1 200 OK Server: nginx/1.23.2 Datum: Tag , TT Mo JJJJ hh : mm : ss TZ Inhaltstyp: text/html Inhaltslänge: 64 Letzte Änderung: Tag , TT Mo JJJJ hh : mm : ss TZ Verbindung: Keep-Alive ETag: "63dc0fcd-40" X-MELDUNG: Erfolg apiKey1 Accept-Ranges: Bytes { "Antwort": "Erfolg", "Autorisiert": True, "Wert": "999" }

Herausforderung 1: Codieren Sie Geheimnisse fest in Ihre App (Nicht!)

Bevor Sie mit dieser Herausforderung beginnen, lassen Sie uns eines klarstellen: Das Festcodieren von Geheimnissen in Ihrer App ist eine schreckliche Idee! Sie werden sehen, wie jeder mit Zugriff auf das Container-Image fest codierte Anmeldeinformationen problemlos finden und extrahieren kann.

Bei dieser Herausforderung kopieren Sie den Code für die API-Client-App in das Build-Verzeichnis, erstellen und führen die App aus und extrahieren das Geheimnis .

Kopieren der API-Client-App

Das Unterverzeichnis app_versions des Verzeichnisses apiclient enthält verschiedene Versionen der einfachen API-Client-App für die vier Herausforderungen, von denen jede etwas sicherer ist als die vorherige (weitere Informationen finden Sie in der Tutorial-Übersicht ).

  1. Wechseln Sie in das API-Client-Verzeichnis:

    cd ~/microservices-march/auth/apiclient
  2. Kopieren Sie die App für diese Herausforderung – die mit einem fest codierten Geheimnis – in das Arbeitsverzeichnis:

    cp ./app_versions/very_bad_hard_code.py ./app.py
  3. Schauen Sie sich die App an:

    cat app.py import urllib.request import urllib.error jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" authstring = "Bearer" + jwt req = urllib.request.Request("http://host.docker.internal") req.add_header("Authorization", authstring) versuche: mit urllib.request.urlopen(req) als Antwort: the_page = response.read() message = response.getheader("X-MESSAGE") print("200 " + message) außer urllib.error.URLError als e: print(str(e.code) + " s " + e.msg)

    Der Code stellt einfach eine Anfrage an einen lokalen Host und druckt entweder eine Erfolgsmeldung oder einen Fehlercode aus.

    Die Anforderung fügt den Autorisierungsheader in dieser Zeile hinzu:

    req.add_header("Autorisierung", Authentisierungszeichenfolge)

    Fällt Ihnen sonst noch etwas auf? Vielleicht ein fest codiertes JWT? Wir werden gleich darauf zurückkommen. Lassen Sie uns zunächst die App erstellen und ausführen.

Erstellen und Ausführen der API-Client-App

Wir verwenden den Befehl „Docker Compose“ zusammen mit einer Docker Compose-YAML-Datei. So ist es etwas einfacher zu verstehen, was passiert.

(Beachten Sie, dass Sie in Schritt 2 des vorherigen Abschnitts die Python-Datei für die API-Client-App, die spezifisch für Herausforderung 1 ist ( very_bad_hard_code.py ), in app.py umbenannt haben. Dies geschieht auch bei den anderen drei Herausforderungen. Die Verwendung von app.py vereinfacht jedes Mal die Logistik, da Sie die Docker-Datei nicht ändern müssen. Dies bedeutet, dass Sie das Argument „-build“ in den Docker -Compose -Befehl aufnehmen müssen, um jedes Mal einen Neuaufbau des Containers zu erzwingen.)

Der Befehl „Docker Compose“ erstellt den Container, startet die Anwendung, stellt eine einzelne API-Anfrage und fährt dann den Container herunter, während die Ergebnisse des API-Aufrufs auf der Konsole angezeigt werden.

Der200 Der Erfolgscode in der vorletzten Zeile der Ausgabe zeigt an, dass die Authentifizierung erfolgreich war. Der Wert apiKey1 ist eine weitere Bestätigung, da er zeigt, dass der Authentifizierungsserver den Anspruch dieses Namens im JWT dekodieren konnte:

docker compose -f docker-compose.hardcode.yml up -build ... apiclient-apiclient-1 | 200 Erfolg apiKey1 apiclient-apiclient-1 wurde mit Code 0 beendet.

Die fest codierten Anmeldeinformationen funktionierten also für unsere API-Client-App einwandfrei – was nicht überraschend ist. Aber ist es sicher? Vielleicht, da der Container dieses Skript nur einmal ausführt, bevor er beendet wird, und keine Shell hat?

Tatsächlich – nein, überhaupt nicht sicher.

Abrufen des Geheimnisses aus dem Container-Image

Durch die Festcodierung der Anmeldeinformationen können diese von jedem eingesehen werden, der auf das Container-Image zugreifen kann, da das Extrahieren des Dateisystems eines Containers eine triviale Übung ist.

  1. Erstellen Sie das Extraktionsverzeichnis und wechseln Sie dorthin:

    mkdir extractcd extrahieren
  2. Listen Sie grundlegende Informationen zu den Container-Images auf. Das Flag --format macht die Ausgabe lesbarer (und aus demselben Grund wird die Ausgabe hier auf zwei Zeilen verteilt):

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" CONTAINER-ID NAMEN BILD …
    11b73106fdf8 apiclient-apiclient-1 apiclient … ad9bdc05b07c exciting_clarke apiserver … … ERSTELLTER STATUS ... Vor 6 Minuten Beendet (0) Vor 4 Minuten ... vor 43 Minuten Up 43 Minuten
  3. Extrahieren Sie das aktuellste Apiclient- Image als .tar- Datei. Für <Container-ID>ersetzen Sie den Wert aus der CONTAINER AUSWEIS Feld in der obigen Ausgabe (11b73106fdf8 in diesem Tutorial):

    docker export -o api.tar <Container-ID>

    Das Erstellen des api.tar- Archivs, das das gesamte Dateisystem des Containers enthält, dauert einige Sekunden. Ein Ansatz zum Auffinden von Geheimnissen besteht darin, das gesamte Archiv zu extrahieren und zu analysieren. Wie sich jedoch herausgestellt hat, gibt es eine Abkürzung zum Auffinden wahrscheinlich interessanter Informationen: die Anzeige des Containerverlaufs mit dem Befehl Docker History“ . (Diese Verknüpfung ist besonders praktisch, da sie auch für Container funktioniert, die Sie auf Docker Hub oder einem anderen Container-Verzeichnis finden und daher möglicherweise nicht über die Docker-Datei , sondern nur über das Container-Image verfügen.)

  4. Zeigen Sie den Verlauf des Containers an:

    Docker-Verlauf, APIClient- IMAGE ERSTELLT …
    9396dde2aad0 vor 8 Minuten ...  vor 8 Minuten ...  vor 28 Minuten ... ... ERSTELLT NACH GRÖSSE … … CMD ["python" "./app.py"] 622B … … KOPIEREN ./app.py ./app.py # buildkit 0B ... ... ARBEITSVERZEICHNIS /usr/app/src 0B … … KOMMENTAR ... buildkit.dockerfile.v0 … buildkit.dockerfile.v0 … buildkit.dockerfile.v0

    Die Ausgabezeilen erfolgen in umgekehrter chronologischer Reihenfolge. Sie zeigen, dass das Arbeitsverzeichnis auf /usr/app/src festgelegt wurde und anschließend die Datei mit dem Python-Code für die App kopiert und ausgeführt wurde. Man muss kein großer Detektiv sein, um herauszufinden, dass sich der Kerncode dieses Containers in /usr/app/src/app.py befindet und dies daher ein wahrscheinlicher Speicherort für Anmeldeinformationen ist.

  5. Mit diesem Wissen ausgestattet, extrahieren Sie nur diese Datei:

    tar --extract --file=api.tar usr/app/src/app.py
  6. Zeigen Sie den Inhalt der Datei an und schon haben wir Zugriff auf das „sichere“ JWT:

    cat usr/app/src/app.py … jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" …

Herausforderung 2: Übergeben Sie Geheimnisse als Umgebungsvariablen (nochmals: Nein!)

Wenn Sie Einheit 1 von Microservices vom März 2023 (Anwenden der Zwölf-Faktor-App auf Microservices-Architekturen) abgeschlossen haben, sind Sie mit der Verwendung von Umgebungsvariablen zum Übergeben von Konfigurationsdaten an Container vertraut. Wenn Sie es verpasst haben, keine Sorge – es ist nach Ihrer Registrierung auf Abruf verfügbar.

Bei dieser Herausforderung übergeben Sie Geheimnisse als Umgebungsvariablen. Wie auch die Methode aus Challenge 1 empfehlen wir diese nicht! Es ist nicht so schlimm wie das Festcodieren von Geheimnissen, aber wie Sie sehen werden, hat es einige Schwächen.

Es gibt vier Möglichkeiten, Umgebungsvariablen an einen Container zu übergeben:

  • Verwenden Sie die ENV- Anweisung in einer Docker-Datei, um eine Variablensubstitution durchzuführen (legen Sie die Variable für alle erstellten Images fest). Zum Beispiel:

    Umgebungsanschluss $Anschluss
  • Verwenden Sie das Flag -e im Docker -Run- Befehl. Zum Beispiel:

    docker run -e PASSWORT=123 meinContainer
  • Verwenden Sie den Umgebungsschlüssel in einer Docker Compose YAML-Datei.
  • Verwenden Sie eine .env- Datei, die die Variablen enthält.

Bei dieser Herausforderung verwenden Sie eine Umgebungsvariable, um das JWT festzulegen, und untersuchen den Container, um zu sehen, ob das JWT verfügbar ist.

Übergeben einer Umgebungsvariablen

  1. Wechseln Sie zurück zum API-Client-Verzeichnis:

    cd ~/microservices-march/auth/apiclient
  2. Kopieren Sie die App für diese Herausforderung – diejenige, die Umgebungsvariablen verwendet – in das Arbeitsverzeichnis und überschreiben Sie dabei die Datei app.py aus Herausforderung 1:

    siehe ./app_versions/medium_environment_variables.py ./app.py
  3. Schauen Sie sich die App an. In den entsprechenden Ausgabezeilen wird das Geheimnis (JWT) als Umgebungsvariable im lokalen Container gelesen:

    cat app.py … jwt = "" wenn "JWT" in os.environ: jwt = "Bearer " + os.environ.get("JWT") …
  4. Wie oben erläutert, gibt es mehrere Möglichkeiten, die Umgebungsvariable in den Container zu bekommen. Aus Konsistenzgründen bleiben wir bei Docker Compose. Zeigen Sie den Inhalt der Docker Compose YAML-Datei an, die den Umgebungsschlüssel zum Festlegen der JWT- Umgebungsvariable verwendet:

    cat docker-compose.env.yml --- Version: „3.9“-Dienste: apiclient: Build: . Image: apiclient extra_hosts: – „host.docker.internal:host-gateway“-Umgebung: – JWT
  5. Führen Sie die App aus, ohne die Umgebungsvariable festzulegen. Der401 Nicht autorisierter Code in der vorletzten Zeile der Ausgabe bestätigt, dass die Authentifizierung fehlgeschlagen ist, weil die API-Client-App das JWT nicht bestanden hat:

    docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 401 Nicht autorisierter apiclient-apiclient-1 wurde mit Code 0 beendet.
  6. Legen Sie die Umgebungsvariable der Einfachheit halber lokal fest. Dies können Sie an dieser Stelle des Tutorials problemlos tun, da es sich derzeit nicht um ein Sicherheitsproblem handelt:

    exportiere JWT=`cat token1.jwt`
  7. Führen Sie den Container erneut aus. Jetzt ist der Test erfolgreich und es wird dieselbe Meldung wie bei Herausforderung 1 angezeigt:

    docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 200 Erfolg apiKey1 apiclient-apiclient-1 wurde mit Code 0 beendet.

Zumindest enthält das Basisimage jetzt nicht das Geheimnis und wir können es zur Laufzeit übergeben, was sicherer ist. Aber es gibt immer noch ein Problem.

Untersuchen Sie den Behälter

  1. Zeigen Sie Informationen zu den Container-Images an, um die Container-ID für die API-Client-App abzurufen (die Ausgabe ist zur besseren Lesbarkeit auf zwei Zeilen verteilt):

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" CONTAINER-ID NAMEN BILD …
    6b20c75830df apiclient-apiclient-1 apiclient … ad9bdc05b07c exciting_clarke apiserver … … ERSTELLTER STATUS ... Vor 6 Minuten Beendet (0) Vor 6 Minuten ... Vor etwa einer Stunde Up Vor etwa einer Stunde
  2. Überprüfen Sie den Container für die API-Client-App. Für <Container-ID>ersetzen Sie den Wert aus der CONTAINER AUSWEIS Feld in der Ausgabe oben (hier 6b20c75830df).

    Mit dem Befehl „Docker Inspect“ können Sie alle gestarteten Container überprüfen, unabhängig davon, ob sie aktuell ausgeführt werden oder nicht. Und genau hier liegt das Problem: Auch wenn der Container nicht läuft, legt die Ausgabe das JWT im Env -Array offen, das unsicher in der Containerkonfiguration gespeichert ist.

    Docker-Inspektion <Container-ID>...
    "Umgebung": [ "JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA...", "PFAD=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LANG=C.UTF-8", "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D", "PYTHON_VERSION=3.11.2", "PYTHON_PIP_VERSION=22.3.1", "PYTHON_SETUPTOOLS_VERSION=65.5.1", „PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...“, „PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c...“ ]

Herausforderung 3: Lokale Geheimnisse verwenden

Inzwischen haben Sie gelernt, dass die Festcodierung von Geheimnissen und die Verwendung von Umgebungsvariablen nicht so sicher ist, wie Sie (oder Ihr Sicherheitsteam) es benötigen.

Um die Sicherheit zu verbessern, können Sie versuchen, lokale Docker-Geheimnisse zum Speichern vertraulicher Informationen zu verwenden. Auch hier gilt, dass es sich nicht um die ultimative Methode handelt, aber es ist gut zu verstehen, wie sie funktioniert. Auch wenn Sie Docker nicht in der Produktion verwenden, ist die wichtige Erkenntnis, wie Sie es erschweren können, das Geheimnis aus einem Container zu extrahieren.

In Docker werden Geheimnisse einem Container über das Dateisystem-Mount /run/secrets/ zugänglich gemacht, wo es eine separate Datei gibt, die den Wert jedes Geheimnisses enthält.

Bei dieser Herausforderung übergeben Sie mithilfe von Docker Compose ein lokal gespeichertes Geheimnis an den Container und überprüfen dann, dass das Geheimnis bei Verwendung dieser Methode nicht im Container sichtbar ist .

Übergeben eines lokal gespeicherten Geheimnisses an den Container

  1. Wie Sie jetzt vielleicht erwarten, beginnen Sie mit dem Wechsel in das Verzeichnis apiclient :

    cd ~/microservices-march/auth/apiclient
  2. Kopieren Sie die App für diese Herausforderung – diejenige, die Geheimnisse aus einem Container verwendet – in das Arbeitsverzeichnis und überschreiben Sie dabei die Datei app.py aus Herausforderung 2:

    siehe ./app_versions/better_secrets.py ./app.py
  3. Sehen Sie sich den Python-Code an, der den JWT-Wert aus der Datei /run/secrets/jot liest. (Und ja, wir sollten wahrscheinlich überprüfen, ob die Datei nur eine Zeile hat. Vielleicht in Microservices März 2024?)

    cat app.py … jotfile = "/run/secrets/jot" jwt = "" wenn os.path.isfile(jotfile): mit open(jotfile) als jwtfile: für Zeile in jwtfile: jwt = "Bearer " + Zeile …

    OK, also wie werden wir dieses Geheimnis erschaffen? Die Antwort finden Sie in der Datei docker-compose.secrets.yml .

  4. Sehen Sie sich die Docker Compose-Datei an, in der die geheime Datei im Abschnitt „Secrets“ definiert und dann vom Apiclient -Dienst referenziert wird:

    cat docker-compose.secrets.yml --- Version: "3.9"-Geheimnisse: jot: Datei: token1.jwt Dienste: APIClient: Build: . extra_hosts: - "host.docker.internal:host-gateway"-Geheimnisse: - jot

Stellen Sie sicher, dass das Geheimnis im Container nicht sichtbar ist.

  1. Führen Sie die App aus. Da wir das JWT innerhalb des Containers zugänglich gemacht haben, ist die Authentifizierung mit der mittlerweile bekannten Meldung erfolgreich:

    docker compose -f docker-compose.secrets.yml up -build ... apiclient-apiclient-1 | 200 Erfolg apiKey1 apiclient-apiclient-1 wurde mit Code 0 beendet.
  2. Zeigen Sie Informationen zu den Container-Images an und notieren Sie sich die Container-ID für die API-Client-App (Beispielausgabe finden Sie in Schritt 1 unter „Untersuchen des Containers aus Herausforderung 2“):

    docker ps -a --format "Tabelle {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  3. Überprüfen Sie den Container für die API-Client-App. Für <Container-ID>ersetzen Sie den Wert aus der CONTAINER AUSWEIS Feld in der Ausgabe des vorherigen Schritts. Anders als bei der Ausgabe in Schritt 2 von „Container untersuchen“ gibt es am Anfang des Abschnitts „Env“ keine Zeile „JWT=“ :

    Docker-Inspektion <Container-ID>
    "Env": [
    "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "LANG=C.UTF-8",
    "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D",
    "PYTHON_VERSION=3.11.2",
    "PYTHON_PIP_VERSION=22.3.1",
    "PYTHON_SETUPTOOLS_VERSION=65.5.1",
    "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...",
    „PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c...“
    ]

    So weit, so gut, aber unser Geheimnis liegt im Containerdateisystem unter /run/secrets/jot . Vielleicht können wir es von dort mit der gleichen Methode extrahieren wie beim Abrufen des Geheimnisses aus dem Container-Image aus Herausforderung 1.

  4. Wechseln Sie in das Extraktionsverzeichnis (das Sie während Challenge 1 erstellt haben) und exportieren Sie den Container in ein Tar- Archiv:

    cd extractdocker export -o api2.tar <Container-ID>
  5. Suchen Sie in der TAR -Datei nach der Datei mit den Geheimnissen:

    tar tvf api2.tar | grep jot -rwxr-xr-x 0 0 0 0 Mo DD hh :mm run/secrets/jot

    Oh oh, die Datei mit dem JWT darin ist sichtbar. Haben wir nicht gesagt, dass das Einbetten von Geheimnissen in den Container „sicher“ sei? Ist es genauso schlimm wie in Challenge 1?

  6. Mal sehen – extrahieren Sie die Datei mit den Geheimnissen aus der TAR -Datei und sehen Sie sich ihren Inhalt an:

    tar --extract --file=api2.tar ausführen/secrets/jotcat ausführen/secrets/jot

    Gute Neuigkeiten! Es gibt keine Ausgabe vom Cat -Befehl, was bedeutet, dass die Datei run/secrets/jot im Containerdateisystem leer ist – kein Geheimnis darin zu sehen! Selbst wenn sich in unserem Container ein geheimes Artefakt befindet, ist Docker intelligent genug, keine vertraulichen Daten im Container zu speichern.

Allerdings weist diese Containerkonfiguration, obwohl sie sicher ist, einen Nachteil auf. Dies hängt von der Existenz einer Datei namens token1.jwt im lokalen Dateisystem ab, wenn Sie den Container ausführen. Wenn Sie die Datei umbenennen, schlägt der Versuch, den Container neu zu starten, fehl. (Sie können dies selbst ausprobieren, indem Sie token1.jwt umbenennen [nicht löschen!] und den Docker -Compose - Befehl aus Schritt 1 erneut ausführen.)

Wir sind also auf halbem Weg: Der Container verwendet Geheimnisse auf eine Weise, die sie vor einer einfachen Kompromittierung schützt, aber auf dem Host sind die Geheimnisse immer noch ungeschützt. Sie möchten keine Geheimnisse unverschlüsselt in einer einfachen Textdatei speichern. Es ist Zeit, ein Tool zur Geheimnisverwaltung einzuführen.

Herausforderung 4: Verwenden Sie einen Secrets Manager

Mithilfe eines Geheimnis-Managers können Sie Geheimnisse während ihres gesamten Lebenszyklus verwalten, abrufen und rotieren. Es gibt eine große Auswahl an Geheimnismanagern und sie alle erfüllen einen ähnlichen Zweck:

  • Geheimnisse sicher aufbewahren
  • Kontrollieren Sie den Zugriff
  • Verteilen Sie sie zur Laufzeit
  • Aktivieren der geheimen Rotation

Zu Ihren Optionen für die Geheimnisverwaltung gehören:

Der Einfachheit halber verwendet diese Herausforderung Docker Swarm, aber die Prinzipien sind für viele Geheimnismanager dieselben.

Bei dieser Herausforderung erstellen Sie ein Geheimnis in Docker , kopieren das Geheimnis und den API-Clientcode , stellen den Container bereit , prüfen, ob Sie das Geheimnis extrahieren können, und rotieren das Geheimnis .

Konfigurieren eines Docker-Geheimnisses

  1. Wechseln Sie wie mittlerweile üblich in das Verzeichnis apiclient :

    cd ~/microservices-march/auth/apiclient
  2. Docker Swarm initialisieren:

    Docker Swarm init. Swarm initialisiert: Aktueller Knoten (t0o4eix09qpxf4ma1rrs9omrm) ist jetzt ein Manager. ...
  3. Erstellen Sie ein Geheimnis und speichern Sie es in token1.jwt :

    Docker-Geheimnis erstellt Jot ./token1.jwt qe26h73nhb35bak5fr5east27
  4. Informationen zum Geheimnis anzeigen. Beachten Sie, dass der geheime Wert (das JWT) selbst nicht angezeigt wird:

    Docker Secret Inspect Jot [ { "ID": "qe26h73nhb35bak5fr5east27", "Version": { "Index": 11 }, "Erstellt am": " JJJJ - MM - TT T hh : mm : ss . ms Z", "AktualisiertAm": " JJJJ - MM - TT T hh : mm : ss . ms Z", "Spec": { "Name": "jot", "Labels": {} } } ]

Verwenden Sie ein Docker-Geheimnis

Die Verwendung des Docker-Geheimnisses im API-Client-Anwendungscode ist genau dasselbe wie die Verwendung eines lokal erstellten Geheimnisses – Sie können es aus dem Dateisystem /run/secrets/ lesen. Sie müssen lediglich den geheimen Qualifizierer in Ihrer Docker Compose YAML-Datei ändern.

  1. Sehen Sie sich die Docker Compose YAML-Datei an. Beachten Sie den Wert „true“ im externen Feld, der angibt, dass wir ein Docker-Swarm-Geheimnis verwenden:

    cat docker-compose.secretmgr.yml --- Version: "3.9"-Geheimnisse: jot: extern: wahre Dienste: APIClient: Build: . Image: APIClient extra_hosts: - "host.docker.internal:host-gateway"-Geheimnisse: - jot

    Wir können also davon ausgehen, dass diese Compose-Datei mit unserem vorhandenen API-Client-Anwendungscode funktioniert. Naja, fast. Docker Swarm (oder jede andere Container-Orchestrierungsplattform) bietet zwar einen großen Mehrwert, bringt aber auch eine gewisse zusätzliche Komplexität mit sich.

    Da Docker Compose nicht mit externen Geheimnissen funktioniert, müssen wir einige Docker-Swarm-Befehle verwenden, insbesondere Docker Stack Deploy . Docker Stack verbirgt die Konsolenausgabe, daher müssen wir die Ausgabe in ein Protokoll schreiben und dann das Protokoll überprüfen.

    Zur Vereinfachung verwenden wir auch eine kontinuierliche while True -Schleife, um den Container am Laufen zu halten.

  2. Kopieren Sie die App für diese Herausforderung – diejenige, die einen Geheimnis-Manager verwendet – in das Arbeitsverzeichnis und überschreiben Sie dabei die Datei app.py aus Herausforderung 3. Wenn wir den Inhalt von app.py anzeigen, sehen wir, dass der Code nahezu identisch mit dem Code für Herausforderung 3 ist. Der einzige Unterschied besteht in der Hinzufügung der while True -Schleife:

    cp ./app_versions/best_secretmgr.py ./app.pycat ./app.py ... while True: time.sleep(5) versuche: mit urllib.request.urlopen(req) als Antwort: the_page = response.read() message = response.getheader("X-MESSAGE") print("200 " + message, file=sys.stderr) außer urllib.error.URLError als e: print(str(e.code) + " " + e.msg, file=sys.stderr)

Stellen Sie den Container bereit und überprüfen Sie die Protokolle

  1. Erstellen Sie den Container (in früheren Herausforderungen hat Docker Compose dies übernommen):

    docker build -t apiclient.
  2. Stellen Sie den Container bereit:

    docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack Netzwerk erstellen secretstack_default Dienst erstellen secretstack_apiclient
  3. Listen Sie die laufenden Container auf und notieren Sie die Container-ID für secretstack_apiclient (wie zuvor ist die Ausgabe zur besseren Lesbarkeit auf mehrere Zeilen verteilt).

    docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" CONTAINER-ID …  
    20d0c83a8b86 ... ad9bdc05b07c ... ... NAMEN ... ... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ... ... exciting_clarke ... ... BILDERSTELLUNGSSTATUS ... apiclient:latest vor 31 Sekunden, aktiv vor 30 Sekunden ... apiserver vor 2 Stunden, aktiv vor 2 Stunden
  4. Zeigen Sie die Docker-Protokolldatei an. <Container-ID>ersetzen Sie den Wert aus der CONTAINER AUSWEIS Feld in der Ausgabe des vorherigen Schritts (hier: 20d0c83a8b86). Die Protokolldatei zeigt eine Reihe von Erfolgsmeldungen, weil wir dem Anwendungscode die while True -Schleife hinzugefügt haben. Drücken Sie Strg+C, um den Befehl zu beenden.

    Docker-Protokolle -f <Container-ID>200 Erfolg apiKey1
    200 Erfolg apiKey1
    200 Erfolg apiKey1
    200 Erfolg apiKey1
    200 Erfolg apiKey1
    200 Erfolg apiKey1
    200 Erfolg apiKey1
    ...
    ^c

Versuchen Sie, an das Geheimnis zu gelangen

Wir wissen, dass keine sensiblen Umgebungsvariablen festgelegt sind (Sie können dies jedoch jederzeit mit dem Befehl „Docker Inspect“ überprüfen, wie in Schritt 2 von „Container untersuchen“ in Herausforderung 2).

Aus Herausforderung 3 wissen wir auch, dass die Datei /run/secrets/jot leer ist, aber Sie können Folgendes überprüfen:

cd extractdocker export -o api3.tar 
tar --extract --file=api3.tar run/secrets/jot
cat run/secrets/jot

Erfolg! Sie können das Geheimnis weder aus dem Container abrufen noch direkt aus dem Docker-Geheimnis lesen.

Drehen Sie das Geheimnis

Natürlich können wir mit den richtigen Berechtigungen einen Dienst erstellen und ihn so konfigurieren, dass er das Geheimnis in das Protokoll liest oder als Umgebungsvariable festlegt. Darüber hinaus ist Ihnen möglicherweise aufgefallen, dass die Kommunikation zwischen unserem API-Client und -Server unverschlüsselt (Klartext) erfolgt.

Daher ist es bei fast jedem Geheimnisverwaltungssystem weiterhin möglich, dass Geheimnisse verloren gehen. Eine Möglichkeit, die Möglichkeit entstehender Schäden zu begrenzen, besteht darin, Geheimnisse regelmäßig zu rotieren (ersetzen).

Mit Docker Swarm können Sie Geheimnisse nur löschen und dann neu erstellen (Kubernetes ermöglicht die dynamische Aktualisierung von Geheimnissen). Sie können auch keine Geheimnisse löschen, die an laufende Dienste angehängt sind.

  1. Listen Sie die laufenden Dienste auf:

    Docker-Dienst ls ID NAME MODE ... sl4mvv48vgjz secretstack_apiclient repliziert ... ... REPLIKATE-BILDPORTS ... 1/1 apiclient:neueste
  2. Löschen Sie den Dienst secretstack_apiclient .

    Docker-Dienst RM Secretstack_Apiclient
  3. Löschen Sie das Geheimnis und erstellen Sie es mit einem neuen Token neu:

    Docker-Geheimnis rm jot
    Docker-Geheimnis erstellt jot ./token2.jwt
  4. Erstellen Sie den Dienst neu:

    Docker-Stack bereitstellen --Compose-File docker-compose.secretmgr.yml secretstack
  5. Suchen Sie die Container-ID für Apiclient (Beispielausgabe finden Sie in Schritt 3 unter „Bereitstellen des Containers und Überprüfen der Protokolle“ ):

    docker ps --format "Tabelle {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  6. Zeigen Sie die Docker-Protokolldatei an, die eine Reihe von Erfolgsmeldungen enthält. Für <Container-ID>ersetzen Sie den Wert aus der CONTAINER AUSWEIS Feld in der Ausgabe des vorherigen Schritts. Drücken Sie Strg+C, um den Befehl zu beenden.

    Docker-Protokolle -f <Container-ID>200 Erfolg apiKey2
    200 Erfolg apiKey2
    200 Erfolg apiKey2
    200 Erfolg apiKey2
    ...
    ^c

Sehen Sie die Änderung von apiKey1 zu apiKey2 ? Sie haben das Geheimnis preisgegeben.

In diesem Tutorial akzeptiert der API-Server noch beide JWTs, in einer Produktionsumgebung können Sie jedoch ältere JWTs außer Kraft setzen, indem Sie bestimmte Werte für Ansprüche im JWT anfordern oder die Ablaufdaten von JWTs überprüfen.

Beachten Sie auch, dass Ihr Code das Geheimnis häufig erneut lesen muss, um neue Geheimniswerte zu erfassen, wenn Sie ein Geheimnissystem verwenden, das die Aktualisierung Ihres Geheimnisses zulässt.

Aufräumen

So bereinigen Sie die Objekte, die Sie in diesem Lernprogramm erstellt haben:

  1. Löschen Sie den Dienst secretstack_apiclient .

    Docker-Dienst RM Secretstack_Apiclient
  2. Löschen Sie das Geheimnis.

    Docker-Geheimnis RM Jot
  3. Verlassen Sie den Schwarm (vorausgesetzt, Sie haben nur für dieses Tutorial einen Schwarm erstellt).

    Docker-Schwarm verlassen --force
  4. Beenden Sie den laufenden APIServer- Container.

    docker ps -a | grep "APIServer" | awk {'print $1'} |xargs docker kill
  5. Löschen Sie nicht benötigte Container, indem Sie sie auflisten und dann löschen.

    docker ps -a --format "Tabelle {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <Container-ID>
  6. Löschen Sie alle unerwünschten Container-Images, indem Sie sie auflisten und löschen.

    Docker-Image-Liste Docker-Image rm <Bild-ID>

Nächste Schritte

Sie können dieses Blog verwenden, um das Tutorial in Ihrer eigenen Umgebung zu implementieren oder es in unserem browserbasierten Labor auszuprobieren ( hier registrieren ). Um mehr zum Thema Bereitstellung von Kubernetes-Diensten zu erfahren, verfolgen Sie die anderen Aktivitäten in Einheit 2: Grundlagen der Verwaltung von Microservices-Geheimnissen .

Weitere Informationen zur produktionstauglichen JWT-Authentifizierung mit NGINX Plus finden Sie in unserer Dokumentation . Lesen Sie in unserem Blog „Authentifizierung von API-Clients mit JWT und NGINX Plus“ .


„Dieser Blogbeitrag kann auf Produkte verweisen, die nicht mehr verfügbar und/oder nicht mehr unterstützt werden. Die aktuellsten Informationen zu verfügbaren F5 NGINX-Produkten und -Lösungen finden Sie in unserer NGINX-Produktfamilie . NGINX ist jetzt Teil von F5. Alle vorherigen NGINX.com-Links werden auf ähnliche NGINX-Inhalte auf F5.com umgeleitet."