Dieser Beitrag ist eines von vier Tutorials, die Ihnen dabei helfen, die Konzepte aus „Microservices March 2023: Erste Schritte mit der Bereitstellung von Microservices“ in die Praxis umzusetzen:
- So stellen Sie Microservices bereit und konfigurieren sie
- So verwalten Sie Geheimnisse in Containern sicher (dieser Beitrag)
- So automatisieren Sie Canary-Releases für Microservices mit GitHub Actions
- So nutzen Sie OpenTelemetry-Tracing, um Ihre Microservices besser zu verstehen
Viele Ihrer Microservices benötigen Geheimnisse, um sicher betrieben werden zu können. Beispiele für Geheimnisse sind der private Schlüssel eines SSL/TLS-Zertifikats, ein API-Schlüssel zur Authentifizierung bei einem anderen Dienst oder ein SSH-Schlüssel für die Remote-Anmeldung. Eine ordnungsgemäße Verwaltung von Geheimnissen setzt voraus, dass deren Verwendung strikt auf die erforderlichen Kontexte beschränkt wird und dass nur bei Bedarf auf sie zugegriffen werden kann. In der Eile der Anwendungsentwicklung wird diese Vorgehensweise jedoch häufig vernachlässigt. Die Folge: Eine unsachgemäße Verwaltung von Geheimnissen ist eine häufige Ursache für Datenlecks und Sicherheitslücken.
Überblick über das Tutorial
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 nutzen. In den vier Aufgaben dieses Tutorials probieren Sie vier verschiedene Methoden zur Verwaltung von Secrets aus. Dabei lernen Sie nicht nur, wie Sie Secrets in Ihren Containern richtig verwalten, sondern auch, welche Methoden ungeeignet sind:
- Geheimnisse fest in Ihrer App codieren
- Geheimnisse als Umgebungsvariablen übergeben
- Lokale Secrets verwenden
- Verwenden Sie einen Secrets Manager
Obwohl in diesem Tutorial ein JWT als Beispiel für ein Geheimnis verwendet wird, lassen sich die Techniken auf alle vertraulichen Daten für Container anwenden, etwa Datenbankzugangsdaten, private SSL-Schlüssel und andere API-Schlüssel.
Das Tutorial nutzt zwei zentrale Softwarekomponenten:
- API-Server – Ein Container, in dem NGINX Open Source und einfacher NGINX-JavaScript-Code ausgeführt werden. Dieser extrahiert einen Claim aus dem JWT und gibt den Wert eines Claims oder, falls kein Claim vorhanden ist, eine Fehlermeldung zurück.
- API-Client – Ein Container, in dem sehr einfacher Python-Code ausgeführt wird, der lediglich eine
GET-Anfrage an den API-Server sendet
Sehen Sie sich dieses Video an, um das Tutorial in Aktion zu erleben.
Am einfachsten lässt sich dieses Tutorial durchführen, indem Sie sich für Microservices March anmelden und die bereitgestellte browserbasierte Laborumgebung nutzen. Dieser Beitrag enthält eine Anleitung dazu, wie Sie das Tutorial in Ihrer eigenen Umgebung durchführen.
Voraussetzungen und Einrichtung
Voraussetzungen
Um das Tutorial in Ihrer eigenen Umgebung durchzuführen, benötigen Sie:
- Eine Linux-/Unix-kompatible Umgebung
- Grundkenntnisse im Umgang mit der Linux-Kommandozeile
- Ein Texteditor wie
nanoodervim - Docker (einschließlich Docker Compose und Docker Engine Swarm)
curl(auf den meisten Systemen bereits installiert)git(auf den meisten Systemen bereits installiert)
Hinweise:
- In diesem Tutorial wird ein Testserver verwendet, der Port 80 überwacht. Wenn Sie Port 80 bereits verwenden, legen Sie beim Starten des Testservers mit dem Befehl
‑peinen anderen Port fest:dockerrun. Fügen Sie anschließend in dencurl-Befehlen anlocalhostdas Suffix:<port_number>an. - Im gesamten Tutorial wird die Eingabeaufforderung der Linux-Befehlszeile weggelassen, damit Sie die Befehle einfacher kopieren und in Ihr Terminal einfügen können. Die Tilde (
~) steht für Ihr Home-Verzeichnis.
Einrichtung
In diesem Abschnitt klonen Sie das Repository des Tutorials, starten Sie den Authentifizierungsserver und senden Sie Testanfragen mit und ohne Token.
Tutorial-Repository klonen
- Erstellen Sie in Ihrem Home-Verzeichnis das Verzeichnis microservices-march und klonen Sie das GitHub-Repository dorthin. (Sie können auch einen anderen Verzeichnisnamen verwenden und die Anweisungen entsprechend anpassen.) Das Repository enthält Konfigurationsdateien sowie separate Versionen der API-Client-Anwendung, die unterschiedliche Methoden zum Abrufen von Secrets verwenden.
mkdir ~/microservices-marchcd ~/microservices-march git clone https://github.com/microservices-march/auth.git- Zeigen Sie das Secret an. Es handelt sich um ein signiertes JWT, das üblicherweise zur Authentifizierung von API-Clients gegenüber Servern verwendet wird.
cat ~/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, doch in diesem Tutorial übergibt die API-Client-App es mithilfe des OAuth 2.0 Bearer Token Authorization Frameworks an den Authentifizierungsserver. Dazu wird dem JWT wie in diesem Beispiel Authorization: Bearer vorangestellt:
"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"Authentifizierungsserver erstellen und starten
- Wechseln Sie in das Verzeichnis des Authentifizierungsservers:
cd apiserver- Erstellen Sie das Docker-Image für den Authentifizierungsserver (beachten Sie den Punkt am Ende):
docker build -t apiserver .- Starten Sie den Authentifizierungsserver und vergewissern Sie sich, dass er ausgeführt wird (zur besseren Lesbarkeit ist die Ausgabe auf mehrere Zeilen verteilt):
docker run -d -p 80:80 apiserver docker ps
CONTAINER ID IMAGE COMMAND ...
2b001f77c5cb apiserver "nginx -g 'daemon of..." ...
... CREATED STATUS ...
... 26 seconds ago Up 26 seconds ...
... PORTS ...
... 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp ...
... NAMES
... relaxed_proskuriakovaAuthentifizierungsserver testen
- Überprüfen Sie, ob der Authentifizierungsserver eine Anfrage ohne JWT ablehnt und dabei
401AuthorizationRequiredzurückgibt:
curl -X GET http://localhost<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.23.3</center>
</body>
</html>- Stellen Sie das JWT über den
Authorization-Header bereit. Der Rückgabecode200OKzeigt an, dass die API-Client-App erfolgreich authentifiziert wurde.
curl -i -X GET -H "Authorization: Bearer `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhostHTTP/1.1 200 OK
Server: nginx/1.23.2
Date: Day, DD Mon YYYY hh:mm:ss TZ
Content-Type: text/html
Content-Length: 64
Last-Modified: Day, DD Mon YYYY hh:mm:ss TZ
Connection: keep-alive
ETag: "63dc0fcd-40"
X-MESSAGE: Success apiKey1
Accept-Ranges: bytes
{ "response": "success", "authorized": true, "value": "999" }Aufgabe 1: Geheimnisse fest in Ihre App einprogrammieren (bloß nicht!)
Bevor Sie mit dieser Aufgabe beginnen, möchten wir eines klarstellen: Geheimnisse fest in Ihre App einzuprogrammieren, ist eine äußerst schlechte Idee! Sie werden sehen, wie einfach jeder, der Zugriff auf das Container-Image hat, fest einprogrammierte Zugangsdaten finden und extrahieren kann.
In dieser Aufgabe kopieren Sie den Code für die API-Client-App in das Build-Verzeichnis, erstellen und starten die App und extrahieren das Geheimnis.
API-Client-App kopieren
Das Unterverzeichnis app_versions des Verzeichnisses apiclient enthält verschiedene Versionen der einfachen API-Client-App für die vier Aufgaben, die jeweils etwas sicherer als die vorherige Version sind (weitere Informationen finden Sie unter „Tutorial-Übersicht“).
- Wechseln Sie in das Verzeichnis des API-Clients:
cd ~/microservices-march/auth/apiclient- Kopieren Sie die App für diese Aufgabe – also die mit einem fest codierten Geheimnis – in das Arbeitsverzeichnis:
cp ./app_versions/very_bad_hard_code.py ./app.py- Sehen Sie sich die App an:
cat app.pyimport 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)
try:
with urllib.request.urlopen(req) as response:
the_page = response.read()
message = response.getheader("X-MESSAGE")
print("200 " + message)
except urllib.error.URLError as e:
print(str(e.code) + " s " + e.msg)- Der Code sendet einfach eine Anfrage an einen lokalen Host und gibt entweder eine Erfolgsmeldung oder einen Fehlercode aus. Die Anfrage fügt in dieser Zeile den
Authorization-Header hinzu:
req.add_header("Authorization", authstring)- Fällt Ihnen noch etwas auf? Vielleicht ein fest codiertes JWT? Darauf kommen wir gleich zurück. Zunächst erstellen und starten wir die App.
API-Client-App erstellen und ausführen
Wir verwenden den Befehl „docker compose“ zusammen mit einer Docker-Compose-YAML-Datei. Dadurch lässt sich etwas leichter nachvollziehen, was vor sich geht.
(Beachten Sie, dass Sie in Schritt 2 des vorherigen Abschnitts die Python-Datei für die API-Client-App, die speziell für Aufgabe 1 vorgesehen ist (very_bad_hard_code.py), in app.py umbenannt haben. Dies werden Sie auch bei den anderen drei Aufgaben tun. Die durchgängige Verwendung von app.py vereinfacht den Ablauf, da Sie das Dockerfile nicht ändern müssen. Allerdings müssen Sie dabei das Argument ‑build in den Befehl docker compose aufnehmen, um jedes Mal eine Neuerstellung des Containers zu erzwingen.)
Der Befehl „docker compose“ erstellt den Container, startet die Anwendung, sendet eine einzelne API-Anfrage und fährt anschließend den Container herunter. Dabei werden die Ergebnisse des API-Aufrufs auf der Konsole angezeigt.
Der 200-Code Success 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 mit diesem Namen im JWT dekodieren konnte:
docker compose -f docker-compose.hardcode.yml up -build
...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0Die fest codierten Anmeldedaten haben für unsere API-Client-App also wie erwartet funktioniert. Aber ist das auch sicher? Vielleicht, denn der Container führt dieses Skript nur einmal aus, bevor er beendet wird, und verfügt über keine Shell.
Tatsächlich – nein, überhaupt nicht sicher.
Geheimnis aus dem Container-Image abrufen
Fest im Code hinterlegte Zugangsdaten können von jedem eingesehen werden, der Zugriff auf das Container-Image hat, da sich das Dateisystem eines Containers ohne Weiteres extrahieren lässt.
- Erstellen Sie das Verzeichnis extract und wechseln Sie dorthin:
mkdir extractcd extract- Zeigen Sie grundlegende Informationen zu den Container-Images an. Das Flag „
--format“ verbessert die Lesbarkeit der Ausgabe (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 NAMES IMAGE ...
11b73106fdf8 apiclient-apiclient-1 apiclient ...
ad9bdc05b07c exciting_clarke apiserver ...
... CREATED STATUS
... 6 minutes ago Exited (0) 4 minutes ago
... 43 minutes ago Up 43 minutes- Extrahieren Sie das neueste apiclient-Image als .tar-Datei. Ersetzen Sie für
<container_ID>den Wert durch den Wert aus dem FeldCONTAINERIDin der obigen Ausgabe (in diesem Tutorial11b73106fdf8):
docker export -o api.tar <container_ID>- Das Erstellen des Archivs api.tar dauert einige Sekunden. Es enthält das gesamte Dateisystem des Containers. Eine Möglichkeit, geheime Daten zu finden, besteht darin, das gesamte Archiv zu extrahieren und zu analysieren. Es gibt jedoch eine Abkürzung, um wahrscheinlich interessante Inhalte zu finden: die Anzeige des Containerverlaufs mit dem Befehl
dockerhistory. (Diese Abkürzung ist besonders praktisch, da sie auch bei Containern funktioniert, die Sie auf Docker Hub oder in einer anderen Container-Registry finden und für die Ihnen daher möglicherweise nicht das Dockerfile, sondern nur das Container-Image vorliegt.) - Zeigen Sie den Verlauf des Containers an:
docker history apiclient
IMAGE CREATED ...
9396dde2aad0 8 minutes ago ...
<missing> 8 minutes ago ...
<missing> 28 minutes ago ...
... CREATED BY SIZE ...
... CMD ["python" "./app.py"] 622B ...
... COPY ./app.py ./app.py # buildkit 0B ...
... WORKDIR /usr/app/src 0B ...
... COMMENT
... buildkit.dockerfile.v0
... buildkit.dockerfile.v0
... buildkit.dockerfile.v0- Die Ausgabezeilen sind in umgekehrt chronologischer Reihenfolge angeordnet. Sie zeigen, dass das Arbeitsverzeichnis auf /usr/app/src festgelegt und anschließend die Python-Codedatei der App dorthin kopiert und ausgeführt wurde. Man muss kein großer Detektiv sein, um daraus zu schließen, dass sich die zentrale Codebasis dieses Containers unter /usr/app/src/app.py befindet und dies daher ein wahrscheinlicher Speicherort für Zugangsdaten ist.
- Mit diesem Wissen können Sie nun gezielt diese Datei extrahieren:
tar --extract --file=api.tar usr/app/src/app.py- Zeigen Sie den Inhalt der Datei an, und schon haben wir Zugriff auf das „sichere“ JWT erhalten:
cat usr/app/src/app.py
...
jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
...Herausforderung 2: Geheimnisse als Umgebungsvariablen übergeben (wieder: nein!)
Wenn Sie Einheit 1 des Microservices March 2023 (Anwendung der Zwölf-Faktoren-App auf Microservices-Architekturen) abgeschlossen haben, sind Sie bereits damit vertraut, Konfigurationsdaten mithilfe von Umgebungsvariablen an Container zu übergeben. Falls Sie die Einheit verpasst haben, keine Sorge – sie ist nach der Registrierung auf Abruf verfügbar.
In dieser Aufgabe übergeben Sie Geheimnisse als Umgebungsvariablen. Wie schon die Methode aus Aufgabe 1 empfehlen wir auch diese nicht! Sie ist zwar nicht so schlecht wie das feste Einprogrammieren von Geheimnissen, hat aber, wie Sie sehen werden, einige Schwächen.
Es gibt vier Möglichkeiten, Umgebungsvariablen an einen Container zu übergeben:
- Verwenden Sie die
ENV-Anweisung in einem Dockerfile, um Variablen zu ersetzen (die Variable wird für alle erstellten Images festgelegt). Beispiel:
ENV PORT $PORT- Verwenden Sie das Flag
‑ebeim Befehldockerrun. Beispiel:
docker run -e PASSWORD=123 mycontainer- Verwenden Sie den Schlüssel „
environment“ in einer Docker-Compose-YAML-Datei. - Verwenden Sie eine .env-Datei, die die Variablen enthält.
In dieser Aufgabe verwenden Sie eine Umgebungsvariable, um das JWT festzulegen, und untersuchen den Container, um festzustellen, ob das JWT offengelegt wird.
Eine Umgebungsvariable übergeben
- Wechseln Sie zurück in das Verzeichnis des API-Clients:
cd ~/microservices-march/auth/apiclient- Kopieren Sie die App für diese Aufgabe – also die App, die Umgebungsvariablen verwendet – in das Arbeitsverzeichnis und überschreiben Sie dabei die Datei „app.py“ aus Aufgabe 1:
cp ./app_versions/medium_environment_variables.py ./app.py- Sehen Sie sich die App an. In den relevanten Ausgabezeilen wird das Geheimnis (JWT) als Umgebungsvariable im lokalen Container eingelesen:
cat app.py...
jwt = ""
if "JWT" in os.environ:
jwt = "Bearer " + os.environ.get("JWT")
...- Wie oben erläutert, gibt es verschiedene Möglichkeiten, die Umgebungsvariable in den Container zu übertragen. Aus Gründen der Einheitlichkeit verwenden wir weiterhin Docker Compose. Zeigen Sie den Inhalt der Docker-Compose-YAML-Datei an, in der mit dem Schlüssel „
environment“ die Umgebungsvariable „JWT“ festgelegt wird:
cat docker-compose.env.yml---
version: "3.9"
services:
apiclient:
build: .
image: apiclient
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- JWT- Führen Sie die App aus, ohne die Umgebungsvariable festzulegen. Der
401-FehlerUnauthorizedin der vorletzten Zeile der Ausgabe bestätigt, dass die Authentifizierung fehlgeschlagen ist, weil die API-Client-App das JWT nicht übergeben hat:
docker compose -f docker-compose.env.yml up -build...
apiclient-apiclient-1 | 401 Unauthorized
apiclient-apiclient-1 exited with code 0- Legen Sie der Einfachheit halber die Umgebungsvariable lokal fest. An dieser Stelle des Tutorials ist das in Ordnung, da dies momentan nicht das relevante Sicherheitsproblem ist:
export JWT=`cat token1.jwt`- Führen Sie den Container erneut aus. Nun ist der Test erfolgreich und zeigt dieselbe Meldung wie in Aufgabe 1 an:
docker compose -f docker-compose.env.yml up -build
...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0Zumindest enthält das Basis-Image nun nicht mehr das Geheimnis, und wir können es zur Laufzeit übergeben, was sicherer ist. Es gibt jedoch weiterhin ein Problem.
Container untersuchen
- Zeigen Sie Informationen zu den Container-Images an, um die Container-ID der API-Client-App zu ermitteln (zur besseren Lesbarkeit ist die Ausgabe auf zwei Zeilen verteilt):
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"CONTAINER ID NAMES IMAGE ...
6b20c75830df apiclient-apiclient-1 apiclient ...
ad9bdc05b07c exciting_clarke apiserver ...
... CREATED STATUS
... 6 minutes ago Exited (0) 6 minutes ago
... About an hour ago Up About an hour- Überprüfen Sie den Container der API-Client-App. Ersetzen Sie für
<container_ID>den Wert durch den Wert aus dem FeldCONTAINERIDin der obigen Ausgabe (hier6b20c75830df). Mit dem Befehldockerinspectkönnen Sie alle gestarteten Container überprüfen, unabhängig davon, ob sie derzeit ausgeführt werden. Und genau das ist das Problem: Obwohl der Container nicht ausgeführt wird, legt die Ausgabe das JWT im ArrayEnvoffen, da es unsicher in der Containerkonfiguration gespeichert ist.
docker inspect <container_ID>...
"Env": [
"JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA...",
"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..."
]Aufgabe 3: Lokale Geheimnisse verwenden
Inzwischen haben Sie gelernt, dass das Festcodieren von Geheimnissen und die Verwendung von Umgebungsvariablen nicht so sicher sind, wie Sie (oder Ihr Sicherheitsteam) es benötigen.
Um die Sicherheit zu erhöhen, können Sie versuchen, vertrauliche Informationen mithilfe lokaler Docker-Secrets zu speichern. Auch dies ist nicht die optimale Methode, aber es ist hilfreich zu verstehen, wie sie funktioniert. Selbst wenn Sie Docker nicht in der Produktion einsetzen, ist die wichtigste Erkenntnis, wie Sie das Extrahieren des Secrets aus einem Container erschweren können.
In Docker werden Secrets einem Container über den Dateisystem-Mount /run/secrets/ bereitgestellt. Dort enthält eine separate Datei den Wert des jeweiligen Secrets.
In dieser Aufgabe übergeben Sie ein lokal gespeichertes Secret an den Container mithilfe von Docker Compose und überprüfen anschließend, dass das Secret nicht sichtbar ist im Container, wenn diese Methode verwendet wird.
Ein lokal gespeichertes Geheimnis an den Container übergeben
- Wie Sie inzwischen vielleicht schon erwarten, wechseln Sie zunächst in das Verzeichnis apiclient:
cd ~/microservices-march/auth/apiclient- Kopieren Sie die App für diese Aufgabe – also die App, die Secrets aus einem Container verwendet – in das Arbeitsverzeichnis und überschreiben Sie dabei die Datei „app.py“ aus Aufgabe 2:
cp ./app_versions/better_secrets.py ./app.py- 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 enthält. Vielleicht im Microservices March 2024?)
cat app.py...
jotfile = "/run/secrets/jot"
jwt = ""
if os.path.isfile(jotfile):
with open(jotfile) as jwtfile:
for line in jwtfile:
jwt = "Bearer " + line
...- Okay, wie erstellen wir nun dieses Secret? Die Antwort finden wir in der Datei docker-compose.secrets.yml.
- Sehen Sie sich die Docker-Compose-Datei an, in der die Secret-Datei im Abschnitt
secretsdefiniert und anschließend vom Dienstapiclientreferenziert wird:
cat docker-compose.secrets.yml---
version: "3.9"
secrets:
jot:
file: token1.jwt
services:
apiclient:
build: .
extra_hosts:
- "host.docker.internal:host-gateway"
secrets:
- jotÜberprüfen, dass das Secret im Container nicht sichtbar ist
- Führen Sie die App aus. Da wir das JWT innerhalb des Containers zugänglich gemacht haben, ist die Authentifizierung erfolgreich und die inzwischen vertraute Meldung wird angezeigt:
docker compose -f docker-compose.secrets.yml up -build...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0- Zeigen Sie Informationen zu den Container-Images an und notieren Sie sich dabei die Container-ID der API-Client-App (eine Beispielausgabe finden Sie in Schritt 1 unter „Container untersuchen“ aus Aufgabe 2):
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"- Überprüfen Sie den Container für die API-Client-App. Ersetzen Sie für „
<container_ID>“ den Platzhalter durch den Wert aus dem Feld „CONTAINERID“ in der Ausgabe des vorherigen Schritts. Anders als in der Ausgabe in Schritt 2 unter „Container untersuchen“ gibt es am Anfang des Abschnitts „JWT=“ keine entsprechende Zeile im Abschnitt „Env“:
docker inspect <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 befindet sich im Container-Dateisystem unter /run/secrets/jot. Vielleicht können wir es dort mit derselben Methode extrahieren wie unter „Geheimnis aus dem Container-Image abrufen“ aus Challenge 1.
- Wechseln Sie in das Verzeichnis „extract“ (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>- Suchen Sie in der tar-Datei nach der Secrets-Datei:
tar tvf api2.tar | grep jot-rwxr-xr-x 0 0 0 0 Mon DD hh:mm run/secrets/jot- Oh oh, die Datei mit dem JWT ist sichtbar. Hatten wir nicht gesagt, dass das Einbetten von Geheimnissen in den Container „sicher“ sei? Ist die Lage genauso schlimm wie in Challenge 1?
- Sehen wir mal nach – extrahieren Sie die Datei mit den Geheimnissen aus der tar-Datei und sehen Sie sich ihren Inhalt an:
tar --extract --file=api2.tar run/secrets/jotcat run/secrets/jot- Gute Nachrichten! Der Befehl
catgibt nichts aus. Das bedeutet, dass die Datei run/secrets/jot im Container-Dateisystem leer ist – dort ist also kein Geheimnis zu finden! Selbst wenn sich in unserem Container ein Secrets-Artefakt befindet, ist Docker intelligent genug, keine sensiblen Daten im Container zu speichern.
Allerdings hat diese Container-Konfiguration trotz ihrer Sicherheit einen Nachteil. Beim Ausführen des Containers ist sie darauf angewiesen, dass im lokalen Dateisystem eine Datei namens token1.jwt vorhanden ist. Wenn Sie die Datei umbenennen, schlägt der Versuch fehl, den Container neu zu starten. (Sie können dies selbst ausprobieren, indem Sie token1.jwt umbenennen [nicht löschen!] und den Befehl docker compose aus Schritt 1 erneut ausführen.)
Damit haben wir die Hälfte geschafft: Der Container verwendet Secrets so, dass sie nicht ohne Weiteres kompromittiert werden können, doch auf dem Host ist das Secret weiterhin ungeschützt. Secrets sollten nicht unverschlüsselt in einer Klartextdatei gespeichert werden. Es ist an der Zeit, ein Tool zur Verwaltung von Secrets einzusetzen.
Aufgabe 4: Einen Secrets Manager verwenden
Ein Secrets Manager hilft Ihnen dabei, Geheimnisse während ihres gesamten Lebenszyklus zu verwalten, abzurufen und zu rotieren. Es stehen zahlreiche Secrets Manager zur Auswahl, die alle einen ähnlichen Zweck erfüllen:
- Geheimnisse sicher speichern
- Zugriff kontrollieren
- Zur Laufzeit verteilen
- Rotation von Geheimnissen aktivieren
Für die Verwaltung von Geheimnissen stehen Ihnen folgende Optionen zur Verfügung:
- Alle Cloud-Anbieter verfügen über einen Dienst zur Verwaltung von Geheimnissen (zum Beispiel AWS Secrets Manager, den Secret Manager von Google Cloud Platform und den Key Vault von Microsoft Azure)
- Kubernetes verfügt über das Secret-Objekt
- Hashicorp Vault ist ein beliebter plattformübergreifender Geheimnismanager
- OpenShift verfügt über einen Dienst zur Verwaltung von Geheimnissen
- Docker Swarm verfügt über einen Dienst zur Verwaltung von Geheimnissen
Der Einfachheit halber wird in dieser Aufgabe Docker Swarm verwendet, die Prinzipien sind jedoch bei vielen Secrets-Managern dieselben.
In dieser Aufgabe 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.
Ein Docker-Secret konfigurieren
- Wie inzwischen üblich, wechseln Sie in das Verzeichnis apiclient:
cd ~/microservices-march/auth/apiclient- Docker Swarm initialisieren:
docker swarm init
Swarm initialized: current node (t0o4eix09qpxf4ma1rrs9omrm) is now a manager.
...- Erstellen Sie ein Geheimnis und speichern Sie es in „token1.jwt“:
docker secret create jot ./token1.jwtqe26h73nhb35bak5fr5east27- Zeigen Sie Informationen zum Secret an. Beachten Sie, dass der Secret-Wert (das JWT) selbst nicht angezeigt wird:
docker secret inspect jot[
{
"ID": "qe26h73nhb35bak5fr5east27",
"Version": {
"Index": 11
},
"CreatedAt": "YYYY-MM-DDThh:mm:ss.msZ",
"UpdatedAt": "YYYY-MM-DDThh:mm:ss.msZ",
"Spec": {
"Name": "jot",
"Labels": {}
}
}
]Ein Docker-Secret verwenden
Die Verwendung des Docker-Secrets im Anwendungscode des API-Clients funktioniert genauso wie bei einem lokal erstellten Secret – Sie können es aus dem Dateisystem unter /run/secrets/ auslesen. Sie müssen lediglich den Secret-Bezeichner in Ihrer Docker-Compose-YAML-Datei ändern.
- Sehen Sie sich die Docker-Compose-YAML-Datei an. Beachten Sie den Wert
trueim Feldexternal, der angibt, dass wir ein Docker-Swarm-Secret verwenden:
cat docker-compose.secretmgr.yml---
version: "3.9"
secrets:
jot:
external: true
services:
apiclient:
build: .
image: apiclient
extra_hosts:
- "host.docker.internal:host-gateway"
secrets:
- jot- Wir können also davon ausgehen, dass diese Compose-Datei mit dem Anwendungscode unseres vorhandenen API-Clients funktioniert. Na ja, fast. Docker Swarm (oder jede andere Plattform zur Container-Orchestrierung) bietet zwar einen erheblichen Mehrwert, bringt aber auch zusätzliche Komplexität mit sich. Da
dockercomposenicht mit externen Secrets funktioniert, müssen wir einige Docker-Swarm-Befehle verwenden, insbesonderedockerstackdeploy. Docker Stack blendet die Konsolenausgabe aus, daher müssen wir die Ausgabe in eine Protokolldatei schreiben und diese anschließend prüfen. Zur Vereinfachung verwenden wir außerdem eine fortlaufendewhileTrue-Schleife, damit der Container weiterläuft. - Kopieren Sie die App für diese Challenge – also diejenige, die einen Secrets Manager verwendet – in das Arbeitsverzeichnis und überschreiben Sie dabei die Datei app.py aus Challenge 3. Wenn Sie den Inhalt von app.py anzeigen, sehen Sie, dass der Code nahezu identisch mit dem Code aus Challenge 3 ist. Der einzige Unterschied besteht darin, dass eine
while-True-Schleife hinzugefügt wurde:
cp ./app_versions/best_secretmgr.py ./app.pycat ./app.py
...
while True:
time.sleep(5)
try:
with urllib.request.urlopen(req) as response:
the_page = response.read()
message = response.getheader("X-MESSAGE")
print("200 " + message, file=sys.stderr)
except urllib.error.URLError as e:
print(str(e.code) + " " + e.msg, file=sys.stderr)Container bereitstellen und Protokolle überprüfen
- Erstellen Sie den Container (in den vorherigen Aufgaben hat Docker Compose dies übernommen):
docker build -t apiclient .- Stellen Sie den Container bereit:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
Creating network secretstack_default
Creating service secretstack_apiclient- Listen Sie die laufenden Container auf und notieren Sie sich 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 ...
... NAMES ...
... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ...
... exciting_clarke ...
... IMAGE CREATED STATUS
... apiclient:latest 31 seconds ago Up 30 seconds
... apiserver 2 hours ago Up 2 hours- Zeigen Sie die Docker-Protokolldatei an. Ersetzen Sie für „
<container_ID>“ den Wert durch den Wert aus dem Feld „CONTAINERID“ in der Ausgabe des vorherigen Schritts (hier:20d0c83a8b86). Die Protokolldatei zeigt eine Reihe von Erfolgsmeldungen an, da wir dem Anwendungscode die Schleife „whileTrue“ hinzugefügt haben. Drücken SieCtrl+c, um den Befehl zu beenden.
docker logs -f <container_ID>200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
...
^cVersuchen Sie, auf das Secret zuzugreifen
Wir wissen, dass keine sensiblen Umgebungsvariablen gesetzt sind (Sie können dies jedoch jederzeit mit dem Befehl docker inspect überprüfen, wie in Schritt 2 von „Container untersuchen“ in Aufgabe 2 beschrieben).
Aus Challenge 3 wissen wir außerdem, dass die Datei /run/secrets/jot leer ist, aber Sie können dies überprüfen:
cd extractdocker export -o api3.tar tar --extract --file=api3.tar run/secrets/jot cat run/secrets/jotErfolg! Sie können das Geheimnis weder aus dem Container abrufen noch direkt aus dem Docker-Secret auslesen.
Secret rotieren
Natürlich können wir mit den entsprechenden Berechtigungen einen Dienst erstellen und ihn so konfigurieren, dass er das Secret in das Protokoll schreibt oder als Umgebungsvariable festlegt. Außerdem ist Ihnen vielleicht aufgefallen, dass die Kommunikation zwischen unserem API-Client und dem Server unverschlüsselt (im Klartext) erfolgt.
Daher ist es bei fast jedem System zur Verwaltung von Geheimnissen weiterhin möglich, dass Geheimnisse offengelegt werden. Eine Möglichkeit, potenzielle Folgeschäden zu begrenzen, besteht darin, Geheimnisse regelmäßig zu rotieren (zu ersetzen).
Mit Docker Swarm können Sie Secrets nur löschen und anschließend neu erstellen (Kubernetes ermöglicht die dynamische Aktualisierung von Secrets). Außerdem können Sie keine Secrets löschen, die laufenden Services zugeordnet sind.
- Listen Sie die laufenden Dienste auf:
docker service lsID NAME MODE ...
sl4mvv48vgjz secretstack_apiclient replicated ...
... REPLICAS IMAGE PORTS
... 1/1 apiclient:latest- Löschen Sie den Dienst „secretstack_apiclient“.
docker service rm secretstack_apiclient- Löschen Sie das Secret und erstellen Sie es mit einem neuen Token erneut:
docker secret rm jot docker secret create jot ./token2.jwt- Erstellen Sie den Dienst neu:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack- Ermitteln Sie die Container-ID für „
apiclient“ (eine Beispielausgabe finden Sie in Schritt 3 unter „Container bereitstellen und Protokolle überprüfen“):
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"- Zeigen Sie die Docker-Protokolldatei an, die eine Reihe von Erfolgsmeldungen enthält. Ersetzen Sie für
<container_ID>, den Wert durch den Wert aus dem FeldCONTAINERIDin der Ausgabe des vorherigen Schritts. Drücken SieCtrl+c, um den Befehl zu beenden.
docker logs -f <container_ID>200 Success apiKey2
200 Success apiKey2
200 Success apiKey2
200 Success apiKey2
...
^cSiehst du die Änderung von apiKey1 zu apiKey2? Du hast das Secret rotiert.
In diesem Tutorial akzeptiert der API-Server weiterhin beide JWTs. In einer Produktionsumgebung können Sie ältere JWTs jedoch außer Kraft setzen, indem Sie bestimmte Werte für die Claims im JWT voraussetzen oder die Ablaufdaten der JWTs überprüfen.
Beachten Sie außerdem: Wenn Sie ein System zur Verwaltung von Geheimnissen verwenden, in dem Ihr Geheimnis aktualisiert werden kann, muss Ihr Code das Geheimnis regelmäßig neu einlesen, damit neue Werte übernommen werden.
Bereinigen
So bereinigen Sie die Objekte, die Sie in diesem Tutorial erstellt haben:
- Löschen Sie den Dienst „secretstack_apiclient“.
docker service rm secretstack_apiclient- Löschen Sie das Secret.
docker secret rm jot- Verlassen Sie den Swarm (vorausgesetzt, Sie haben eigens für dieses Tutorial einen Swarm erstellt).
docker swarm leave --force- Beenden Sie den laufenden apiserver-Container.
docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill- Löschen Sie nicht benötigte Container, indem Sie sie zunächst auflisten und anschließend löschen.
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <container_ID>- Löschen Sie alle nicht benötigten Container-Images, indem Sie sie auflisten und anschließend löschen.
docker image list docker image rm <image_ID>Nächste Schritte
Mithilfe dieses Blogbeitrags können Sie das Tutorial in Ihrer eigenen Umgebung umsetzen oder es in unserem browserbasierten Lab ausprobieren (hier registrieren). Wenn Sie mehr über die Bereitstellung von Kubernetes-Diensten erfahren möchten, sehen Sie sich auch die anderen Aktivitäten in Einheit 2: Grundlagen der Verwaltung von Microservices-Secrets an.
Weitere Informationen zur produktionsreifen JWT-Authentifizierung mit NGINX Plus finden Sie in unserer Dokumentation. Lesen Sie außerdem den Beitrag „Authenticating API Clients with JWT and NGINX Plus“ in unserem Blog.

Über den Autor

Ähnliche Blogbeiträge
Die Zukunft von NGINX: Zurück zu unseren Open-Source-Wurzeln
Beim NGINX Sprint 2022 besinnen wir uns auf unsere Open-Source-Wurzeln. Erfahren Sie mehr über die Initiativen, die wir im kommenden Jahr verfolgen, und teilen Sie uns mit, was Sie sich wünschen.

Ein Leitfaden zur Auswahl eines Ingress-Controllers, Teil 1: Ermitteln Sie Ihre Anforderungen
Ein Ingress-Controller kann eines der leistungsstärksten Tools in Ihrem Kubernetes-Stack sein. Erfahren Sie, wie Sie Ihre Ingress-Anforderungen ermitteln, um die beste Option auszuwählen.
Bereitstellung des NGINX Ingress Controllers auf Amazon EKS: So haben wir getestet
Wir haben kürzlich den Lösungsleitfaden zum NGINX Ingress Controller um Empfehlungen zur Dimensionierung für Amazon Elastic Kubernetes Service (EKS) ergänzt. In diesem Blogbeitrag erläutern wir, wie wir diese Empfehlungen erarbeitet haben, und stellen Ihnen alle Informationen zur Verfügung, die Sie benötigen, um selbst ähnliche Tests durchzuführen.

Protokollierung des Datenverkehrs zwischen NGINX und Upstream-Servern bei CDN77
Bei der Fehlersuche bei Datenverkehrsproblemen zwischen NGINX und Upstream-Servern konnte CDN77 die benötigten Daten nur aus dem Fehlerprotokoll mit aktivierter Debug-Ausgabe beziehen, was die Leistung beeinträchtigt und riesige Dateien erzeugt. Daher entwickelte das Team ein neues benutzerdefiniertes Modul, das ausschließlich die benötigten Daten protokolliert.
Ankündigung von NGINX Plus R16
NGINX Plus R16 ist eine einheitliche, elastische Ingress- und Egress-Schicht für Anwendungen, die die Funktionen eines Load Balancers, API-Gateways und einer WAF vereint und neue Features wie clusterfähige Ratenbegrenzung, Schlüssel-Wert-Speicher, „Random with Two Choices“, verbessertes UDP-Load-Balancing und vieles mehr bietet.
5 Gründe für den Umstieg auf softwarebasiertes Load Balancing
Entdecken Sie fünf Gründe für den Wechsel zu einem softwarebasierten Load Balancer wie NGINX Plus, darunter Kosteneinsparungen, die Einführung von DevOps und eine einheitliche Lösung für alle Umgebungen.