Aller au contenu
Conteneurs & Orchestration medium

Secrets Docker : Gérer les données sensibles en toute sécurité

26 min de lecture

logo docker

Un secret est toute donnée sensible que vous ne voulez pas exposer : mot de passe de base de données, clé API, certificat TLS, token d'authentification. Lorsque vous travaillez avec Docker, il est tentant de passer ces secrets via des variables d'environnement ou de les copier directement dans l'image. Erreur fatale ! Une fois dans l'image, ces secrets sont accessibles à quiconque la télécharge. Ce guide vous montre comment gérer vos secrets proprement, au build comme au runtime.

  • Pourquoi c'est critique : où vos secrets peuvent fuiter (images, logs, historique)
  • BuildKit secrets : injection sécurisée au moment du build avec --mount=type=secret
  • Docker Compose secrets : gestion déclarative des secrets au runtime
  • Injection au runtime : bind mounts, tmpfs et variables d'environnement
  • CI/CD : intégration avec GitLab CI, GitHub Actions
  • Bonnes pratiques : chiffrement avec SOPS, audit des images
  • Connaissance de base des Dockerfile et de la CLI Docker
  • Accès à un environnement Docker fonctionnel

Selon une étude GitGuardian, des milliers d'images sur Docker Hub contiennent encore des clés AWS valides, tokens d'API et identifiants de production.

RisqueConséquenceExemple réel
Piratage cloudMachines supprimées ou utilisées pour miner des cryptosClé AWS exposée → facture de 50 000€
Fuite de donnéesAccès non autorisé à vos bases de donnéesToken PostgreSQL → dump de la BDD clients
Compromission CI/CDAccès à tous vos pipelines et artefactsSecret GitLab → push de code malveillant
Attaque supply chainInfection de toutes les images dérivéesToken NPM → package malveillant publié

Comprendre où les secrets peuvent fuiter est la première étape pour les protéger.

VecteurExemplePourquoi c'est dangereux
DockerfileENV API_KEY=abc123Visible dans l'historique et l'image
Image finaleCOPY .env /app/Accessible à quiconque pull l'image
Logs CI/CDecho $SECRET dans un scriptLogs souvent accessibles à toute l'équipe
Conteneur runtimedocker exec envVariables d'environnement visibles
Git historyCommit d'un .env puis suppressiongit log révèle tout
# ❌ INTERDIT : Le secret reste dans l'historique de l'image
ENV DATABASE_PASSWORD=supersecret
# ❌ INTERDIT : Le fichier est copié dans une couche permanente
COPY config.env /app/config.env
# ❌ INTERDIT : ARG est visible dans docker history
ARG NPM_TOKEN
RUN npm install --registry=https://:${NPM_TOKEN}@npm.pkg.github.com

Vérification : Vous pouvez voir ces secrets avec docker history :

Fenêtre de terminal
docker history mon_image --no-trunc
# Affiche toutes les instructions, y compris les ARG et ENV

Même sans être dans l'image finale, un secret peut fuiter dans les logs :

Fenêtre de terminal
# ❌ DANGEREUX : Le secret apparaît dans les logs
echo "Connexion avec le token: $API_TOKEN"
# ✅ SÉCURISÉ : Ne jamais afficher de secret
echo "Connexion au service..."

BuildKit est le moteur de build moderne de Docker, activé par défaut depuis Docker 23.0. Sa fonctionnalité phare pour la sécurité : le montage temporaire de secrets avec --mount=type=secret.

Le secret est monté uniquement pendant l'exécution d'une instruction RUN, puis disparaît. Il n'est jamais stocké dans une couche de l'image.

Fonctionnement du secret mount BuildKit : le secret est injecté temporairement pendant le RUN puis supprimé

Deux morceaux sont nécessaires et ils vivent à des endroits différents : la directive --mount=type=secret dans le Dockerfile, et le drapeau --secret sur la ligne de commande docker build. L'identifiant id fait le lien entre les deux et doit être rigoureusement identique des deux côtés. L'onglet Commande build montre les deux sources possibles : un fichier désigné par src=, ou une variable d'environnement portant le même nom que l'id lorsque src= est omis.

# syntax=docker/dockerfile:1
FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
WORKDIR /app
# target= remplace le chemin par défaut : le secret est ici monté
# sur /etc/pip.conf, et non sur /run/secrets/pip_conf
RUN --mount=type=secret,id=pip_conf,target=/etc/pip.conf \
pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]

Exemple complet : accès à un registre NPM privé

Section intitulée « Exemple complet : accès à un registre NPM privé »

Le registre npm privé est le cas d'école : npm ci a besoin du token pendant l'installation des dépendances, et plus jamais ensuite. Les quatre étapes ci-dessous construisent l'image, puis prouvent que le token n'y a laissé aucune trace. C'est ce contrôle final qui est le plus souvent omis, alors qu'il transforme une intention en garantie vérifiable.

  1. Créer le fichier .npmrc local (ne pas le commiter !)

    Fenêtre de terminal
    echo "//npm.pkg.github.com/:_authToken=${NPM_TOKEN}" > .npmrc
    echo ".npmrc" >> .gitignore
  2. Écrire le Dockerfile avec mount secret

    # syntax=docker/dockerfile:1
    FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293
    WORKDIR /app
    COPY package*.json ./
    # Le .npmrc est monté temporairement pour npm install
    RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci --omit=dev
    COPY . .
    CMD ["node", "server.js"]
  3. Builder l'image avec le secret

    Fenêtre de terminal
    docker build --secret id=npmrc,src=.npmrc -t mon-app .
  4. Vérifier que le secret n'est pas dans l'image

    Fenêtre de terminal
    # Le fichier ne doit pas exister
    docker run --rm mon-app cat /root/.npmrc
    # cat: /root/.npmrc: No such file or directory
    # L'historique ne doit pas contenir le token
    docker history mon-app --no-trunc | grep -i token
    # (aucun résultat)

Toutes ces options ont une valeur par défaut exploitable : la référence Dockerfile fixe mode à 0400, uid et gid à 0, et le point de montage à /run/secrets/<id> quand target est absent. Deux d'entre elles changent réellement le comportement. required=true transforme un secret oublié en échec de build immédiat, au lieu d'un fichier vide monté en silence. Le couple uid/gid devient indispensable dès que l'instruction RUN s'exécute sous un utilisateur non root, qui ne pourrait pas lire un fichier appartenant à root en 0400.

OptionDescriptionExemple
idIdentifiant du secret (obligatoire)id=my_secret
targetChemin de montage dans le conteneurtarget=/etc/config.json
requiredÉchoue si le secret n'est pas fournirequired=true
modePermissions du fichier (octal)mode=0400
uid, gidPropriétaire du fichieruid=1000,gid=1000
# Exemple avec toutes les options
RUN --mount=type=secret,id=db_password,target=/run/secrets/db,required=true,mode=0400,uid=1000 \
/app/init-db.sh

Depuis la syntaxe Dockerfile 1.10, l'option env= injecte le secret directement dans une variable d'environnement du RUN, sans passer par un fichier :

RUN --mount=type=secret,id=api_key,env=API_KEY \
curl -H "Authorization: Bearer $API_KEY" https://api.example.com/data

Docker Compose propose un mécanisme déclaratif pour injecter des secrets dans les conteneurs au moment de l'exécution. Les secrets sont montés en tant que fichiers dans /run/secrets/.

La déclaration se fait à deux endroits, et c'est ce qui déroute au premier essai. Un bloc secrets: à la racine du fichier indique d'où vient chaque valeur. Une clé secrets: dans le service liste ceux auxquels ce service a droit. Un conteneur ne voit que ce qu'il a explicitement demandé : c'est ce qui permet de confier le mot de passe root à la base de données sans jamais l'exposer au conteneur applicatif.

services:
app:
image: mon-app
secrets:
- db_password # Monté dans /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt # Source du secret

Cette pile illustre le partage sélectif : db_root_password n'est déclaré que dans le service db, alors que db_password est listé dans les deux services parce que WordPress et MySQL doivent tous deux le connaître. Notez qu'aucune variable ne contient de mot de passe, seulement des chemins vers /run/secrets/, grâce aux variables suffixées _FILE.

services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_root_password
- db_password
volumes:
- db_data:/var/lib/mysql
wordpress:
image: wordpress:7.0.2@sha256:9fac4d47b61186131ffefb5d966f0045d0eea94bfd7bd40cafae29b78a709d1b
depends_on:
- db
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
db_root_password:
file: ./secrets/db_root_password.txt
volumes:
db_data:

Le bloc racine accepte trois provenances, et le choix se fait sur une seule question : qui détient la valeur au moment du docker compose up ? En développement, un fichier local ignoré par Git suffit. En CI/CD, la plateforme injecte déjà la valeur dans l'environnement du runner, la clé environment: évite alors d'écrire un fichier intermédiaire. En Docker Swarm, external: true désigne un secret créé au préalable par docker secret create et chiffré dans le magasin Raft du cluster.

SourceSyntaxeCas d'usage
Fichier localfile: ./secret.txtDéveloppement, fichiers chiffrés avec SOPS
Variable d'environnementenvironment: MY_VARCI/CD, secrets injectés par la plateforme
Secret externeexternal: trueDocker Swarm, secrets pré-créés
secrets:
# Depuis un fichier
from_file:
file: ./secrets/api_key.txt
# Depuis une variable d'environnement
from_env:
environment: API_KEY
# Secret externe (Docker Swarm)
from_swarm:
external: true

Docker Compose permet aussi de passer des secrets au moment du build :

services:
app:
build:
context: .
secrets:
- npm_token
secrets:
npm_token:
environment: NPM_TOKEN

En dehors de Docker Compose, plusieurs méthodes permettent d'injecter des secrets au lancement d'un conteneur.

La colonne à lire en premier est Persistance : elle dit où la valeur du secret se trouve physiquement pendant que le conteneur tourne, et tout le reste en découle. Une variable d'environnement ressort avec docker inspect et reste lisible dans /proc/<pid>/environ. Un bind mount laisse le fichier en clair sur le disque de l'hôte, exposé à toute sauvegarde ou instantané de volume. Un montage tmpfs ne touche jamais le stockage, au prix d'un secret perdu au redémarrage du conteneur.

MéthodeSécuritéPersistanceCas d'usage
Variables d'environnement⚠️ FaiblePendant la vie du conteneurDebug, développement local
Bind mount (readonly)✅ MoyenneFichier sur l'hôteProduction simple
tmpfs✅✅ ÉlevéeRAM uniquementSecrets éphémères, haute sécurité
Docker Swarm secrets✅✅ ÉlevéeChiffré at-restClusters Swarm
Vault/External✅✅✅ Très élevéeExterneEnterprise, compliance

Variables d'environnement (à éviter en production)

Section intitulée « Variables d'environnement (à éviter en production) »
Fenêtre de terminal
# ⚠️ Le secret est visible avec docker inspect
docker run -e DATABASE_PASSWORD=secret mon-app
# ⚠️ Aussi visible avec docker exec
docker exec mon-container env | grep PASSWORD

Quand c'est acceptable : développement local, tests, conteneurs éphémères jetables.

Fenêtre de terminal
docker run \
--mount type=bind,source="$(pwd)/secrets/config.json",target=/run/secrets/config.json,readonly \
mon-app

Avantages : Simple, le fichier reste sur l'hôte (pas dans l'image). Inconvénients : Le fichier existe en clair sur le disque de l'hôte.

Le montage tmpfs stocke les données uniquement en RAM. Aucune trace sur le disque.

Fenêtre de terminal
# Créer un tmpfs pour les secrets
docker run \
--mount type=tmpfs,destination=/run/secrets,tmpfs-size=1m,tmpfs-mode=0700 \
-e SECRET_DATA="$(cat secret.json)" \
--entrypoint /bin/sh mon-app \
-c 'echo "$SECRET_DATA" > /run/secrets/config.json && exec /app/start.sh'

GitLab permet de définir des variables masquées et de type fichier.

  1. Configurer la variable dans GitLab

    • Aller dans Settings > CI/CD > Variables
    • Ajouter une variable NPM_TOKEN
    • Cocher Mask variable et Protect variable
  2. Utiliser dans .gitlab-ci.yml

    build:
    stage: build
    image: docker:29.6.2@sha256:bfec1f5159c63a81ca6fdedbd81404d2c0e16378ed0feec3bb3fbf3998847659
    services:
    - docker:dind
    variables:
    DOCKER_BUILDKIT: "1"
    script:
    # Créer le fichier temporaire
    - echo "$NPM_TOKEN" > .npmrc
    # Builder avec le secret
    - docker build --secret id=npmrc,src=.npmrc -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    # Nettoyer
    - rm -f .npmrc
    # Pousser l'image
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

L'action docker/build-push-action accepte une clé secrets: dont chaque ligne reprend la syntaxe id=valeur attendue par BuildKit : le npm_token déclaré ici est exactement l'id que le Dockerfile consomme. Trois précautions rendent ce workflow acceptable en production. Les actions sont épinglées par SHA de commit, un tag comme v5 pouvant être redéplacé par son auteur sur un autre commit. Le workflow part de permissions: {} et le job ne récupère que packages: write, strict nécessaire pour pousser sur GHCR. Enfin persist-credentials: false empêche le checkout d'écrire le GITHUB_TOKEN dans .git/config, où n'importe quelle étape suivante pourrait le relire.

name: Build and Push
on:
push:
branches: [main]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Connexion à GHCR
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build with secrets
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
secrets: |
"npm_token=${{ secrets.NPM_TOKEN }}"

Même avec --secret, les fichiers de secrets doivent être protégés avant le build. SOPS (Secrets OPerationS) permet de chiffrer des fichiers YAML, JSON ou ENV tout en gardant les clés lisibles.

Le point qui rend ce cycle sûr : le fichier chiffré est versionné dans Git, le fichier déchiffré ne l'est jamais et disparaît avant la fin du job. SOPS ne chiffre que les valeurs et laisse les clés lisibles, ce qui permet de relire un diff et de voir quelle entrée a changé sans en exposer le contenu. Ajoutez le nom du fichier déchiffré au .gitignore avant la première exécution : un rm oublié en fin de script suffit sinon à faire fuiter le secret dans un commit.

  1. Chiffrer le fichier de secrets

    Fenêtre de terminal
    # Avec une clé GPG
    sops -e -i secrets.json
    # Avec AWS KMS
    sops --kms arn:aws:kms:eu-west-1:123:key/abc -e -i secrets.json
  2. Commiter le fichier chiffré (les valeurs sont illisibles)

    {
    "api_key": "ENC[AES256_GCM,data:abc123...,type:str]",
    "db_password": "ENC[AES256_GCM,data:def456...,type:str]"
    }
  3. Déchiffrer dans le pipeline CI

    Fenêtre de terminal
    # Le runner a accès à la clé de déchiffrement
    sops -d secrets.json > decrypted.json
    docker build --secret id=config,src=decrypted.json -t mon-app .
    rm -f decrypted.json

Chaque interdit de la colonne de gauche renvoie à un mécanisme détaillé plus haut dans la page. S'il ne fallait en retenir qu'une seule ligne, ce serait la première, parce que c'est la seule dont la conséquence est irréversible : une image publiée avec un ENV sensible oblige à révoquer le secret chez son émetteur, pas seulement à reconstruire l'image. BuildKit vous prévient d'ailleurs de lui-même avec l'avertissement SecretsUsedInArgOrEnv dès qu'un ARG ou un ENV porte un nom évoquant un mot de passe.

❌ Ne jamais faire✅ Toujours faire
ENV SECRET=xxx dans Dockerfile--mount=type=secret avec BuildKit
COPY .env /app/Monter le secret au runtime
ARG pour les secretsSecrets Compose ou tmpfs
Commiter des secrets en clairChiffrer avec SOPS
echo $SECRET dans les logsMasquer et ne jamais afficher

Ces quatre commandes se lancent après le build et avant le docker push. Les deux premières inspectent l'image sans la démarrer, les deux suivantes font appel à des scanners spécialisés. Une sortie vide ne constitue jamais une preuve absolue : ces outils travaillent sur des motifs connus et passent à côté d'un secret au format inhabituel. Ils attrapent en revanche la grande majorité des fuites accidentelles, ce qui justifie de les câbler en CI.

Fenêtre de terminal
# Vérifier qu'aucun secret n'est dans l'historique
docker history mon-image --no-trunc | grep -iE 'password|secret|token|key'
# Vérifier le contenu de l'image
docker run --rm mon-image find / -name "*.env" -o -name "*secret*" 2>/dev/null
# Scanner avec Trivy
trivy image mon-image --scanners secret
# Scanner avec trufflehog : le préfixe docker:// désigne une image
# du démon local, sans lui le scanner tenterait un pull sur un registre
trufflehog docker --image docker://mon-image --results=verified

Ces outils ne se substituent pas les uns aux autres, ils couvrent des surfaces différentes. Gitleaks intervient avant le commit, en crochet pre-commit. trufflehog relit l'historique Git déjà écrit ainsi que les couches d'une image. Trivy et Dockle travaillent sur l'image construite. Placer au minimum un contrôle avant le commit et un avant le push évite d'avoir à réécrire un historique public, opération toujours douloureuse sur un dépôt partagé.

OutilUsageLien
TrivyScan d'images DockerGuide Trivy
trufflehogDétection dans Git et imagesGuide trufflehog
GitleaksPre-commit hookgitleaks.io
DockleAudit de bonnes pratiques Dockergoodwithtech/dockle

Le symptôme le plus déroutant est le secret vide. BuildKit ne signale rien quand la source est absente : il monte un fichier de taille nulle et le RUN échoue plus loin, avec un message d'erreur qui ne mentionne jamais les secrets. Ajouter required=true déplace l'échec au bon endroit. Le second piège classique est le permission denied, qui survient dès que l'instruction RUN tourne sous un utilisateur non root : le mode par défaut 0400 appartient à root, il faut donc passer uid et gid.

SymptômeCause probableSolution
secret not foundSecret non passé au buildVérifier --secret id=xxx,src=fichier
Secret visible dans docker historyUtilisation de ARG/ENVMigrer vers --mount=type=secret
Permission denied sur /run/secretsMode trop restrictifAjuster mode=0400 ou uid/gid
Secret vide dans le conteneurFichier source inexistantVérifier le chemin du src
_FILE ne fonctionne pasImage ne supporte pas la conventionLire le fichier dans un entrypoint custom

Depuis Docker Engine 23.0, les notes de version indiquent que Buildx et BuildKit sont le constructeur par défaut sous Linux et que docker build est un alias de docker buildx build : il n'y a donc plus rien à activer. La commande docker info | grep -i buildkit, encore souvent recopiée, ne renvoie plus rien sur les versions récentes et ne prouve donc pas l'absence de BuildKit. Deux contrôles restent fiables : la colonne BUILDKIT de docker buildx ls, qui donne la version du moteur, et la première ligne de l'aide de docker build.

Fenêtre de terminal
# La colonne BUILDKIT affiche la version du moteur pour chaque constructeur
docker buildx ls
# Doit afficher : Usage: docker buildx build [OPTIONS] PATH | URL | -
docker build --help | head -1

Si vous héritez d'une machine où le constructeur historique a été rétabli, la variable DOCKER_BUILDKIT=0 en est la cause. Le supprimer suffit à retrouver BuildKit, et le constructeur historique affiche de lui-même un avertissement de dépréciation à chaque build.

  1. Ne jamais utiliser ARG, ENV ou COPY pour les secrets dans un Dockerfile, ils persistent dans l'image.

  2. BuildKit --mount=type=secret est la méthode recommandée pour les secrets au build, le secret est temporaire et n'est jamais stocké.

  3. Docker Compose secrets offre une gestion déclarative au runtime, les secrets sont montés dans /run/secrets/.

  4. Préférer les fichiers aux variables d'environnement, les env vars fuient facilement via logs, proc, inspect.

  5. tmpfs pour les secrets ultra-sensibles, aucune trace sur le disque, uniquement en RAM.

  6. Chiffrer les fichiers de secrets avec SOPS avant de les commiter, permet de versionner sans exposer.

  7. Scanner régulièrement vos images avec Trivy ou trufflehog, la détection automatique complète la prévention.

Dix questions tirées de la banque conteneurs reprennent les points sur lesquels les erreurs sont les plus fréquentes : différence entre ARG et --mount=type=secret, chemin de montage par défaut, comportement de la convention _FILE. Comptez huit minutes, le seuil de réussite est fixé à 70 %.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

10 questions
8 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn