Où finissent les variables d’environnement d’un conteneur
Démarrez un conteneur avec -e DB_PASSWORD=hunter2 et lancez docker inspect dessus :
| |
| |
Le mot de passe reste là en clair, lisible par quiconque a accès au socket Docker, par docker exec api env, et par n’importe quel processus qui tourne dans le conteneur. Ce n’est pas un bug : c’est exactement à ça que servent les variables d’environnement. Le problème, c’est de les utiliser pour des valeurs qui ne devraient jamais être aussi visibles.
Où définir une variable d’environnement Docker : build time ou run time
Quatre mécanismes, trois durées de vie. L’endroit où vous définissez une variable détermine combien de temps elle vit et qui peut la relire :
| Mécanisme | Défini où | Reste dans l’image ? | Visible avec docker inspect ? |
|---|---|---|---|
ARG dans le Dockerfile | Au build uniquement | Non (sauf si copié dans ENV) | Non, mais enregistré dans l’historique de build |
ENV dans le Dockerfile | Au build | Oui, intégré dans chaque couche suivante | Oui |
-e / --env sur docker run | Au démarrage du conteneur | Non | Oui |
--env-file sur docker run | Au démarrage du conteneur | Non | Oui |
Au démarrage, vous les passez une par une, ou depuis un fichier :
| |
.env.production ressemble à ça :
| |
--env-file devient le meilleur choix dès que vous dépassez deux ou trois variables : la commande de démarrage reste lisible et les valeurs se retrouvent à un seul endroit que vous pouvez comparer avec un diff.
Variables d’environnement dans Docker Compose : environment, env_file et .env
Compose propose trois emplacements pour les variables, et deux sont des fichiers presque homonymes. .env et env_file: font des travaux complètement différents.
.envà la racine du projet est lu par Compose lui-même, pour substituer les placeholders${VARIABLE}danscompose.yaml. Il n’atteint jamais le conteneur, sauf si vous le référencez aussi sousenvironment:.env_file:liste des fichiers dont le contenu est injecté dans l’environnement du conteneur, exactement comme--env-filesurdocker run.environment:définit les variables directement dans le fichier Compose, en ligne.
| |
Quand une variable est définie à plus d’un endroit, Compose la résout dans cet ordre, du plus prioritaire au moins prioritaire : un -e passé à docker compose run, puis environment:, puis env_file:, puis le ENV déjà intégré à l’image. Si NODE_ENV apparaît à la fois dans environment: et dans .env.api, c’est la valeur d’environment: qui l’emporte.
Pourquoi les variables d’environnement ne conviennent pas aux secrets
Aucun des mécanismes ci-dessus ne cache une valeur à qui a accès au conteneur ou à l’hôte :
docker inspectaffiche chaque variable au démarrage, comme montré plus haut.docker exec <conteneur> envles affiche depuis l’intérieur.- Un processus enfant hérite de tout l’environnement, y compris un debugger, un crash reporter, ou une dépendance que vous n’avez pas auditée.
- Tout ce qui vide son environnement au démarrage envoie la valeur dans votre agrégateur de logs, et un nombre surprenant de frameworks font exactement ça quand le debug logging est activé.
- Les variables d’environnement définies au build avec
ENVsont permanentes :docker history --no-trunc my-apiaffiche la valeur exacte, et elle reste dans l’image sur chaque registry où vous la poussez.
Rien de tout ça ne demande qu’un attaquant compromette le conteneur. L’accès que beaucoup de gens ont déjà suffit : le socket Docker, les logs de la CI, le registry d’images.
Comment fonctionnent les secrets Docker : la valeur est montée comme fichier
Arrêtez de confier les identifiants au conteneur comme variables d’environnement et montez-les comme fichiers. Compose le fait de lui-même, sans Swarm :
| |
Compose monte db_password.txt sur /run/secrets/db_password dans le conteneur, en lecture seule et nulle part ailleurs : cette valeur n’apparaît jamais dans docker inspect, dans docker exec ... env, ni dans l’image. Le suffixe _FILE est une convention déjà prise en charge par les images officielles de Postgres, MySQL et MongoDB : au démarrage, le script d’entrypoint lit le fichier au lieu d’attendre la valeur directement.
Si votre application ne prend pas en charge cette convention, lisez le fichier vous-même au démarrage : une ligne dans la plupart des langages, par exemple open('/run/secrets/db_password').read().strip() en Python.
Sur un cluster Swarm, le secret équivalent est créé et distribué par l’orchestrateur plutôt que par un fichier local :
| |
Swarm stocke le secret chiffré au repos et en transit, et le monte dans un filesystem en mémoire sur chaque réplique : il n’est jamais écrit sur la couche modifiable du conteneur.
Garder les secrets hors de l’image au build
ARG et ENV dans un Dockerfile posent le même problème un cran plus tôt : une valeur passée comme argument de build reste enregistrée dans l’historique de build de l’image, même si vous ne la transformez jamais en ENV.
| |
| |
Cette commande docker history la retrouve. Le token disparaît du filesystem final si vous ne faites pas de COPY du fichier de configuration plus loin, mais il reste lisible en permanence dans les métadonnées de l’image, qui voyagent avec elle sur chaque registry.
Le flag --secret de BuildKit évite ça en montant la valeur dans une seule étape RUN, comme fichier en mémoire qui ne devient jamais une couche :
| |
| |
npm_token n’existe que pour la durée de cette instruction RUN et n’est enregistré nulle part où docker history pourrait le voir.
Quand utiliser une variable d’environnement, quand utiliser un secret
| Situation | Utilisez |
|---|---|
| Configuration non sensible (port, log level, feature flag) | -e, --env-file, ou environment:/env_file: de Compose |
| Mot de passe de base de données, clé API, clé TLS au runtime | Un secret Docker, monté comme fichier |
Token d’authentification nécessaire seulement pendant docker build | --secret de BuildKit avec RUN --mount=type=secret |
| Valeur intégrée à l’image intentionnellement (version de l’app, commit de build) | ARG copié dans ENV : c’est correct, ce n’est pas un secret |
Une seule question tranche : ça vous dérangerait que cette valeur fuite ? Si oui, elle ne passe pas par -e, --env-file, environment:, ni par un ARG/ENV de Dockerfile. Elle passe par un secret monté comme fichier. Les réglages que votre application affiche volontiers dans ses propres logs restent de simples variables d’environnement, plus faciles à surcharger selon l’environnement.
Allez donc vérifier. Lancez docker inspect --format '{{json .Config.Env}}' sur les conteneurs qui tournent en ce moment, et lisez le résultat comme le diff d’une pull request. Tout ce que vous n’aimeriez pas voir relu en public doit rejoindre un bloc secrets: dans votre fichier Compose, avec une variable _FILE à la place du mot de passe. Si le fichier doit survivre au conteneur plutôt que venir du contexte de build, placez-le sur un volume Docker.