Tutoriel NGINX : comment gérer les secrets de manière sécurisée dans les conteneurs

NGINX | 14 mars 2023

Cet article fait partie d’une série de quatre tutoriels qui vous aident à mettre en pratique les concepts de Microservices March 2023 : commencez à déployer des microservices:

Bon nombre de vos microservices ont besoin de secrets pour fonctionner de manière sécurisée. Parmi les secrets figurent notamment la clé privée d’un certificat SSL/TLS, une clé API permettant de s’authentifier auprès d’un autre service ou encore une clé SSH pour la connexion à distance. Une gestion rigoureuse des secrets exige de limiter strictement leur utilisation aux seuls contextes où ils sont nécessaires et d’empêcher tout accès en dehors de ces situations. Pourtant, cette pratique est souvent négligée dans la précipitation liée au développement d’applications. Résultat : une mauvaise gestion des secrets est une cause fréquente de fuites d’informations et d’exploitations malveillantes.

Présentation du tutoriel

Dans ce tutoriel, nous vous montrons comment distribuer et utiliser en toute sécurité un jeton Web JSON (JWT) qu’un conteneur client utilise pour accéder à un service. Au fil des quatre exercices de ce tutoriel, vous testerez quatre méthodes différentes de gestion des secrets afin d’apprendre non seulement à gérer correctement les secrets dans vos conteneurs, mais aussi à reconnaître les méthodes inadaptées :

Bien que ce tutoriel utilise un JWT comme exemple de secret, ces techniques s’appliquent à toutes les données destinées aux conteneurs que vous devez garder secrètes, telles que les identifiants de connexion aux bases de données, les clés privées SSL et d’autres clés d’API.

Ce tutoriel s’appuie sur deux composants logiciels principaux :

  • Serveur d’API – Un conteneur exécutant NGINX Open Source et du code NGINX JavaScript élémentaire qui extrait une revendication du JWT et renvoie la 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 effectue simplement une requête GET auprès du serveur API

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

Le moyen le plus simple de suivre ce tutoriel est de vous inscrire à Microservices March et d’utiliser l’environnement de travaux pratiques fourni, accessible depuis votre navigateur. Cet article explique comment exécuter le tutoriel dans votre propre environnement.

Prérequis et configuration

Prérequis

Pour suivre ce tutoriel dans votre propre environnement, vous avez besoin des éléments suivants :

  • Un environnement compatible avec Linux/Unix
  • Connaissances de base de la ligne de commande Linux
  • A text editor like nano or 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 :

  • Ce tutoriel utilise un serveur de test qui écoute sur le port 80. Si vous utilisez déjà le port 80, utilisez l’option ‑p pour attribuer un autre port au serveur de test lorsque vous le démarrez avec la commande docker run. Ajoutez ensuite le suffixe :<port_number> à localhost dans les commandes curl.
  • Tout au long de ce tutoriel, l’invite de commande Linux est omise afin de faciliter le copier-coller des commandes dans votre terminal. Le tilde (~) représente votre répertoire personnel.

Configuration

Dans cette section, vous allez cloner le dépôt du tutoriel, démarrer le serveur d’authentification, puis envoyer des requêtes de test avec et sans jeton.

Cloner le dépôt du tutoriel

  1. Dans votre répertoire personnel, créez le répertoire microservices-march et clonez-y le dépôt GitHub. (Vous pouvez également utiliser un autre nom de répertoire et adapter les instructions en conséquence.) Le dépôt comprend des fichiers de configuration ainsi que plusieurs versions distinctes de l’application cliente de l’API, qui utilisent différentes méthodes pour obtenir les secrets.
mkdir ~/microservices-marchcd ~/microservices-march git clone https://github.com/microservices-march/auth.git
  1. Affichez le secret. Il s’agit d’un JWT signé, couramment utilisé pour authentifier les clients d’API auprès des serveurs.
cat ~/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 tutoriel, l’application cliente de l’API le transmet au serveur d’authentification à l’aide du cadre d’autorisation par jeton Bearer OAuth 2.0. Cela consiste à faire précéder le JWT de Authorization: Bearer, comme dans cet exemple :

"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

Compiler et démarrer le serveur d’authentification

  1. Accédez au répertoire du serveur d’authentification :
cd apiserver
  1. Créez l’image Docker du serveur d’authentification (notez le point final) :
docker build -t apiserver .
  1. Démarrez le serveur d’authentification et vérifiez qu’il est en cours d’exécution (la sortie est répartie sur plusieurs lignes pour en faciliter la lecture) :
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_proskuriakova

Tester le serveur d’authentification

  1. Vérifiez que le serveur d’authentification rejette toute requête ne contenant pas le JWT et renvoie 401 Authorization Required:
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>
  1. Fournissez le JWT à l’aide de l’en-tête Authorization. Le code de retour 200 OK indique que l’application cliente de l’API s’est authentifiée avec succès.
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" }

Défi 1 : coder en dur des secrets dans votre application (surtout pas !)

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

Dans ce défi, vous allez copier le code de l’application cliente de l’API dans le répertoire de build, compiler et exécuter l’application, puis extraire le secret.

Copier l’application cliente de l’API

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

  1. Accédez au répertoire du client API :
cd ~/microservices-march/auth/apiclient
  1. Copiez l’application de ce défi – celle qui contient un secret codé en dur – dans le répertoire de travail :
cp ./app_versions/very_bad_hard_code.py ./app.py
  1. Jetez un œil à l’application :
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)
  1. Le code envoie simplement une requête à un hôte local et affiche soit un message de réussite, soit un code d’échec. La requête ajoute l’en-tête « Authorization » à cette ligne :
req.add_header("Authorization", authstring)
  1. Remarquez-vous autre chose ? Peut-être un JWT codé en dur ? Nous y reviendrons dans un instant. Commençons par compiler et exécuter l’application.

Compiler et exécuter l’application cliente de l’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 de l’application cliente de l’API propre au défi 1 (very_bad_hard_code.py) en app.py. Vous ferez de même pour les trois autres défis. Utiliser app.py à chaque fois simplifie les opérations, car vous n’avez pas besoin de modifier le Dockerfile. Cela signifie toutefois que vous devez inclure l’argument ‑build dans la commande docker compose afin de forcer la 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 d’API dans la console.

Le code 200 Success figurant sur l’avant-dernière ligne de la sortie indique que l’authentification a réussi. La valeur apiKey1 le confirme également, car elle montre que le serveur d’authentification a pu décoder la revendication portant ce nom dans le JWT :

docker compose -f docker-compose.hardcode.yml up -build
...
apiclient-apiclient-1  | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0

Les identifiants codés en dur ont donc bien fonctionné pour notre application cliente d’API — rien de surprenant. Mais est-ce sécurisé ? Peut-être, puisque le conteneur n’exécute ce script qu’une seule fois avant de s’arrêter et ne dispose pas d’un shell ?

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

Récupérer le secret depuis l’image de conteneur

Le fait de coder en dur les identifiants permet à toute personne ayant accès à l’image du conteneur de les consulter, car l’extraction du système de fichiers d’un conteneur est une opération triviale.

  1. Créez le répertoire « extract », puis placez-vous dedans :
mkdir extractcd extract
  1. Affichez les informations de base sur les images de conteneur. L’option « --format » rend la sortie plus lisible (et, pour la même raison, celle-ci est répartie ici sur deux lignes) :
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
  1. Extrayez l’image apiclient la plus récente sous forme de fichier .tar. Pour <container_ID>, remplacez-la par la valeur du champ CONTAINER ID dans la sortie ci-dessus (11b73106fdf8 dans ce tutoriel) :
docker export -o api.tar <container_ID>
  1. La création de l’archive api.tar prend quelques secondes. Celle-ci contient l’intégralité du système de fichiers du conteneur. Une méthode pour rechercher des secrets consiste à extraire l’ensemble de l’archive et à l’analyser, mais il existe en fait un raccourci pour trouver ce qui est susceptible d’être intéressant : afficher l’historique du conteneur à l’aide de la commande docker history. (Ce raccourci est particulièrement pratique, car il fonctionne également pour les conteneurs trouvés sur Docker Hub ou dans un autre registre de conteneurs, pour lesquels vous ne disposez donc peut-être pas du fichier Dockerfile, mais uniquement de l’image du conteneur).
  2. Affichez l’historique du conteneur :
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
  1. Les lignes de sortie sont présentées dans l’ordre chronologique inverse. Elles indiquent que le répertoire de travail a été défini sur /usr/app/src, puis que le fichier de code Python de l’application y a été copié et exécuté. Nul besoin d’être un grand détective pour en déduire que le code principal de ce conteneur se trouve dans /usr/app/src/app.py, ce qui en fait un emplacement probable pour trouver des identifiants.
  2. Fort de ces informations, extrayez uniquement ce fichier :
tar --extract --file=api.tar usr/app/src/app.py
  1. Affichez le contenu du fichier et, comme ça, nous avons obtenu l’accès au JWT « sécurisé » :
cat usr/app/src/app.py
...
jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
...

Défi 2 : transmettre des secrets sous forme de variables d’environnement (encore une fois, non !)

Si vous avez suivi l’unité 1 de Microservices March 2023 (Appliquer l’application à douze facteurs aux architectures de microservices), vous savez déjà comment utiliser des variables d’environnement pour transmettre des données de configuration aux conteneurs. Si vous l’avez manquée, pas d’inquiétude : elle est disponible à la demande après votre inscription.

Dans ce défi, vous transmettez des secrets sous forme de variables d’environnement. Comme pour la méthode du défi 1, nous ne recommandons pas cette approche ! Elle n’est pas aussi mauvaise que l’intégration de secrets en dur dans le code, mais, comme vous le verrez, elle 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’option « ‑e » avec la commande docker run. Par exemple :
docker run -e PASSWORD=123 mycontainer
  • Utilisez la clé « environment » dans un fichier YAML Docker Compose.
  • Utilisez un fichier .env contenant les variables.

Dans ce défi, vous allez utiliser une variable d’environnement pour définir le JWT et examiner le conteneur afin de vérifier si le JWT est exposé.

Transmettre une variable d’environnement

  1. Revenez dans le répertoire du client API :
cd ~/microservices-march/auth/apiclient
  1. Copiez l’application de ce défi – celle qui utilise des variables d’environnement – dans le répertoire de travail, en remplaçant le fichier app.py du défi 1 :
cp ./app_versions/medium_environment_variables.py ./app.py
  1. Examinez l’application. Dans les lignes de sortie concernées, le secret (JWT) est lu en tant que variable d’environnement dans le conteneur local :
cat app.py...
jwt = ""
if "JWT" in os.environ:
    jwt = "Bearer " + os.environ.get("JWT")
...
  1. Comme expliqué ci-dessus, il existe plusieurs façons de transmettre la variable d’environnement au conteneur. Par souci de cohérence, nous continuerons à utiliser Docker Compose. Affichez le contenu du fichier YAML de Docker Compose, qui utilise la clé « environment » pour définir la variable d’environnement « JWT » :
cat docker-compose.env.yml---
version: "3.9"
services:
  apiclient:
    build: .
    image: apiclient
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - JWT
  1. Exécutez l’application sans définir la variable d’environnement. Le code 401 Unauthorized figurant sur l’avant-dernière ligne de la sortie confirme que l’authentification a échoué, car l’application cliente de l’API n’a pas transmis le JWT :
docker compose -f docker-compose.env.yml up -build...
apiclient-apiclient-1  | 401 Unauthorized
apiclient-apiclient-1 exited with code 0
  1. Pour simplifier, définissez la variable d’environnement localement. Vous pouvez le faire à ce stade du tutoriel, car ce n’est pas le problème de sécurité qui nous préoccupe pour le moment :
export JWT=`cat token1.jwt`
  1. Exécutez à nouveau le conteneur. Le test réussit désormais, avec le même message que dans le défi 1 :
docker compose -f docker-compose.env.yml up -build
... 
apiclient-apiclient-1  | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0

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

Examiner le conteneur

  1. Affichez les informations sur les images de conteneur afin d’obtenir l’ID du conteneur de l’application cliente de l’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}}"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
  1. Inspectez le conteneur de l’application cliente de l’API. Pour <container_ID>, remplacez-le par la valeur du champ CONTAINER ID dans la sortie ci-dessus (ici 6b20c75830df). La commande docker inspect permet d’inspecter tous les conteneurs lancés, qu’ils soient en cours d’exécution ou non. Et c’est bien là le problème : même si le conteneur n’est pas en cours d’exécution, la sortie révèle le JWT dans le tableau Env, enregistré de manière non sécurisée dans la configuration du conteneur.
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..."
]

Défi 3 : utiliser des secrets locaux

Vous savez désormais que coder les secrets en dur et utiliser des variables d’environnement n’offre pas le niveau de sécurité dont vous (ou votre équipe de sécurité) avez besoin.

Pour renforcer la sécurité, vous pouvez essayer d’utiliser les secrets Docker locaux pour stocker des informations sensibles. Là encore, ce n’est pas la méthode de référence, mais il est utile d’en comprendre le fonctionnement. Même si vous n’utilisez pas Docker en production, l’essentiel est de comprendre comment rendre difficile l’extraction du secret depuis un conteneur.

Dans Docker, les secrets sont mis à la disposition d’un conteneur via le point de montage du système de fichiers « /run/secrets/ », où un fichier distinct contient la valeur de chaque secret.

Dans cet exercice, vous allez transmettre au conteneur un secret stocké localement à l’aide de Docker Compose, puis vérifier 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 vous en doutez probablement à présent, commencez par vous placer dans le répertoire apiclient :
cd ~/microservices-march/auth/apiclient
  1. Copiez l’application de ce défi — celle qui utilise des secrets depuis l’intérieur d’un conteneur — dans le répertoire de travail, en remplaçant le fichier app.py du défi 2 :
cp ./app_versions/better_secrets.py ./app.py
  1. Examinez le code Python, qui lit la valeur du JWT dans le fichier « /run/secrets/jot ». (Et oui, nous devrions probablement vérifier que ce fichier ne contient qu’une seule ligne. Peut-être dans 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
...
  1. Bien, alors comment allons-nous créer ce secret ? La réponse se trouve dans le fichier docker-compose.secrets.yml.
  2. Examinez le fichier Docker Compose, dans lequel le fichier secret est défini dans la section « secrets », puis référencé par le service « apiclient :
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

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

  1. Exécutez l’application. Comme nous avons rendu le JWT accessible dans le conteneur, l’authentification réussit et affiche le message désormais familier :
docker compose -f docker-compose.secrets.yml up -build...
apiclient-apiclient-1  | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0
  1. Affichez les informations sur les images de conteneur, en notant l’ID du conteneur de l’application cliente de l’API (pour un exemple de sortie, consultez l’étape 1 de la section « Examiner le conteneur » du défi 2) :
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  1. Inspectez le conteneur de l’application cliente de l’API. Pour « <container_ID> », remplacez cette valeur par celle du champ « CONTAINER ID » figurant dans la sortie de l’étape précédente. Contrairement à la sortie de l’étape 2 de la section « Examiner le conteneur », aucune ligne « JWT= » ne figure au début de la section « 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..."
]
  1. Jusqu’ici, tout va bien, mais notre secret se trouve dans le système de fichiers du conteneur, à l’emplacement « /run/secrets/jot ». Nous pouvons peut-être l’en extraire en utilisant la même méthode que dans « Récupérer le secret depuis l’image du conteneur » du défi 1.
  2. Accédez au répertoire extract (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>
  1. Recherchez le fichier de secrets dans le fichier « tar » :
tar tvf api2.tar | grep jot-rwxr-xr-x  0 0      0           0 Mon DD hh:mm run/secrets/jot
  1. Oups, le fichier contenant le JWT est visible. N’avions-nous pas dit qu’intégrer des secrets dans le conteneur était « sécurisé » ? La situation est-elle aussi mauvaise que dans le défi 1 ?
  2. Voyons voir : extrayez le fichier contenant les secrets de l’archive tar et examinez son contenu :
tar --extract --file=api2.tar run/secrets/jotcat run/secrets/jot
  1. Bonne nouvelle ! La commande cat ne renvoie rien, ce qui signifie que le fichier run/secrets/jot du système de fichiers du conteneur est vide : aucun secret ne s’y trouve ! Même si notre conteneur contient un artefact de secret, Docker est suffisamment intelligent pour ne stocker aucune donnée sensible dans le conteneur.

Cela dit, bien que cette configuration de conteneur soit sécurisée, elle présente un inconvénient. Elle dépend de l’existence d’un fichier nommé token1.jwt dans le système de fichiers local au moment où vous exécutez le conteneur. Si vous renommez ce fichier, toute tentative de redémarrage du conteneur échoue. (Vous pouvez le vérifier vous-même en renommant [sans le supprimer !] token1.jwt et en exécutant à nouveau la commande docker compose de l’étape 1.)

Nous avons donc fait la moitié du chemin : le conteneur utilise les secrets de manière à éviter qu’ils ne soient facilement compromis, mais le secret reste sans protection sur l’hôte. Il est préférable de ne pas stocker de secrets non chiffrés dans un fichier texte brut. Il est temps de faire appel à 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 renouveler les secrets tout au long de leur cycle de vie. Il existe de nombreux gestionnaires de secrets parmi lesquels choisir, et ils remplissent tous un rôle similaire :

  • Stocker les secrets de manière sécurisée
  • Contrôler les accès
  • Les distribuer au moment de l’exécution
  • Activer la rotation des secrets

Vos options de gestion des secrets sont les suivantes :

Par souci de simplicité, ce challenge utilise Docker Swarm, mais les principes sont les mêmes pour de nombreux gestionnaires de secrets.

Dans ce défi, vous allez créer un secret dans Docker, copier le secret et le code du client API, déployer le conteneur, tenter d’extraire le secret, puis renouveler le secret.

Configurer un secret Docker

  1. Comme à l’accoutumée, accédez au répertoire apiclient :
cd ~/microservices-march/auth/apiclient
  1. Initialisez Docker Swarm :
docker swarm init
Swarm initialized: current node (t0o4eix09qpxf4ma1rrs9omrm) is now a manager.
...
  1. Créez un secret et stockez-le dans « token1.jwt » :
docker secret create jot ./token1.jwtqe26h73nhb35bak5fr5east27
  1. Affichez les informations relatives au secret. Notez que la valeur du secret (le JWT) n’est pas affichée :
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": {}
    }
  }
]

Utiliser un secret Docker

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

  1. Examinez le fichier YAML Docker Compose. Notez la valeur true dans le champ external, qui indique que nous utilisons un secret Docker Swarm :
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
  1. Nous pouvons donc nous attendre à ce que ce fichier Compose fonctionne avec le code existant de notre application cliente d’API. Enfin, presque. Si Docker Swarm (ou toute autre plateforme d’orchestration de conteneurs) apporte une réelle valeur ajoutée, il introduit également une certaine complexité supplémentaire. Comme docker compose ne prend pas en charge les secrets externes, nous allons devoir utiliser certaines commandes Docker Swarm, notamment docker stack deploy. Docker Stack masque la sortie de la console ; nous devons donc l’écrire dans un journal, puis consulter celui-ci. Pour simplifier les choses, nous utilisons également une boucle continue while True afin de maintenir le conteneur en cours d’exécution.
  2. Copiez l’application de ce défi – celle qui utilise un gestionnaire de secrets – dans le répertoire de travail, en remplaçant le fichier app.py du défi 3. En affichant le contenu de app.py, nous constatons que le code est presque identique à celui du défi 3. La seule différence est l’ajout de la boucle while True :
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)

Déployer le conteneur et consulter les journaux

  1. Construisez le conteneur (dans les défis précédents, Docker Compose s'en chargeait) :
docker build -t apiclient .
  1. Déployez le conteneur :
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
Creating network secretstack_default
Creating service secretstack_apiclient
  1. Répertoriez les conteneurs en cours d’exécution et notez l’ID du conteneur secretstack_apiclient (comme précédemment, la sortie est répartie sur plusieurs lignes pour en faciliter la lecture).
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
  1. Affichez le fichier journal Docker ; pour <container_ID>, remplacez-le par la valeur du champ CONTAINER ID figurant 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.
docker logs -f <container_ID>200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
...
^c

Essayez d’accéder au secret

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

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

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

Succès ! Vous ne pouvez pas récupérer le secret depuis le conteneur, ni le lire directement depuis le secret Docker.

Renouveler le secret

Bien entendu, avec les privilèges appropriés, nous pouvons créer un service et le configurer pour qu’il écrive le secret dans le journal ou le définisse comme variable d’environnement. Par ailleurs, vous avez peut-être remarqué que les communications entre notre client API et le serveur ne sont pas chiffrées (elles sont en texte clair).

Ainsi, une fuite de secrets reste possible avec presque tous les systèmes de gestion des secrets. Pour limiter les dommages potentiels qui pourraient en résulter, une solution consiste à renouveler (remplacer) régulièrement les secrets.

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

  1. Répertoriez les services en cours d’exécution :
docker service lsID             NAME                    MODE         ... 
sl4mvv48vgjz   secretstack_apiclient   replicated   ... 


    ... REPLICAS   IMAGE              PORTS
    ... 1/1        apiclient:latest
  1. Supprimez le service « secretstack_apiclient ».
docker service rm secretstack_apiclient
  1. Supprimez le secret et recréez-le avec un nouveau jeton :
docker secret rm jot docker secret create jot ./token2.jwt
  1. Recréez le service :
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
  1. Recherchez l’ID du conteneur pour apiclient (pour un exemple de sortie, consultez l’étape 3 de la section « Déployer le conteneur et consulter les journaux ») :
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  1. Affichez le fichier journal de Docker, qui contient une série de messages de réussite. Pour « <container_ID> », remplacez cette valeur par celle du champ « CONTAINER ID » dans la sortie de l’étape précédente. Appuyez sur Ctrl+c pour quitter la commande.
docker logs -f <container_ID>200 Success apiKey2
200 Success apiKey2
200 Success apiKey2
200 Success apiKey2
...
^c

Vous voyez le passage de « apiKey1 » à « apiKey2 » ? Vous avez renouvelé le secret.

Dans ce tutoriel, le serveur d’API accepte toujours les deux JWT, mais dans un environnement de production, vous pouvez rendre les anciens JWT obsolètes en exigeant certaines valeurs pour les revendications qu’ils contiennent ou en vérifiant leur date d’expiration.

Notez également que si vous utilisez un système de gestion des secrets permettant la mise à jour de votre secret, votre code doit relire fréquemment celui-ci afin de prendre en compte ses nouvelles valeurs.

Nettoyage

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

  1. Supprimez le service « secretstack_apiclient ».
docker service rm secretstack_apiclient
  1. Supprimez le secret.
docker secret rm jot
  1. Quittez le swarm (en supposant que vous en avez créé un uniquement pour ce tutoriel).
docker swarm leave --force
  1. Arrêtez le conteneur apiserver en cours d’exécution.
docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill
  1. Supprimez les conteneurs indésirables en les répertoriant, puis en les supprimant.
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <container_ID>
  1. Supprimez toutes les images de conteneur indésirables en les répertoriant, puis en les supprimant.
docker image list docker image rm <image_ID>

Étapes suivantes

Vous pouvez vous appuyer sur cet article pour suivre le tutoriel dans votre propre environnement ou l’essayer dans notre atelier accessible depuis un navigateur (inscrivez-vous ici). Pour en savoir plus sur l’exposition des services Kubernetes, poursuivez avec les autres activités de l’Unité 2 : Les fondamentaux de la gestion des secrets des microservices.

Pour en savoir plus sur l’authentification JWT adaptée aux environnements de production avec NGINX Plus, consultez notre documentation et lisez l’article « Authentification des clients d’API avec JWT et NGINX Plus » sur notre blog.

Bannière indiquant « Microservices March 2023 : inscrivez-vous gratuitement dès aujourd’hui »


Partager
Mots-clés : Security, F5 NGINX, Tech

À propos de l’auteur

Robert Haynes
Robert HaynesResponsable marketing technique

Autres articles de Robert Haynes

Articles de blog associés

Tendances actuelles des technologies cloud natives, de l’ingénierie de plateforme et de l’IA
NGINX | 13/10/2025

Tendances actuelles des technologies cloud natives, de l’ingénierie de plateforme et de l’IA

Découvrez les principaux enseignements tirés des réponses à l’enquête annuelle F5 NGINX sur les dernières tendances en matière de technologies cloud natives, de sécurité, d’ingénierie de plateforme et d’IA.

Quels contrôleurs Ingress NGINX sont affectés par les vulnérabilités CVE-2022-4886, CVE-2023-5043 et CVE-2023-5044 ?
NGINX | 03/11/2023

Quels contrôleurs Ingress NGINX sont affectés par les vulnérabilités CVE-2022-4886, CVE-2023-5043 et CVE-2023-5044 ?

Lorsque de nouvelles CVE sont signalées, il est facile de ne plus savoir si elles concernent un outil de contrôleur Ingress NGINX particulier, car il existe plusieurs projets. Cet article présente les CVE et explique comment distinguer les différents projets de contrôleurs Ingress afin de dissiper toute confusion.

Votre API est-elle en temps réel ? Testez sa latence avec l’outil rtapi de NGINX
NGINX | 11/03/2020

Votre API est-elle en temps réel ? Testez sa latence avec l’outil rtapi de NGINX

Votre API répond-elle aux requêtes en moins de 30 ms ? Si ce n’est pas le cas, il ne s’agit pas d’une API en temps réel offrant la faible latence exigée par la grande majorité des organisations. Mesurez la latence de votre API avec l’outil rtapi de NGINX et découvrez comment NGINX peut l’aider à offrir une réactivité en temps réel.

Un testeur d’expressions régulières pour NGINX et NGINX Plus
NGINX | 11/06/2019

Un testeur d’expressions régulières pour NGINX et NGINX Plus

Le testeur d’expressions régulières (regex) pour NGINX et NGINX Plus vous évite de tâtonner en vous indiquant si une expression régulière utilisée dans un bloc location ou map correspond aux valeurs comme prévu. Il génère la configuration appropriée et affiche les valeurs des groupes de capture.

Caches partagés avec les clusters de cache NGINX Plus, partie 1
NGINX | 24/01/2017

Caches partagés avec les clusters de cache NGINX Plus, partie 1

Optimisez la capacité de votre cache web en le répartissant entre plusieurs serveurs de cache web NGINX ou NGINX Plus. Découvrez comment procéder dans cet article.

Introduction aux microservices
NGINX | 19/05/2015

Introduction aux microservices

Les microservices suscitent actuellement beaucoup d’intérêt. Cet article de blog est le premier d’une série de sept consacrée à la conception, au développement et au déploiement de microservices.