BLOG | NGINX

Tutoriel NGINX : Comment gérer en toute sécurité les secrets dans les conteneurs

NGINX-Partie-de-F5-horiz-black-type-RGB
Vignette de Robert Haynes
Robert Haynes
Publié le 14 mars 2023

Cet article est l'un des quatre tutoriels qui vous aident à mettre en pratique les concepts de Microservices de mars 2023 : Commencez à fournir des microservices :

Bon nombre de vos microservices ont besoin de secrets pour fonctionner en toute sécurité. Les exemples de secrets incluent la clé privée d’un certificat SSL/TLS, une clé API pour s’authentifier auprès d’un autre service ou une clé SSH pour la connexion à distance. Une bonne gestion des secrets nécessite de limiter strictement les contextes dans lesquels les secrets sont utilisés aux seuls endroits où ils doivent être et d'empêcher l'accès aux secrets sauf en cas de besoin. Mais cette pratique est souvent ignorée dans la précipitation du développement des applications. Le résultat ? Une mauvaise gestion des secrets est une cause fréquente de fuites d’informations et d’exploitations.

Présentation du didacticiel

Dans ce didacticiel, nous montrons comment distribuer et utiliser en toute sécurité un jeton Web JSON (JWT) qu'un conteneur client utilise pour accéder à un service. Dans les quatre défis de ce didacticiel, vous expérimentez quatre méthodes différentes de gestion des secrets, pour apprendre non seulement comment gérer correctement les secrets dans vos conteneurs, mais également les méthodes qui sont inadéquates :

Bien que ce didacticiel utilise un JWT comme exemple de secret, les techniques s'appliquent à tout ce qui concerne les conteneurs que vous devez garder secret, comme les informations d'identification de base de données, les clés privées SSL et d'autres clés API.

Le didacticiel s'appuie sur deux composants logiciels principaux :

  • Serveur API – Un conteneur exécutant NGINX Open Source et un code JavaScript NGINX de base qui extrait une revendication du JWT et renvoie une valeur de l'une des revendications ou, si aucune revendication n'est présente, un message d'erreur
  • Client API – Un conteneur exécutant un code Python très simple qui envoie simplement une requête GET au serveur API

Regardez cette vidéo pour une démonstration du didacticiel en action.

Le moyen le plus simple de réaliser ce tutoriel est de vous inscrire à Microservices March et d'utiliser le laboratoire basé sur un navigateur fourni. Cet article fournit des instructions pour exécuter le didacticiel dans votre propre environnement.

Prérequis et configuration

Prérequis

Pour réaliser le didacticiel dans votre propre environnement, vous avez besoin de :

  • Un environnement compatible Linux/Unix
  • Connaissance de base de la ligne de commande Linux
  • Un éditeur de texte comme nano ou vim
  • Docker (y compris Docker Compose et Docker Engine Swarm )
  • curl (déjà installé sur la plupart des systèmes)
  • git (déjà installé sur la plupart des systèmes)

Remarques :

  • Le didacticiel utilise un serveur de test écoutant sur le port 80. Si vous utilisez déjà le port 80, utilisez l’indicateur -p pour définir une valeur différente pour le serveur de test lorsque vous le démarrez avec la commande docker run . Ensuite, incluez le :<numéro_de_port> suffixe sur hôte local dans le boucle commandes.
  • Tout au long du didacticiel, l'invite sur la ligne de commande Linux est omise, pour faciliter le copier-coller des commandes dans votre terminal. Le tilde ( ~ ) représente votre répertoire personnel.

Installation

Dans cette section, vous clonez le référentiel du didacticiel , démarrez le serveur d'authentification et envoyez des requêtes de test avec et sans jeton.

Cloner le dépôt du didacticiel

  1. Dans votre répertoire personnel, créez le répertoire microservices-march et clonez le référentiel GitHub dedans. (Vous pouvez également utiliser un nom de répertoire différent et adapter les instructions en conséquence.) Le référentiel comprend des fichiers de configuration et des versions distinctes de l'application cliente API qui utilisent différentes méthodes pour obtenir des secrets.

    mkdir ~/microservices-marchcd ~/microservices-march
    Clone git https://github.com/microservices-march/auth.git
  2. Affichez le secret. Il s’agit d’un JWT signé, couramment utilisé pour authentifier les clients API auprès des serveurs.

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

Bien qu'il existe plusieurs façons d'utiliser ce jeton pour l'authentification, dans ce didacticiel, l'application cliente API le transmet au serveur d'authentification à l'aide du framework d'autorisation de jeton porteur OAuth 2.0 . Cela implique de préfixer le JWT avec l'autorisation : Porteur comme dans cet exemple :

« Autorisation : Porteur eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

Créer et démarrer le serveur d'authentification

  1. Accédez au répertoire du serveur d’authentification :

    cd apiserver
  2. Créez l’image Docker pour le serveur d’authentification (notez le point final) :

    docker build -t apiserver .
  3. Démarrez le serveur d’authentification et confirmez qu’il est en cours d’exécution (la sortie est répartie sur plusieurs lignes pour plus de lisibilité) :

    docker run -d -p 80:80 apiserver docker ps ID DU CONTENEUR IMAGE COMMANDE ...
    2b001f77c5cb apiserver "nginx -g 'daemon de..." ... ... STATUT CRÉÉ ... ... Il y a 26 secondes En hausse de 26 secondes ... ... PORTS ... ... 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp ... ... NOMS ...relaxed_proskuriakova

Tester le serveur d'authentification

  1. Vérifiez que le serveur d'authentification rejette une demande qui n'inclut pas le JWT, en renvoyant 401Autorisation requise :

    curl -X OBTENIR http://localhost<html>
    <head><title>Autorisation 401 requise</title></head>
    <body>
    <center><h1>Autorisation 401 requise</h1></center>
    <hr><center>nginx/1.23.3</center>
    </body>
    </html>
  2. Fournissez le JWT à l’aide de l’en-tête d’autorisation . Le200 Le code de retour OK indique que l'application cliente API a été authentifiée avec succès.

    curl -i -X GET -H "Autorisation : Porteur `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhost HTTP/1.1 200 OK Serveur : nginx/1.23.2 Date : Jour , JJ Lun AAAA hh : mm : ss TZ Type de contenu : text/html Longueur du contenu : 64 Dernière modification : Jour , JJ Lun AAAA hh : mm : ss TZ Connexion : keep-alive ETag : « 63dc0fcd-40 » X-MESSAGE : Succès apiKey1 Accept-Ranges : octets { "response": "success", "authorized": true, "value": "999" }

Défi 1 : Des secrets codés en dur dans votre application (pas du tout !)

Avant de commencer ce défi, soyons clairs : coder en dur des secrets dans votre application est une très mauvaise idée ! Vous verrez comment toute personne ayant accès à l’image du conteneur peut facilement trouver et extraire les informations d’identification codées en dur.

Dans ce défi, vous copiez le code de l'application cliente API dans le répertoire de build, créez et exécutez l'application , puis extrayez le secret .

Copier l'application client API

Le sous-répertoire app_versions du répertoire apiclient contient différentes versions de l'application client API simple pour les quatre défis, chacune légèrement plus sécurisée que la précédente (voir Présentation du didacticiel pour plus d'informations).

  1. Accédez au répertoire client de l'API :

    cd ~/microservices-march/auth/apiclient
  2. Copiez l'application pour ce défi – celle avec un secret codé en dur – dans le répertoire de travail :

    cp ./app_versions/très_mauvais_code_dur.py ./app.py
  3. Jetez un œil à l'application :

    cat app.py import urllib.request import urllib.error jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" authstring = "Porteur " + jwt req = urllib.request.Request("http://host.docker.internal") req.add_header("Autorisation", authstring) essayez : avec urllib.request.urlopen(req) comme réponse : the_page = response.read() message = response.getheader("X-MESSAGE") print("200 " + message) sauf urllib.error.URLError comme e : print(str(e.code) + " s " + e.msg)

    Le code fait simplement une demande à un hôte local et imprime soit un message de réussite, soit un code d'échec.

    La requête ajoute l'en-tête d'autorisation sur cette ligne :

    req.add_header("Autorisation", authstring)

    Remarquez-vous autre chose ? Peut-être un JWT codé en dur ? Nous y reviendrons dans une minute. Commençons par créer et exécuter l’application.

Créer et exécuter l'application client API

Nous utilisons la commande Docker Compose avec un fichier YAML Docker Compose, ce qui permet de comprendre un peu plus facilement ce qui se passe.

(Notez qu'à l'étape 2 de la section précédente, vous avez renommé le fichier Python pour l'application cliente API spécifique au défi 1 ( very_bad_hard_code.py ) en app.py . Vous ferez également cela dans les trois autres défis. L'utilisation de app.py à chaque fois simplifie la logistique car vous n'avez pas besoin de modifier le Dockerfile . Cela signifie que vous devez inclure l'argument -build dans la commande docker compose pour forcer une reconstruction du conteneur à chaque fois.)

La commande docker compose crée le conteneur, démarre l'application, effectue une seule requête API, puis arrête le conteneur, tout en affichant les résultats de l'appel API sur la console.

Le200 Le code de réussite sur l’ avant-dernière ligne de la sortie indique que l’authentification a réussi. La valeur apiKey1 est une confirmation supplémentaire, car elle montre que le serveur d'authentification a pu décoder la revendication de ce nom dans le JWT :

docker compose -f docker-compose.hardcode.yml up -build ... apiclient-apiclient-1 | 200 Succès apiKey1 apiclient-apiclient-1 est sorti avec le code 0

Les informations d’identification codées en dur ont donc fonctionné correctement pour notre application cliente API, ce qui n’est pas surprenant. Mais est-ce sécurisé ? Peut-être, puisque le conteneur exécute ce script une seule fois avant de quitter et n’a pas de shell ?

En fait, non, ce n’est pas du tout sécurisé.

Récupérer le secret de l'image du conteneur

Le codage en dur des informations d'identification les laisse ouvertes à l'inspection par toute personne pouvant accéder à l'image du conteneur, car l'extraction du système de fichiers d'un conteneur est un exercice trivial.

  1. Créez le répertoire d’extraction et accédez-y :

    mkdir extractcd extraire
  2. Répertoriez les informations de base sur les images du conteneur. L'indicateur --format rend la sortie plus lisible (et la sortie est répartie sur deux lignes ici pour la même raison) :

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" ID CONTENEUR NOMS IMAGE ...
    11b73106fdf8 apiclient-apiclient-1 apiclient ... ad9bdc05b07c excitant_clarke apiserver ... ... STATUT CRÉÉ ... Il y a 6 minutes Sorti (0) Il y a 4 minutes ... Il y a 43 minutes En haut 43 minutes
  3. Extraire l'image apiclient la plus récente sous forme de fichier .tar . Pour <container_ID>, remplacez la valeur de la RÉCIPIENT IDENTIFIANT champ dans la sortie ci-dessus (11b73106fdf8 dans ce tutoriel) :

    docker export -o api.tar <container_ID>

    Il faut quelques secondes pour créer l'archive api.tar , qui inclut l'intégralité du système de fichiers du conteneur. Une approche pour trouver des secrets consiste à extraire l'intégralité de l'archive et à l'analyser, mais il s'avère qu'il existe un raccourci pour trouver ce qui est susceptible d'être intéressant : afficher l'historique du conteneur avec la commande docker history . (Ce raccourci est particulièrement pratique car il fonctionne également pour les conteneurs que vous trouvez sur Docker Hub ou un autre registre de conteneurs et qui peuvent donc ne pas avoir le Dockerfile , mais uniquement l'image du conteneur).

  4. Afficher l'historique du conteneur :

    historique du docker apiclient IMAGE CRÉÉE ...
    9396dde2aad0 il y a 8 minutes ...  il y a 8 minutes ...  il y a 28 minutes ... ... CRÉÉ PAR TAILLE ... ... CMD ["python" "./app.py"] 622B ... ... COPIE ./app.py ./app.py # buildkit 0B ... ... RÉPERTOIRE DE TRAVAIL /usr/app/src 0B ... ... COMMENTAIRE ... buildkit.dockerfile.v0 ... buildkit.dockerfile.v0 ... buildkit.dockerfile.v0

    Les lignes de sortie sont classées par ordre chronologique inverse. Ils montrent que le répertoire de travail a été défini sur /usr/app/src , puis le fichier de code Python pour l'application a été copié et exécuté. Il n’est pas nécessaire d’être un grand détective pour déduire que la base de code principale de ce conteneur se trouve dans /usr/app/src/app.py , et qu’il s’agit donc d’un emplacement probable pour les informations d’identification.

  5. Armé de ces connaissances, extrayez simplement ce fichier :

    tar --extract --file=api.tar usr/app/src/app.py
  6. Affichez le contenu du fichier et, comme ça, nous avons accès au JWT « sécurisé » :

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

Défi 2 : Transmettre des secrets en tant que variables d’environnement (encore une fois, non !)

Si vous avez terminé l'unité 1 de Microservices de mars 2023 (Appliquer l'application à douze facteurs aux architectures de microservices), vous savez utiliser les variables d'environnement pour transmettre des données de configuration aux conteneurs. Si vous l'avez manqué, n'ayez crainte : il est disponible à la demande après votre inscription .

Dans ce défi, vous transmettez des secrets en tant que variables d’environnement. Comme la méthode du Challenge 1 , nous ne recommandons pas celle-ci ! Ce n’est pas aussi grave que de coder en dur des secrets, mais comme vous le verrez, cela présente quelques faiblesses.

Il existe quatre façons de transmettre des variables d’environnement à un conteneur :

  • Utilisez l’instruction ENV dans un Dockerfile pour effectuer une substitution de variable (définir la variable pour toutes les images créées). Par exemple:

    ENV PORT $PORT
  • Utilisez l’indicateur ‑e sur la commande docker run . Par exemple:

    docker run -e PASSWORD=123 monconteneur
  • Utilisez la clé d’environnement dans un fichier YAML Docker Compose.
  • Utilisez un fichier .env contenant les variables.

Dans ce défi, vous utilisez une variable d’environnement pour définir le JWT et examinez le conteneur pour voir si le JWT est exposé.

Transmettre une variable d'environnement

  1. Revenez au répertoire client de l’API :

    cd ~/microservices-march/auth/apiclient
  2. Copiez l'application pour ce défi (celle qui utilise des variables d'environnement) dans le répertoire de travail, en écrasant le fichier app.py du défi 1 :

    cp ./app_versions/medium_environment_variables.py ./app.py
  3. Jetez un œil à l'application. Dans les lignes de sortie concernées, le secret (JWT) est lu comme une variable d'environnement dans le conteneur local :

    cat app.py ... jwt = "" si "JWT" dans os.environ : jwt = "Porteur " + os.environ.get("JWT") ...
  4. Comme expliqué ci-dessus, il existe plusieurs façons d'introduire la variable d'environnement dans le conteneur. Pour des raisons de cohérence, nous nous en tenons à Docker Compose. Affichez le contenu du fichier YAML Docker Compose, qui utilise la clé d'environnement pour définir la variable d'environnement JWT :

    chat docker-compose.env.yml --- version: « 3.9 » services : apiclient : build : . image : apiclient hôtes supplémentaires : - « host.docker.internal:host-gateway » environnement : - JWT
  5. Exécutez l'application sans définir la variable d'environnement. Le401 Un code non autorisé sur l' avant-dernière ligne de la sortie confirme que l'authentification a échoué car l'application cliente API n'a pas transmis le JWT :

    docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 401 apiclient-apiclient-1 non autorisé s'est terminé avec le code 0
  6. Pour plus de simplicité, définissez la variable d’environnement localement. Il est tout à fait possible de le faire à ce stade du didacticiel, car ce n’est pas le problème de sécurité qui nous préoccupe actuellement :

    exporter JWT=`token1.jwt`
  7. Exécutez à nouveau le conteneur. Le test réussit maintenant, avec le même message que dans le défi 1 :

    docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 200 Succès apiKey1 apiclient-apiclient-1 est sorti avec le code 0

Ainsi, au moins maintenant, l’image de base ne contient pas le secret et nous pouvons le transmettre au moment de l’exécution, ce qui est plus sûr. Mais il reste un problème.

Examiner le conteneur

  1. Affichez des informations sur les images du conteneur pour obtenir l'ID du conteneur pour l'application cliente API (la sortie est répartie sur deux lignes pour plus de lisibilité) :

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" ID CONTENEUR NOMS IMAGE ...
    6b20c75830df apiclient-apiclient-1 apiclient ... ad9bdc05b07c excitant_clarke apiserver ... ... STATUT CRÉÉ ... Il y a 6 minutes Sorti (0) Il y a 6 minutes ... Il y a environ une heure En haut Il y a environ une heure
  2. Inspectez le conteneur de l'application cliente API. Pour <container_ID>, remplacez la valeur de la RÉCIPIENT IDENTIFIANT champ dans la sortie ci-dessus (ici 6b20c75830df).

    La commande docker inspect vous permet d'inspecter tous les conteneurs lancés, qu'ils soient actuellement en cours d'exécution ou non. Et c’est là le problème : même si le conteneur n’est pas en cours d’exécution, la sortie expose le JWT dans le tableau Env , enregistré de manière non sécurisée dans la configuration du conteneur.

    inspection du docker <container_ID>...
    "Env": [ "JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA...", "CHEMIN=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LANG=C.UTF-8", "CLÉ_GPG=A035C8C19219BA821ECEA86B64E628F8D684696D", "VERSION_PYTHON=3.11.2", "VERSION_PYTHON_PIP=22.3.1", "VERSION_PYTHON_SETUPTOOLS=65.5.1", "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...", "PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..." ]

Défi 3 : Utiliser les secrets locaux

Vous avez maintenant appris que le codage en dur des secrets et l’utilisation de variables d’environnement ne sont pas aussi sûrs que vous (ou votre équipe de sécurité) le souhaiteriez.

Pour améliorer la sécurité, vous pouvez essayer d'utiliser des secrets Docker locaux pour stocker des informations sensibles. Encore une fois, ce n’est pas la méthode de référence, mais il est bon de comprendre comment elle fonctionne. Même si vous n’utilisez pas Docker en production, l’essentiel est de savoir comment rendre difficile l’extraction du secret d’un conteneur.

Dans Docker, les secrets sont exposés à un conteneur via le système de fichiers mount /run/secrets/ où il existe un fichier séparé contenant la valeur de chaque secret.

Dans ce défi, vous transmettez un secret stocké localement au conteneur à l'aide de Docker Compose, puis vérifiez que le secret n'est pas visible dans le conteneur lorsque cette méthode est utilisée.

Transmettre un secret stocké localement au conteneur

  1. Comme vous pouvez vous y attendre maintenant, vous commencez par accéder au répertoire apiclient :

    cd ~/microservices-march/auth/apiclient
  2. Copiez l'application pour ce défi (celle qui utilise les secrets d'un conteneur) dans le répertoire de travail, en écrasant le fichier app.py du défi 2 :

    cp ./app_versions/better_secrets.py ./app.py
  3. Jetez un œil au code Python, qui lit la valeur JWT à partir du fichier /run/secrets/jot . (Et oui, nous devrions probablement vérifier que le fichier n’a qu’une seule ligne. Peut-être dans Microservices mars 2024 ?)

    cat app.py ... jotfile = "/run/secrets/jot" jwt = "" si os.path.isfile(jotfile) : avec open(jotfile) comme jwtfile : pour la ligne dans jwtfile : jwt = "Bearer " + ligne ...

    OK, alors comment allons-nous créer ce secret ? La réponse est dans le fichier docker-compose.secrets.yml .

  4. Jetez un œil au fichier Docker Compose, où le fichier secret est défini dans la section secrets puis référencé par le service apiclient :

    chat docker-compose.secrets.yml --- version: "3.9" secrets : jot : fichier : token1.jwt services : apiclient : build : . extra_hosts : - "host.docker.internal:host-gateway" secrets : - jot

Vérifiez que le secret n’est pas visible dans le conteneur

  1. Exécutez l’application. Étant donné que nous avons rendu le JWT accessible dans le conteneur, l’authentification réussit avec le message désormais familier :

    docker compose -f docker-compose.secrets.yml up -build ... apiclient-apiclient-1 | 200 Succès apiKey1 apiclient-apiclient-1 est sorti avec le code 0
  2. Affichez des informations sur les images du conteneur, en notant l'ID du conteneur pour l'application cliente API (pour un exemple de sortie, voir l'étape 1 dans Examiner le conteneur du défi 2) :

    docker ps -a --format "table {{.ID}}\t{{.Noms}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  3. Inspectez le conteneur de l'application cliente API. Pour <container_ID>, remplacez la valeur de la RÉCIPIENT IDENTIFIANT champ dans la sortie de l'étape précédente. Contrairement à la sortie de l'étape 2 de Examiner le conteneur , il n'y a pas de ligne JWT= au début de la section Env :

    inspection du docker <container_ID>
    "Env": [
    "CHEMIN=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "LANG=C.UTF-8",
    "CLÉ_GPG=A035C8C19219BA821ECEA86B64E628F8D684696D",
    "VERSION_PYTHON=3.11.2",
    "VERSION_PYTHON_PIP=22.3.1",
    "VERSION_PYTHON_SETUPTOOLS=65.5.1",
    "URL_PYTHON_GET_PIP=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...",
    "PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..."
    ]

    Jusqu'ici tout va bien, mais notre secret se trouve dans le système de fichiers conteneur dans /run/secrets/jot . Peut-être pouvons-nous l'extraire de là en utilisant la même méthode que dans Récupérer le secret de l'image conteneur du défi 1.

  4. Accédez au répertoire d'extraction (que vous avez créé lors du défi 1) et exportez le conteneur dans une archive tar :

    cd extractdocker export -o api2.tar <container_ID>
  5. Recherchez le fichier secrets dans le fichier tar :

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

    Oh oh, le fichier contenant le JWT est visible. N’avons-nous pas dit que l’intégration de secrets dans le conteneur était « sécurisée » ? Les choses sont-elles aussi mauvaises que dans le défi 1 ?

  6. Voyons voir – extrayons le fichier secrets du fichier tar et regardons son contenu :

    tar --extract --file=api2.tar exécuter/secrets/jotcat exécuter/secrets/jot

    Bonnes nouvelles! Il n'y a pas de sortie de la commande cat , ce qui signifie que le fichier run/secrets/jot dans le système de fichiers du conteneur est vide – aucun secret à y voir ! Même s'il existe un artefact secret dans notre conteneur, Docker est suffisamment intelligent pour ne stocker aucune donnée sensible dans le conteneur.

Cela dit, même si cette configuration de conteneur est sécurisée, elle présente un défaut. Cela dépend de l'existence d'un fichier appelé token1.jwt dans le système de fichiers local lorsque vous exécutez le conteneur. Si vous renommez le fichier, une tentative de redémarrage du conteneur échoue. (Vous pouvez essayer vous-même en renommant [sans supprimer !] token1.jwt et en exécutant à nouveau la commande docker compose de l'étape 1.)

Nous sommes donc à mi-chemin : le conteneur utilise les secrets d’une manière qui les protège d’une compromission facile, mais le secret n’est toujours pas protégé sur l’hôte. Vous ne voulez pas que des secrets soient stockés non chiffrés dans un fichier texte brut. Il est temps d’introduire un outil de gestion des secrets.

Défi 4 : Utiliser un gestionnaire de secrets

Un gestionnaire de secrets vous aide à gérer, récupérer et faire tourner les secrets tout au long de leur cycle de vie. Il existe de nombreux gestionnaires de secrets parmi lesquels choisir et ils remplissent tous le même objectif :

  • Stockez vos secrets en toute sécurité
  • Contrôler l'accès
  • Distribuez-les au moment de l'exécution
  • Activer la rotation secrète

Vos options de gestion des secrets incluent :

Pour plus de simplicité, ce défi utilise Docker Swarm, mais les principes sont les mêmes pour de nombreux gestionnaires de secrets.

Dans ce défi, vous créez un secret dans Docker , copiez le secret et le code client API , déployez le conteneur , voyez si vous pouvez extraire le secret et faites pivoter le secret .

Configurer un secret Docker

  1. Comme c'est désormais la tradition, passez au répertoire apiclient :

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

    docker swarm init Swarm initialisé : le nœud actuel (t0o4eix09qpxf4ma1rrs9omrm) est désormais un gestionnaire. ...
  3. Créez un secret et stockez-le dans token1.jwt :

    création d'un secret docker jot ./token1.jwt qe26h73nhb35bak5fr5east27
  4. Afficher les informations sur le secret. Notez que la valeur secrète (le JWT) n'est pas elle-même affichée :

    inspection du secret docker jot [ { "ID": "qe26h73nhb35bak5fr5east27", "Version": { "Index": 11 }, "Créé à": " AAAA - MM - JJ J hh : mm : ss . ms Z ", " Mis à jour à " : " AAAA - MM - JJ J hh : mm : ss . ms Z", "Spec": { "Nom": "jot", "Libellés": {} } } ]

Utiliser un secret Docker

L’utilisation du secret Docker dans le code de l’application cliente API est exactement la même que l’utilisation d’un secret créé localement : vous pouvez le lire à partir du système de fichiers /run/secrets/ . Il vous suffit de modifier le qualificateur secret dans votre fichier YAML Docker Compose.

  1. Jetez un œil au fichier YAML de Docker Compose. Notez la valeur true dans le champ externe , indiquant que nous utilisons un secret Docker Swarm :

    chat docker-compose.secretmgr.yml --- version: "3.9" secrets : jot : externe : vrai services : apiclient : build : . image : apiclient extra_hosts : - "host.docker.internal:host-gateway" secrets : - jot

    Nous pouvons donc nous attendre à ce que ce fichier Compose fonctionne avec notre code d’application client API existant. Enfin, presque. Bien que Docker Swarm (ou toute autre plateforme d’orchestration de conteneurs) apporte beaucoup de valeur supplémentaire, il apporte également une certaine complexité supplémentaire.

    Étant donné que Docker Compose ne fonctionne pas avec des secrets externes, nous allons devoir utiliser certaines commandes Docker Swarm, en particulier Docker StackDeploy . Docker Stack masque la sortie de la console, nous devons donc écrire la sortie dans un journal, puis inspecter le journal.

    Pour faciliter les choses, nous utilisons également une boucle while True continue pour maintenir le conteneur en cours d'exécution.

  2. Copiez l’application pour ce défi (celle qui utilise un gestionnaire de secrets) dans le répertoire de travail, en écrasant le fichier app.py du défi 3. En affichant le contenu de app.py , nous voyons que le code est presque identique au code du Challenge 3. La seule différence est l'ajout de la boucle while True :

    cp ./app_versions/best_secretmgr.py ./app.pycat ./app.py ... tant que True : time.sleep(5) essayer : avec urllib.request.urlopen(req) comme réponse : the_page = response.read() message = response.getheader("X-MESSAGE") print("200 " + message, file=sys.stderr) sauf urllib.error.URLError comme e : print(str(e.code) + " " + e.msg, file=sys.stderr)

Déployer le conteneur et vérifier les journaux

  1. Construisez le conteneur (dans les défis précédents, Docker Compose s'en est occupé) :

    docker build -t apiclient .
  2. Déployer le conteneur :

    docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack Création du réseau secretstack_default Création du service secretstack_apiclient
  3. Répertoriez les conteneurs en cours d'exécution, en notant l'ID de conteneur pour secretstack_apiclient (comme précédemment, la sortie est répartie sur plusieurs lignes pour plus de lisibilité).

    docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" ID DU CONTENEUR ...  
    20d0c83a8b86 ... ad9bdc05b07c ... ... NOMS ... ... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ... ... excitant_clarke ... ... STATUT DE CRÉATION D'IMAGE ... apiclient:latest il y a 31 secondes En hausse de 30 secondes ... apiserver il y a 2 heures En hausse de 2 heures
  4. Afficher le fichier journal Docker ; pour <container_ID>, remplacez la valeur de la RÉCIPIENT IDENTIFIANT champ dans la sortie de l'étape précédente (ici, 20d0c83a8b86). Le fichier journal affiche une série de messages de réussite, car nous avons ajouté la boucle while True au code de l'application. Appuyez sur Ctrl+c pour quitter la commande.

    journaux docker -f <container_ID>200 Succès apiKey1
    200 Succès apiKey1
    200 Succès apiKey1
    200 Succès apiKey1
    200 Succès apiKey1
    200 Succès apiKey1
    ...
    ^c

Essayez d'accéder au secret

Nous savons qu’aucune variable d’environnement sensible n’est définie (mais vous pouvez toujours vérifier avec la commande docker inspect comme à l’étape 2 de Examiner le conteneur dans le défi 2).

Du défi 3, nous savons également que le fichier /run/secrets/jot est vide, mais vous pouvez vérifier :

cd extractdocker export -o api3.tar 
tar --extract --file=api3.tar exécuter/secrets/jot
cat exécuter/secrets/jot

Succès! Vous ne pouvez pas obtenir le secret du conteneur, ni le lire directement à partir du secret Docker.

Faire tourner le secret

Bien sûr, avec les bons privilèges, nous pouvons créer un service et le configurer pour lire le secret dans le journal ou le définir comme variable d'environnement. De plus, vous avez peut-être remarqué que la communication entre notre client API et notre serveur n'est pas chiffrée (texte brut).

La fuite de secrets est donc toujours possible avec presque tous les systèmes de gestion des secrets. Une façon de limiter la possibilité de dommages est de faire tourner (remplacer) régulièrement les secrets.

Avec Docker Swarm, vous pouvez uniquement supprimer puis recréer des secrets (Kubernetes permet la mise à jour dynamique des secrets). Vous ne pouvez pas non plus supprimer les secrets associés aux services en cours d’exécution.

  1. Lister les services en cours d'exécution :

    docker service ls ID NOM MODE ... sl4mvv48vgjz secretstack_apiclient répliqué ... ... PORTS D'IMAGES DE RÉPLIQUES ... 1/1 apiclient:dernier
  2. Supprimez le service secretstack_apiclient .

    service docker rm secretstack_apiclient
  3. Supprimez le secret et recréez-le avec un nouveau jeton :

    docker secret rm jot
    docker secret create jot ./token2.jwt
  4. Recréer le service :

    déploiement de la pile docker --compose-file docker-compose.secretmgr.yml secretstack
  5. Recherchez l'ID du conteneur pour apiclient (pour un exemple de sortie, voir l'étape 3 dans Déployer le conteneur et vérifier les journaux ) :

    docker ps --format "table {{.ID}}\t{{.Noms}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  6. Affichez le fichier journal Docker, qui affiche une série de messages de réussite. Pour <container_ID>, remplacez la valeur de la RÉCIPIENT IDENTIFIANT champ dans la sortie de l'étape précédente. Appuyez sur Ctrl+c pour quitter la commande.

    journaux docker -f <container_ID>200 Succès apiKey2
    200 Succès apiKey2
    200 Succès apiKey2
    200 Succès apiKey2
    ...
    ^c

Vous voyez le changement de apiKey1 à apiKey2 ? Vous avez fait tourner le secret.

Dans ce didacticiel, le serveur API accepte toujours les deux JWT, mais dans un environnement de production, vous pouvez déprécier les anciens JWT en exigeant certaines valeurs pour les revendications dans le JWT ou en vérifiant les dates d'expiration des JWT.

Notez également que si vous utilisez un système de secrets qui permet à votre secret d’être mis à jour, votre code doit relire fréquemment le secret afin de récupérer de nouvelles valeurs secrètes.

Nettoyer

Pour nettoyer les objets que vous avez créés dans ce tutoriel :

  1. Supprimez le service secretstack_apiclient .

    service docker rm secretstack_apiclient
  2. Supprimez le secret.

    docker secret rm jot
  3. Quittez l’essaim (en supposant que vous ayez créé un essaim juste pour ce tutoriel).

    essaim de dockers quitter --force
  4. Tuez le conteneur apiserver en cours d'exécution.

    docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill
  5. Supprimez les conteneurs indésirables en les répertoriant puis en les supprimant.

    docker ps -a --format "table {{.ID}}\t{{.Noms}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <container_ID>
  6. Supprimez toutes les images de conteneur indésirables en les répertoriant et en les supprimant.

    liste d'images docker image docker rm <image_ID>

Prochaines étapes

Vous pouvez utiliser ce blog pour implémenter le didacticiel dans votre propre environnement ou l'essayer dans notre laboratoire basé sur un navigateur ( inscrivez-vous ici ). Pour en savoir plus sur le thème de l'exposition des services Kubernetes, suivez les autres activités de l'unité 2 : Gestion des secrets des microservices 101 .

Pour en savoir plus sur l’authentification JWT de niveau production avec NGINX Plus, consultez notre documentation et lisez Authentification des clients API avec JWT et NGINX Plus sur notre blog.


« Cet article de blog peut faire référence à des produits qui ne sont plus disponibles et/ou qui ne sont plus pris en charge. Pour obtenir les informations les plus récentes sur les produits et solutions F5 NGINX disponibles, explorez notre famille de produits NGINX . NGINX fait désormais partie de F5. Tous les liens NGINX.com précédents redirigeront vers un contenu NGINX similaire sur F5.com."