La plupart des fuites de secrets dans les environnements Docker ne sont pas des piratages à la Hollywood. Elles sont ennuyeuses : un .env oublié intégré dans une image, un artefact CI téléchargé « pour débogage », ou un développeur qui exécute docker inspect et colle accidentellement la sortie dans un ticket.
Corriger cela ne demande pas une nouvelle équipe plateforme, une migration vers un coffre-fort, ou un rituel de comité de douze semaines. Il faut une disposition de fichiers qui rende le chemin sécurisé le chemin le plus simple — et qui rende le chemin non sécurisé suffisamment pénible pour que les gens cessent de le faire.
La disposition unique de fichiers qui change tout
Voici la disposition. Ce n’est pas joli. Ce n’est pas ingénieux. C’est le genre de chose que vous adoptez une fois puis dont vous ne voulez plus jamais parler, ce qui est exactement ce que vous souhaitez pour les secrets.
cr0x@server:~$ tree -a -L 3 .
.
├── .dockerignore
├── .gitignore
├── Dockerfile
├── README.md
├── compose.yaml
├── scripts
│ ├── bootstrap-dev.sh
│ └── verify-no-secrets.sh
├── src
│ └── app.py
├── config
│ ├── app.example.yaml
│ └── logging.yaml
└── secrets
├── README.md
├── dev
│ ├── app.env.example
│ └── tls.example
└── runtime
├── DO_NOT_COMMIT
└── .keep
Ce que signifie chaque répertoire (et ce qu’il interdit)
src/: uniquement le code applicatif. Jamais de secrets. Si le code a besoin d’un secret, il le lit au runtime depuis un chemin de fichier ou depuis une variable d’environnement injectée. Il n’embarque pas le secret.config/: configurations non secrètes commises dans git. Fournissez des modèles*.example*pour éviter que les ingénieurs n’inventent des noms ad hoc commeprod.env.secrets/: voici l’astuce. Vous créez un emplacement pour les secrets afin que les gens cessent de les parsemer partout. Mais vous rendez aussi impossible de les committer via des règles et des outils :secrets/dev/contient des exemples uniquement pour le développement local. Les fichiers d’exemple montrent la forme, pas le contenu.secrets/runtime/est l’endroit où les vrais secrets atterrissent sur une machine ou dans un espace de travail pipeline. Il n’est jamais commis. Il est monté au runtime.
.dockerignore: votre première barrière solide. Docker ne peut copier que ce que vous envoyez dans le contexte de build. N’envoyez pas de secrets.compose.yaml: définit des montages/secrets au runtime, pas des copies au build-time.scripts/verify-no-secrets.sh: automatisation qui fait échouer les builds quand quelqu’un essaye d’être créatif.
Le .dockerignore qui fait le gros du travail
Si vous ne retenez qu’une chose de cet article, retenez ceci : votre contexte de build est une surface d’attaque. Le démon Docker (local ou distant) reçoit une archive tar de votre contexte. Si votre .env s’y trouve, vous avez déjà perdu.
cr0x@server:~$ cat .dockerignore
# Never send secrets to the build context
secrets/
**/*.pem
**/*.key
**/*id_rsa*
**/*.p12
**/*.jks
**/.env
**/.env.*
**/*credentials*
**/*token*
**/*secret*
**/kubeconfig
**/.npmrc
**/.pypirc
**/.netrc
# Common junk that bloats builds and leaks internals
.git/
.gitignore
Dockerfile*
compose*.yml
compose*.yaml
node_modules/
__pycache__/
*.log
Oui, vous pouvez encore fuir un secret avec un nom de fichier qui ne correspond pas à ces motifs. C’est pourquoi la disposition inclut aussi des scans et des garde-fous. La défense en profondeur est juste une façon élégante de dire « supposez que quelqu’un finira par faire la mauvaise chose ».
Modèle Dockerfile : builder sans secrets, exécuter avec secrets
Votre Dockerfile ne doit pas accepter des secrets via ARG. Il ne doit pas copier .env. Il ne doit pas RUN echo "$TOKEN". Si vous avez besoin de secrets au build-time (dépôts privés), utilisez les montages de secrets de BuildKit pour que le secret ne devienne jamais une couche.
cr0x@server:~$ sed -n '1,160p' Dockerfile
# syntax=docker/dockerfile:1.6
FROM python:3.12-slim AS runtime
WORKDIR /app
# Install deps first to leverage caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy only the application
COPY src/ ./src/
COPY config/ ./config/
# Run as non-root
RUN useradd -r -u 10001 appuser && chown -R appuser:appuser /app
USER appuser
# Secrets are loaded at runtime from /run/secrets or mounted files
ENV APP_CONFIG=/app/config/logging.yaml
CMD ["python", "-m", "src.app"]
Modèle Compose : injection au runtime, pas intégration au build-time
Compose est l’endroit où les gens deviennent négligents parce que « c’est juste pour le dev ». Et puis le dev devient staging, staging devient prod, et prod devient une revue d’incident.
cr0x@server:~$ sed -n '1,200p' compose.yaml
services:
app:
build:
context: .
image: acme/app:dev
environment:
# Non-secret values only
- LOG_LEVEL=info
volumes:
# Runtime config is fine if it is not secret
- ./config:/app/config:ro
# Real secrets: mounted from secrets/runtime (not in git)
- ./secrets/runtime:/run/secrets:ro
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
ports:
- "8080:8080"
Décision : si votre équipe utilise des fichiers .env, conservez-les — mais stockez-les sous secrets/runtime et montez-les en lecture seule. L’idée est d’empêcher qu’ils n’entrent dans l’historique git ou dans les couches Docker.
Blague 1 : Si vous mettez des secrets dans des variables d’environnement, ils finiront par apparaître dans une capture d’écran. Les humains font des captures d’écran comme s’ils avaient un plan de secours.
Pourquoi les secrets fuient dans Docker (modes de défaillance reproductibles)
Les fuites Docker arrivent parce que Docker est bon pour déplacer des octets et mauvais pour comprendre l’intention. Il archivera volontiers votre contexte de build, mettra en cache des couches pour toujours, et enregistrera des métadonnées auxquelles vous n’avez pas pensé. Pendant ce temps, les ingénieurs optimiseront volontiers pour « ça marche maintenant » parce que le on-call est une expérience formatrice que personne n’a demandée.
Voie de fuite n°1 : le contexte de build contient des secrets
docker build envoie tout le répertoire de contexte au démon. Si le démon est distant (courant en CI ou avec un builder partagé), vous venez d’envoyer des secrets sur le réseau. Même si le Dockerfile ne les copie jamais, la transmission du contexte elle-même peut être journalisée, mise en cache ou inspectée de façon inattendue.
Voie de fuite n°2 : les couches Docker conservent l’historique
Quand vous faites RUN export TOKEN=... && some-command ou RUN echo "$TOKEN" > /tmp/token, vous créez une couche. Même si vous supprimez ensuite le fichier, la couche peut le conserver. L’historique d’image peut aussi révéler des arguments de build et des chaînes de commande.
Voie de fuite n°3 : les variables d’environnement sont découvrables
Les variables d’environnement apparaissent dans docker inspect. Elles peuvent figurer dans des dumps de crash, des bundles de support, des listes de processus et des logs. Elles ont aussi tendance à être copiées dans des labels de monitoring (« juste par commodité »), ce qui explique comment une clé API finit dans un backend de métriques.
Voie de fuite n°4 : montages et permissions mal assortis
Monter un fichier secret est correct. Le monter en écriture et exécuter en root est la façon dont vous vous retrouvez avec des secrets modifiés, des commits accidentels, et des sessions de debug « pourquoi le conteneur a réécrit ma config ? » à 2h du matin.
Voie de fuite n°5 : artefacts CI et logs de débogage
Les systèmes CI adorent les artefacts. Les gens adorent les artefacts pour déboguer. Si votre pipeline télécharge docker inspect, la sortie de env, ou l’archive complète de l’espace de travail, les secrets s’échapperont. Pas parce que quelqu’un est malveillant. Parce que quelqu’un est fatigué.
Quelques faits et contexte historique (parce que nos erreurs sont anciennes)
- Fait 1 : Le modèle de couches d’image Docker repose sur des systèmes de fichiers en union ; supprimer un fichier dans une couche ultérieure ne le retire pas des couches précédentes. C’est pourquoi « j’ai supprimé le secret plus tard » n’est pas une solution.
- Fait 2 : Les variables d’environnement sont un mécanisme de configuration standard depuis l’Unix ancien. Elles sont pratiques — et historiquement désastreuses pour la confidentialité.
- Fait 3 : Le processus de build Docker original envoyait tout le contexte de build en flux tar vers le démon. Ce comportement a façonné des années d’exposition accidentelle de secrets dans les builders CI.
- Fait 4 : BuildKit a introduit des montages de secrets spécifiquement parce que les gens continuaient à fourrer des identifiants dans des build args et des couches d’images pour accéder à des registries privés.
- Fait 5 :
docker historypeut révéler les commandes utilisées pour construire une image. Si un secret apparaît dans une ligne de commandeRUN, il peut être visible même si le fichier n’y est pas. - Fait 6 : De nombreux runtimes et orchestrateurs de conteneurs conservent les variables d’environnement dans des magasins de métadonnées (et parfois dans des logs), multipliant la portée d’un secret mis en
ENV. - Fait 7 : L’adoption précoce des conteneurs encourageait la logique de « tout dans un seul artefact » — tout intégrer dans l’image. Les bonnes pratiques de sécurité ont évolué dans le sens inverse : images immuables, secrets mutables injectés au runtime.
- Fait 8 : Le design de Git rend la suppression des secrets difficile : même si vous supprimez un fichier, il reste dans l’historique à moins de le réécrire. La meilleure fuite est celle que vous ne commettez jamais.
- Fait 9 : De nombreuses violations médiatisées ont commencé par la découverte d’identifiants dans des dépôts sources ou des artefacts, pas par une chaîne d’exploit innovante. Les attaquants adorent les chasses au trésor.
Règles de base : où les secrets peuvent vivre, et où ils ne doivent jamais être
Ce que vous faites
- Gardez les secrets hors du contexte de build. Utilisez
.dockerignorede façon agressive et traitez-le comme un contrôle de sécurité, pas un simple réglage de performance. - Injectez les secrets au runtime via des fichiers montés. Préférez
/run/secrets(chemin conventionnel) ou un montage en lecture seule sous un répertoire dédié. - Utilisez les montages de secrets BuildKit pour l’authentification au build-time. C’est la manière la moins pire d’accéder à des dépendances privées sans faire fuir les identifiants dans les couches.
- Faites du « sécurisé » le layout par défaut. Les développeurs suivent le chemin de moindre résistance ; votre dépôt doit rendre le chemin sûr le plus court.
- Scannez en continu. Ne faites pas confiance aux humains. Et ne faites pas confiance à vous-même d’il y a six mois.
Ce que vous ne faites pas
- Pas de secrets dans
ENVou dansenvironmentde Compose. Si c’est un secret, il doit être dans un fichier ou dans une intégration de magasin de secrets, pas dans des métadonnées. - Ne copiez pas de fichiers secrets dans le Dockerfile. Pas même « pour une seconde ». Les couches sont éternelles.
- Pas de « sortie de débogage » qui affiche les env. Si votre script de debug commence par
env | sort, supprimez-le et excusez-vous auprès du futur. - Ne commettez jamais de vrais secrets, même « juste pour tester ». Les tests sont la façon dont les fuites deviennent permanentes.
Une citation pour rester honnête
Idée paraphrasée de Gene Kim (auteur DevOps/ops) : « La meilleure façon d’améliorer la fiabilité est de rendre les problèmes visibles et de les corriger systématiquement. »
Tâches pratiques : 14 commandes pour auditer et corriger les fuites
Ce sont des tâches prêtes pour la production. Chacune inclut : la commande, ce que la sortie signifie, et la décision à prendre. Exécutez-les sur une machine de développeur, en CI, et sur un serveur de build. Différents environnements fuient de façons différentes.
Tâche 1 : Confirmer que votre contexte de build n’envoie pas de secrets
cr0x@server:~$ docker build --no-cache --progress=plain -t acme/app:check .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 612B done
#2 [internal] load .dockerignore
#2 transferring context: 2B done
#3 [internal] load metadata for docker.io/library/python:3.12-slim
#4 [internal] load build context
#4 transferring context: 48.35kB 0.0s done
...
Signification de la sortie : « transferring context » montre la taille envoyée au démon. Si vous voyez des mégaoctets inattendus, vous envoyez probablement du bazar — ou des secrets.
Décision : Si le contexte est volumineux ou suspect, durcissez .dockerignore et relancez jusqu’à ce que la taille du contexte corresponde à ce que vous seriez à l’aise d’envoyer par email à un inconnu.
Tâche 2 : Vérifier que .dockerignore est bien appliqué
cr0x@server:~$ docker build -t acme/app:ignore-test --no-cache --progress=plain .
#4 [internal] load build context
#4 transferring context: 48.35kB done
#4 DONE 0.1s
Signification de la sortie : Si vous placez temporairement un gros fichier dans secrets/runtime et que la taille du contexte ne change pas, vos motifs d’ignore fonctionnent. Si elle augmente, ce n’est pas le cas.
Décision : Corrigez les motifs jusqu’à ce que le contexte reste stable même lorsque secrets/ contient des fichiers réels.
Tâche 3 : Scanner le dépôt pour les noms de fichiers de secrets courants
cr0x@server:~$ find . -maxdepth 4 -type f \( -name ".env" -o -name ".env.*" -o -name "*.pem" -o -name "*.key" -o -name "*kubeconfig*" \) -print
./secrets/dev/app.env.example
Signification de la sortie : Tout fichier en dehors de secrets/dev qui ressemble à un secret est un problème en attente d’un commit.
Décision : Déplacez les fichiers semblant être des secrets dans secrets/runtime et assurez-vous qu’ils sont ignorés par git et Docker.
Tâche 4 : S’assurer que git n’acceptera pas de secrets sous secrets/runtime
cr0x@server:~$ cat .gitignore
# runtime secrets must never be committed
secrets/runtime/*
!secrets/runtime/.keep
# local env files
.env
.env.*
Signification de la sortie : La ligne de négation conserve un fichier placeholder pour que le répertoire existe. Tout le reste est ignoré.
Décision : Si secrets/runtime n’est pas ignoré, corrigez-le maintenant ; sinon quelqu’un va committer par accident lors d’un hotfix précipité.
Tâche 5 : Détecter les secrets déjà suivis (le piège « ignoré mais commis »)
cr0x@server:~$ git ls-files | grep -E '(^|/)\.env(\.|$)|secrets/runtime|\.pem$|\.key$' || true
Signification de la sortie : Si quelque chose s’affiche, c’est déjà dans l’historique git ou suivi dans l’index.
Décision : Retirez-le de l’index et rotatez l’identifiant. L’ignorance ne désavoue pas une fuite déjà commise.
Tâche 6 : Retirer des fichiers secrets suivis accidentellement (sans supprimer les copies locales)
cr0x@server:~$ git rm --cached -r secrets/runtime
fatal: pathspec 'secrets/runtime' did not match any files
Signification de la sortie : « did not match » est bon : rien n’y est suivi. Si la commande supprime des fichiers, vous aviez un problème.
Décision : Si des fichiers ont été supprimés, committez la suppression et rotatez immédiatement tout secret présent.
Tâche 7 : Inspecter l’historique de l’image pour des arguments ou commandes ayant fuité
cr0x@server:~$ docker history --no-trunc acme/app:check | head
IMAGE CREATED CREATED BY SIZE COMMENT
a1b2c3d4e5f6 2 minutes ago CMD ["python" "-m" "src.app"] 0B buildkit.dockerfile.v0
<missing> 2 minutes ago ENV APP_CONFIG=/app/config/logging.yaml 0B buildkit.dockerfile.v0
<missing> 3 minutes ago RUN /bin/sh -c useradd -r -u 10001 appuser... 1.2MB buildkit.dockerfile.v0
Signification de la sortie : Vous cherchez des tokens, mots de passe, URLs privées, ou des instructions echo qui écrivent des secrets.
Décision : Si quelque chose ressemblant à un secret apparaît, reconstruisez avec un Dockerfile corrigé et révoquez/rotatez les identifiants. Purgez aussi les anciennes images des registres si possible.
Tâche 8 : Inspecter le système de fichiers de l’image pour des fichiers « oops »
cr0x@server:~$ docker run --rm acme/app:check sh -lc 'ls -la /run /run/secrets || true; find /app -maxdepth 3 -type f -name ".env" -o -name "*.pem" -o -name "*.key" 2>/dev/null || true'
ls: /run/secrets: No such file or directory
Signification de la sortie : Dans une image, /run/secrets n’existe généralement pas à moins d’avoir été créé. C’est normal. Ce qui n’est pas normal, c’est de trouver des .env, clés, ou certificats à l’intérieur de l’image.
Décision : Si des fichiers secrets existent dans l’image, traitez-les comme compromis et reconstruisez proprement.
Tâche 9 : Confirmer que les conteneurs n’exécutent pas de variables d’environnement secrètes
cr0x@server:~$ docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' $(docker ps -q --filter name=app) | grep -Ei 'pass|token|secret|key' || true
Signification de la sortie : Aucune sortie est l’objectif. Si vous voyez des variables ressemblant à des secrets, vous les exposez via l’inspection et possiblement via les logs.
Décision : Déplacez ces secrets vers des fichiers montés ou les secrets de l’orchestrateur et nettoyez la configuration d’environnement.
Tâche 10 : Confirmer que les fichiers secrets montés existent et sont en lecture seule
cr0x@server:~$ docker exec -it $(docker ps -q --filter name=app) sh -lc 'mount | grep /run/secrets; ls -la /run/secrets'
/dev/sda1 on /run/secrets type ext4 (ro,relatime)
total 8
drwxr-xr-x 2 root root 4096 Feb 4 10:22 .
drwxr-xr-x 1 root root 4096 Feb 4 10:22 ..
-r--r----- 1 root root 64 Feb 4 10:22 db_password
Signification de la sortie : Le montage montre (ro,...) et le fichier secret n’est pas lisible par tous.
Décision : Si c’est rw ou si les permissions sont laxistes, corrigez les manifests Compose/Kubernetes. Les secrets doivent être lisibles uniquement par l’utilisateur de l’app, pas par tout le conteneur.
Tâche 11 : Vérifier la propriété des fichiers vs l’utilisateur du conteneur (diagnostic mismatch de permissions)
cr0x@server:~$ docker exec -it $(docker ps -q --filter name=app) sh -lc 'id; stat -c "%U %G %a %n" /run/secrets/db_password'
uid=10001(appuser) gid=10001(appuser) groups=10001(appuser)
root root 440 /run/secrets/db_password
Signification de la sortie : Si le conteneur s’exécute en appuser mais que le fichier est possédé par root avec 440, votre app pourrait ne pas pouvoir le lire à moins que les permissions de groupe correspondent.
Décision : Soit exécuter l’app avec une propriété de groupe appropriée, soit utiliser 0444 pour lecture seule lorsque cela est acceptable, soit configurer la projection de secrets de l’orchestrateur avec le bon UID/GID.
Tâche 12 : Scanner l’image pour des chaînes à haute entropie (détecteur bon marché de fuite de secrets)
cr0x@server:~$ docker save acme/app:check | tar -xOf - | strings | grep -E '[A-Za-z0-9+/]{32,}={0,2}' | head
Signification de la sortie : C’est bruyant, mais cela attrape des blobs ressemblant à du base64 qui peuvent indiquer des tokens embarqués.
Décision : Si vous voyez quelque chose de suspect, affinez en exportant le système de fichiers et en greppant des chemins spécifiques. Traitez les positifs sérieusement.
Tâche 13 : Utiliser correctement le montage de secret BuildKit (exemple dépendance privée)
cr0x@server:~$ DOCKER_BUILDKIT=1 docker build \
--secret id=pypi_token,src=./secrets/runtime/pypi_token \
--progress=plain -t acme/app:with-private-deps .
#6 [runtime 2/6] RUN --mount=type=secret,id=pypi_token ...
#6 DONE 8.7s
Signification de la sortie : Vous voyez une étape RUN --mount=type=secret. Cela indique que vous ne copiez pas le token dans l’image.
Décision : Si le Dockerfile utilise ARG à la place, arrêtez-vous et refactorez. Les build args ne sont pas un stockage secret.
Tâche 14 : Prouver que le secret n’a pas fini dans l’image résultante
cr0x@server:~$ docker run --rm acme/app:with-private-deps sh -lc 'grep -R "pypi" -n / 2>/dev/null | head'
Signification de la sortie : Vous cherchez des chaînes de token ou des fichiers de config d’auth. Idéalement, cela ne renvoie rien de significatif (il peut y avoir des correspondances non pertinentes ; enquêtez si c’est le cas).
Décision : Si le token ou le fichier d’auth existe dans le système de fichiers final, l’étape de build a fui. Rotatez et corrigez le Dockerfile.
Trois mini-récits d’entreprise venus du pays du “ça marche sur ma machine”
Mini-récit 1 : L’incident causé par une mauvaise hypothèse
Une équipe liée aux paiements avait une image Docker qui buildait bien pendant des mois. Ils utilisaient un registre de paquets privé et supposaient que leur build arg était « suffisamment sûr » parce qu’il n’était passé qu’en CI. Le Dockerfile prenait ARG NPM_TOKEN, l’écrivait dans ~/.npmrc, installait les dépendances, puis supprimait le fichier. Propre, non ?
Ce n’était pas propre. Le token avait déjà vécu dans une couche. Quelqu’un a ensuite lancé un job interne de scan d’images qui sauvegardait des images dans un magasin d’artefacts partagé pour analyse. Une autre équipe — cherchant à déboguer un build — a récupéré l’artefact et a déballé le tar. Ce token n’était pas « en prod », mais c’était quand même un identifiant fonctionnel.
La portée fut embarrassante, pas catastrophique : accès à des paquets internes, une exposition mineure de la chaîne d’approvisionnement, et une semaine de rotation forcée sur plusieurs projets. Le vrai dommage fut le moral. Des ingénieurs qui faisaient attention à « ne pas committer de secrets » ont appris que les couches Docker sont leur propre type d’historique.
La correction fut ennuyeuse : montages de secrets BuildKit, un .dockerignore strict, et une politique interdisant les tokens dans les instructions Dockerfile. L’équipe a aussi arrêté d’uploader des images brutes comme « artefacts de debug ». Ils uploadent des logs de build avec des nettoyeurs et seulement les métadonnées nécessaires.
Après la tempête, quelqu’un a demandé pourquoi cela n’avait pas été détecté plus tôt. La réponse était simple : tout le monde supposait que « supprimer le fichier » veut dire « il est parti ». Cette hypothèse est ce qui piège avec les couches.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux
Un groupe plateforme a optimisé les temps de build CI en activant une mise en cache agressive des couches Docker sur des runners partagés. Les builds sont devenus plus rapides, et tout le monde était content pendant environ deux sprints. Puis ils ont commencé à voir des comportements « fantômes » : une branche fonctionnelle pouvait build même après la suppression d’un accès à un secret, parce que la couche utilisant le secret était mise en cache.
Le pire : un développeur a ajouté une ligne de debug temporaire affichant une valeur de configuration qui contenait un token. Les logs de build ont été conservés plus longtemps que prévu. Parallèlement, des couches mises en cache contenant des configs de téléchargement de dépendances étaient partagées entre projets. Ce n’est pas censé arriver, mais « censé » n’est pas un mécanisme de contrôle d’accès.
L’incident n’était pas une faille classique. C’était une exposition. La sécurité l’a classé « défaillance de gestion des identifiants ». L’équipe plateforme l’a classé « mauvaise configuration de portée du cache ». Tout le monde a convenu que la cause racine était une optimisation qui réduisait la friction pour les builds et augmentait la friction pour la containment.
Ils ont gardé la mise en cache — mais avec discipline : caches isolés par projet, pas de partage cross-tenant, et règles explicites que toute étape utilisant des secrets doit être non-cacheable ou utiliser des montages de secrets qui ne contaminent pas les couches. Ils ont aussi raccourci la rétention des logs et nettoyé les motifs de clés connus.
La performance est une fonctionnalité. La sécurité aussi. Si vous optimisez l’une en empruntant silencieusement à l’autre, vous paierez des intérêts plus tard.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation
Une entreprise du secteur santé avait une règle simple : secrets/runtime existe dans chaque repo de service, est ignoré par git et Docker, et les pipelines le montent au runtime. Pas d’exception. Les ingénieurs se sont plaints pendant un mois. Puis ils ont cessé d’y prêter attention.
Un vendredi, quelqu’un a accidentellement copié un identifiant de production dans un fichier local nommé prod.env à la racine du dépôt. Le développeur allait committer. Le hook pre-commit a hurlé. La CI aussi. Le contexte de build est resté petit parce que .dockerignore ignorait les motifs .env*, et le scan de dépôt a signalé le blob à haute entropie.
L’identifiant n’a jamais atterri dans git. Il n’a jamais atterri dans l’image. Il n’a jamais atterri dans le registre. La personne l’a quand même rotaté, car on leur avait appris à traiter un « quasi-accident » comme « peut-être compromis ». Cette rotation a pris des minutes, pas des jours, parce qu’ils avaient une procédure documentée et un chemin d’injection de secrets cohérent.
La sécurité n’a pas eu à jouer les détectives. Le SRE n’a pas eu à réveiller qui que ce soit. Le système n’a pas sauvé la journée avec une cryptographie sophistiquée ; il l’a fait avec un emplacement de fichiers prévisible et plusieurs petits garde-fous.
Blague 2 : Le meilleur secret est celui que vous ne fuites jamais. Le deuxième meilleur secret est celui que vous rotaterez avant que quelqu’un ne remarque que vous l’avez fuité.
Mode d’emploi pour diagnostic rapide (vérifier premier/deuxième/troisième)
Quand quelqu’un dit « nous avons fuité un secret » ou « le scanner dit que l’image contient des identifiants », ne débattez pas la théorie. Triez vite. Voici le plan que j’utilise.
Premier : confirmer si le secret est dans l’historique git, dans l’image, ou seulement au runtime
- Vérifier le suivi git :
git ls-files+ grep motifs ; si suivi, traitez comme compromis. - Vérifier l’historique d’image :
docker history --no-truncpour les lignes de commande visibles. - Vérifier le système de fichiers : exécuter un conteneur et
findpour les types de fichiers secrets courants.
Pourquoi en premier : la remédiation diffère. L’historique git est permanente sauf réécriture. Les registres d’image répliquent. L’exposition runtime peut être limitée à un hôte unique.
Deuxième : identifier le mécanisme d’injection
- Vars env :
docker inspectet vérifiez les manifests d’orchestration. - Fichiers montés : vérifiez les montages
/run/secretset les permissions. - Secrets au build-time : recherchez
ARG,--build-arg, et les fichiers d’auth des gestionnaires de paquets (.npmrc,.netrc).
Pourquoi en deuxième : vous devez empêcher la récurrence. La rotation du secret est l’étape zéro ; corriger le chemin est le vrai travail.
Troisième : délimiter le rayon d’impact et rotater sans hésiter
- Cherchez dans les logs CI et les artefacts la chaîne du secret ou les motifs d’identifiant.
- Vérifiez les registres pour les tags/digests affectés ; supprimez-les ou mettez-les en quarantaine.
- Rotatez les identifiants ; invalidez les tokens ; réémettez les clés ; mettez à jour les déploiements pour utiliser le nouveau chemin de secret.
Pourquoi en troisième : les secrets fuient plus vite que vous ne pouvez planifier une réunion. La rotation bat l’analyse paralysée.
Erreurs courantes : symptômes → cause racine → correctif
Erreur 1 : « Nous avons supprimé le fichier dans une étape Dockerfile ultérieure »
Symptômes : le scanner signale l’image ; vous ne trouvez pas le fichier dans le conteneur en cours d’exécution ; la sécurité insiste sur le fait qu’il est toujours présent.
Cause racine : le secret existait dans une couche antérieure ; la suppression ne fait que le masquer dans la vue finale du système de fichiers.
Correctif : reconstruisez sans jamais écrire le secret dans une couche. Utilisez les montages de secrets BuildKit ou récupérez les dépendances en dehors du build image. Purgez les anciens tags d’image et rotatez les identifiants.
Erreur 2 : « C’est bon, c’est seulement dans les variables d’environnement »
Symptômes : des secrets apparaissent dans docker inspect ; des bundles de support contiennent des tokens ; quelqu’un colle la config en chat et maintenant c’est dans l’historique indexable.
Cause racine : les vars env sont des métadonnées ; elles sont copiées, loggées, scrappées et inspectées.
Correctif : montez les secrets comme fichiers sous /run/secrets (ou projections de secrets de l’orchestrateur). Gardez les vars env pour des bascules non secrètes. Si une app ne supporte que les vars env, enveloppez-la : lisez depuis un fichier et exportez dans un petit entrypoint, mais acceptez le risque résiduel et restreignez l’accès aux inspections/logs.
Erreur 3 : « Notre .dockerignore existe, donc on est en sécurité »
Symptômes : le transfert de contexte CI est énorme ; des secrets sont trouvés dans le cache du builder ; différentes machines produisent des résultats différents.
Cause racine : .dockerignore est incomplet, ou vous buildiez depuis un contexte différent de celui que vous croyiez (builds monorepo, mauvais répertoire, différences de chemins CI).
Correctif : vérifiez la taille du contexte dans les logs de build ; définissez explicitement context: dans Compose ; ajoutez des tests qui échouent si secrets/ est dans le contexte ; standardisez les points d’entrée de build (scripts) pour que les développeurs ne lancent pas de builds artisanaux.
Erreur 4 : « Nous montons les secrets, mais l’app ne peut pas les lire »
Symptômes : l’application plante avec permission denied ; les fichiers secrets existent mais les lectures échouent ; les ingénieurs « règlent » en exécutant en root.
Cause racine : mismatch UID/GID et mode de fichier trop strict, ou secrets projetés avec des permissions root-only.
Correctif : exécutez l’app avec un UID stable ; projetez les secrets avec la propriété correcte ; utilisez des modes group-readable ; évitez « exécuter en root » comme rustine.
Erreur 5 : « Nous avons monté ./secrets dans le conteneur et oublié que c’est modifiable »
Symptômes : fichiers secrets modifiés de façon inattendue ; secrets dev locaux écrasés ; quelqu’un commite des fichiers modifiés par accident.
Cause racine : bind mount en écriture depuis l’hôte ; les processus du conteneur écrivent sur le système de fichiers hôte.
Correctif : montez les secrets en lecture seule. Rendre le système de fichiers du conteneur read_only: true et autoriser les écritures uniquement sur des chemins tmpfs.
Erreur 6 : « CI a uploadé des artefacts pour débogage, y compris l’espace de travail »
Symptômes : le secret apparaît dans le magasin d’artefacts CI ; la chaîne du token est trouvée dans des tarballs archivés ; la sécurité demande pourquoi votre pipeline est un service d’exfiltration de données.
Cause racine : règles d’upload d’artefacts trop larges ; pas de scrubber ; pas de séparation entre sortie de build et état de l’espace de travail.
Correctif : n’uploadez que les sorties de build explicites ; n’uploadez jamais docker inspect ou l’espace de travail complet ; nettoyez les logs ; gardez les secrets hors de l’espace de travail en premier lieu avec la disposition décrite ici.
Listes de contrôle / plan pas-à-pas
Pas-à-pas : adopter la disposition de fichiers dans un dépôt existant
- Créer les répertoires :
config/,secrets/dev/,secrets/runtime/,scripts/. - Déplacer les configs non secrètes de la racine du repo vers
config/. Gardez les configs commises non secrètes. - Créer des modèles d’exemples de secrets dans
secrets/dev(ex.app.env.example), et documenter les clés attendues. - Ajouter un
.gitignorestrict poursecrets/runtimeet.env*. - Ajouter un
.dockerignorestrict pour exclure les secrets, git, et la poussière CI. - Refactorer le Dockerfile pour copier uniquement
src/etconfig/. Supprimez toutCOPY . .sauf si vous aimez les audits. - Basculez le runtime vers des montages dans Compose/Kubernetes. Standardisez sur
/run/secrets. - Arrêtez d’utiliser les build args pour des secrets. Remplacez par des montages de secrets BuildKit quand c’est inévitable.
- Ajouter des garde-fous : un script qui scanne les secrets et échoue la CI. Rendez-le assez rapide pour s’exécuter sur chaque PR.
- Rotatez les identifiants si vous trouvez quoi que ce soit de suspect dans l’historique, les images, les registres, ou les logs.
Checklist CI (sécurité minimale viable)
- Builder avec BuildKit activé (
DOCKER_BUILDKIT=1). - Ne pas afficher de variables d’environnement dans les logs.
- Ne pas uploader d’archives d’espace de travail en tant qu’artefacts.
- Ne pas partager le cache de couches Docker entre projets/tenants sauf si conçu et audité.
- Exécuter un scan de secrets sur le diff git et sur l’image construite.
Checklist runtime (conteneurs en production)
- Exécuter en non-root avec un UID stable.
- Monter les secrets en lecture seule ; projeter avec des permissions correctes.
- Rendre le système de fichiers racine en lecture seule ; utiliser tmpfs pour l’espace d’écriture.
- Supprimer les capacités Linux et définir
no-new-privileges. - Assurer que les logs n’incluent jamais le contenu des fichiers secrets ni des dumps d’environnement.
FAQ
1) Un fichier .env est-il toujours une mauvaise idée ?
Non. Un .env est un format pratique. La mauvaise idée est de le laisser dériver dans git, dans les contextes de build Docker, dans les images ou dans les artefacts. Mettez les vrais dans secrets/runtime et ignorez-les.
2) Pourquoi des fichiers montés plutôt que des variables d’environnement ?
Les fichiers montés ont moins de chances d’être loggés, scrappés ou exposés via l’inspection des métadonnées. Ils supportent aussi des permissions filesystem plus strictes. Les vars env sont faciles ; facile n’est pas synonyme de sûr.
3) Qu’en est-il du support « secrets » de Docker Compose ?
Utilisez-le quand il est supporté, mais ne le considérez pas comme magique. Beaucoup d’équipes finissent par binder des fichiers secrets. La disposition fonctionne dans les deux cas : un emplacement stable sur l’hôte ou en CI où les secrets sont stockés et montés au runtime.
4) Les secrets BuildKit peuvent-ils fuir quand même ?
Oui, si vous les affichez, les écrivez sur le disque, ou les copiez dans l’image. BuildKit vous donne un mécanisme d’injection sûr ; il n’annule pas un mauvais script.
5) Nous avons besoin d’un registre de dépendances privé pendant le build. Quel est le modèle le plus sûr ?
Utilisez les montages --secret de BuildKit et configurez le gestionnaire de paquets pour lire les identifiants depuis le montage secret pendant la durée de l’étape d’installation. Gardez l’étape minimale et évitez la mise en cache si vous ne pouvez pas prouver qu’elle est propre.
6) Si un secret est commis mais que le repo est privé, doit-on quand même rotate ?
Oui. « Repo privé » n’est pas une frontière de sécurité que vous pouvez auditer parfaitement. Rotatez, puis nettoyez l’historique si la politique l’exige. La rotation est l’urgence.
7) Comment empêcher les développeurs de contourner la disposition ?
Rendez-la par défaut dans les templates, ajoutez des checks pre-commit et CI, et faites respecter .dockerignore et les patterns Dockerfile en revue. Aussi : gardez le workflow rapide pour que les gens n’inventent pas de hacks « temporaires ».
8) Et Kubernetes ?
Le même principe s’applique : secrets injectés au runtime, idéalement comme fichiers (volumes de secret). Gardez l’image conteneur libre de secrets. Standardisez sur un chemin comme /run/secrets pour que l’app ne se soucie pas de l’origine du secret.
9) Les systèmes de fichiers en lecture seule valent-ils la peine ?
Oui. Ils empêchent toute une classe d’accidents : écrire des secrets dans le filesystem du conteneur, muter la config, et laisser des miettes d’identifiants dans des couches inscriptibles. Couplez-les avec tmpfs pour /tmp et ce dont votre app a besoin.
10) Quelle est la façon la plus rapide de voir si nous fuyons via le contexte de build ?
Exécutez docker build --progress=plain et surveillez la taille du transfert de contexte. Si elle est importante ou change quand vous ajoutez un fichier sous secrets/, vos règles d’ignore sont fausses.
Prochaines étapes (à faire cette semaine)
Les secrets ne fuient pas parce que les ingénieurs sont négligents. Ils fuient parce que les systèmes sont permissifs et que les workflows récompensent la rapidité. Corrigez le workflow.
- Adoptez la disposition :
src/,config/,secrets/dev(exemples),secrets/runtime(réels, ignorés),.dockerignorestrict. - Refactorez les Dockerfiles pour copier uniquement ce dont ils ont besoin. Supprimez
COPY . .sauf si vous pouvez le justifier par écrit. - Déplacez les secrets vers des montages runtime sous
/run/secrets, en lecture seule, avec des permissions sensées et des conteneurs non-root. - Activez BuildKit et utilisez des montages de secrets pour l’auth au build-time. Interdisez
ARGpour les identifiants. - Ajoutez deux garde-fous peu coûteux : scanner le dépôt pour des fichiers à forme de secret et scanner les images construites pour du contenu en forme de secret.
- Si vous trouvez quelque chose : rotatez d’abord, analysez ensuite, et ne négociez pas avec votre propre clairvoyance rétrospective.