"Si ce n'est pas dans Git, c'est bon." C'est une idée fausse. Les secrets fuient aussi dans les logs applicatifs, les images Docker, les variables d'environnement, les dumps mémoire et bien d'autres endroits. Ce guide identifie ces vecteurs de fuite au-delà du code source, les classe par famille et explique les contre-mesures réellement fiables selon le contexte.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Les trois familles de fuite : observabilité, build/artefacts, exécution
- Comment les secrets fuient dans les logs, traces et stack traces
- La distinction entre build secrets et runtime secrets dans Docker
- Pourquoi les variables d'environnement sont plus exposées qu'on ne le pense
- Les contre-mesures pour les dumps, les métadonnées cloud et les fichiers d'auth locaux
Prérequis
Section intitulée « Prérequis »Les trois familles de fuite
Section intitulée « Les trois familles de fuite »Les secrets fuient dans bien plus d'endroits que le code source. Pour mieux appréhender ces surfaces, on peut les regrouper en trois familles :
Fuites d'observabilité
Section intitulée « Fuites d'observabilité »Tout ce qui sert à comprendre le comportement de l'application peut capturer un secret au passage : logs, traces distribuées, stack traces, tickets de support, dumps partagés.
Fuites de build et d'artefacts
Section intitulée « Fuites de build et d'artefacts »Tout ce qui est produit lors de la construction du logiciel peut embarquer un secret : layers Docker, caches de build, artefacts de CI, fichiers de configuration générés.
Fuites d'exécution
Section intitulée « Fuites d'exécution »Tout ce qui est accessible au runtime peut exposer un secret : variables
d'environnement, /proc, métadonnées cloud (IMDS), mémoire du processus.
Tableau des surfaces par famille
Section intitulée « Tableau des surfaces par famille »Ce tableau sert de carte des risques : la colonne « Qui y a accès » est la plus instructive, car elle montre que la fuite ne s'arrête presque jamais à l'endroit où elle se produit. Un secret écrit dans un log part vers l'agrégateur, puis vers l'archivage, puis vers les sauvegardes. Utilisez-le comme grille de revue, surface par surface, plutôt que comme simple récapitulatif.
| Famille | Surface | Comment le secret fuit | Qui y a accès |
|---|---|---|---|
| Observabilité | Logs applicatifs | Affiché en debug, traces HTTP | Ops, SIEM, archives |
| Observabilité | Traces et stack traces | Sérialisation d'objets, middleware | Ops, APM, archives |
| Observabilité | Tickets et chat | Copié-collé pour debug | Équipe, archives |
| Observabilité | Dumps partagés | Attachés à un ticket ou envoyés à un éditeur | Support, archives |
| Build | Images Docker | docker history, layers, cache | Utilisateurs du registry |
| Build | Artefacts CI | Logs de build, fichiers temporaires | Équipes CI/CD |
| Exécution | Variables d'environnement | /proc/*/environ, dumps, enfants | Processus liés, root |
| Exécution | Dumps mémoire | Core dumps, heap dumps | Admins, debug |
| Exécution | Métadonnées cloud | Instance metadata (IMDS) | Tout processus sur l'instance |
| Exécution | Fichiers d'auth locaux | .npmrc, kubeconfig, state Terraform | Utilisateurs, CI |
Fuites d'observabilité : logs et traces
Section intitulée « Fuites d'observabilité : logs et traces »C'est la famille la plus sous-estimée, parce que les outils concernés sont ceux censés vous aider. Un log est conçu pour être conservé, indexé et partagé largement : un secret qui y entre acquiert instantanément ces trois propriétés. Pire, la rotation d'un secret ne nettoie pas les archives, l'ancienne valeur reste consultable aussi longtemps que la rétention le permet.
Le problème
Section intitulée « Le problème »Les secrets fuient dans les logs et traces de multiples manières, pas
seulement via un logger.info explicite :
# ❌ Le secret apparaît dans les logslogger.info(f"Connecting to database with password: {db_password}")
# ❌ L'URL contient le mot de passelogger.debug(f"Database URL: {connection_string}")# Affiche: postgresql://user:SuperSecret@db:5432/mydbMais aussi dans des cas moins évidents :
- Traces HTTP : headers
Authorization, query strings avec tokens - Middleware de debug : sérialisation brute de la requête entrante
- Stack traces : variables locales capturées dans l'exception
- Sérialisation automatique : objets de configuration loggés en entier
- Logs de reverse proxies : Nginx/HAProxy qui loggent les headers par défaut
Où les logs finissent
Section intitulée « Où les logs finissent »Une ligne de log ne reste pas sur le serveur qui l'a produite. Elle est collectée, transportée, indexée, puis archivée, souvent sur plusieurs systèmes dont les droits d'accès n'ont rien à voir avec ceux de l'application d'origine. Le schéma ci-dessous suit ce trajet.
Application → stdout → Container runtime → Node logs → Agrégateur (Loki, ELK) ↓ Archivage S3 ↓ BackupsLe secret traverse toute la chaîne et persiste dans les archives.
Solutions
Section intitulée « Solutions »La défense se construit sur trois niveaux, du plus fiable au plus tardif. Ne rien écrire de sensible reste la seule mesure sûre ; le masquage applicatif rattrape les oublis avant l'émission ; le filtrage dans l'agrégateur intervient en dernier recours, une fois la ligne déjà sortie du processus. Empilez les trois, aucun ne suffit seul.
1. Ne jamais logger de secrets :
# ✅ Logger sans le secretlogger.info("Connecting to database")logger.info(f"Database host: {db_host}, port: {db_port}")2. Utiliser des masques dans les frameworks de logging :
# Python avec structlogimport structlog
def mask_secrets(logger, method_name, event_dict): for key in ['password', 'secret', 'token', 'api_key']: if key in event_dict: event_dict[key] = '***REDACTED***' return event_dict
structlog.configure(processors=[mask_secrets, ...])3. Configuration du log aggregator :
# Loki / Promtail : masquer les patternsscrape_configs: - pipeline_stages: - replace: expression: '(password|secret|token)=([^&\s]+)' replace: '${1}=***REDACTED***'Checklist observabilité
Section intitulée « Checklist observabilité »À dérouler lors d'une revue de code ou d'une recette avant mise en production. Les deux derniers points sont les plus souvent oubliés, car ils ne dépendent pas du code applicatif mais de la configuration d'infrastructure.
- Aucun
logger.info/debugavec des secrets - URLs de connexion sans credentials dans les logs
- Masques configurés dans le framework de logging
- Masques configurés dans l'agrégateur de logs
- Pas de sérialisation brute d'objets de configuration ou de requêtes HTTP
- Reverse proxies configurés pour ne pas logger les headers sensibles
Fuites de build : images Docker et artefacts
Section intitulée « Fuites de build : images Docker et artefacts »Une image de conteneur est immuable et distribuable : c'est tout son intérêt, et c'est aussi ce qui rend une fuite irrattrapable. Une fois l'image poussée sur un registre, toute personne autorisée à la télécharger peut en extraire les couches, y compris celles d'une étape intermédiaire. Corriger le Dockerfile ne suffit donc pas, il faut considérer le secret comme compromis et le renouveler.
Ce qui fuit au build
Section intitulée « Ce qui fuit au build »Le risque ne se limite pas à docker history. Plusieurs mécanismes peuvent
capturer un secret lors du build :
- Commande visible dans l'historique : un
ARGouENVavec un secret apparaît dans les métadonnées de l'image - Fichier copié puis supprimé : le fichier reste dans un layer
intermédiaire même après
RUN rm - Secret récupéré par un outil, puis persisté : un
npm ciqui écrit un.npmrcdans un fichier de config final - Cache de build : les caches de layers peuvent conserver des traces
Les layers : le problème fondamental
Section intitulée « Les layers : le problème fondamental »Chaque instruction dans un Dockerfile crée un layer. Même si vous supprimez un fichier, il existe toujours dans les layers précédents.
# ❌ Le secret est dans un layer intermédiaireFROM alpineCOPY .env /app/.envRUN cat /app/.env && ./setup.shRUN rm /app/.env # Le secret est TOUJOURS dans le layer précédentExtraction concrète
Section intitulée « Extraction concrète »Ces deux commandes n'exigent aucun privilège particulier, seulement l'accès à l'image. La première expose les instructions du Dockerfile telles qu'enregistrées dans les métadonnées, la seconde décompresse toutes les couches sur le disque, y compris celles contenant des fichiers ultérieurement « supprimés ».
docker history my-image --no-trunc# Affiche toutes les commandes, y compris les secrets en ARG/ENV
docker save my-image | tar -xf -# Extrait tous les layers, y compris ceux "supprimés"Build secrets : --mount=type=secret
Section intitulée « Build secrets : --mount=type=secret »Docker fournit un mécanisme dédié pour exposer un secret uniquement
pendant une instruction RUN, sans qu'il soit enregistré dans un layer.
# ✅ Le secret n'est jamais dans un layerSHELL ["/bin/bash", "-o", "pipefail", "-c"]RUN --mount=type=secret,id=db_password \ cat /run/secrets/db_password | ./setup.shdocker build --secret id=db_password,src=./db_password.txt .Multi-stage builds
Section intitulée « Multi-stage builds »Le multi-stage build découpe la construction en plusieurs étapes dont une seule produit l'image finale. Les couches des étapes intermédiaires ne sont pas publiées : seuls les fichiers explicitement recopiés avec COPY --from= arrivent dans le résultat. Combiné au montage de secret, c'est la configuration de référence pour installer des dépendances privées.
# ✅ Le secret n'est que dans le stage de buildFROM node:20 AS builderWORKDIR /appCOPY . .RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci
FROM node:20-slim@sha256:2cf067cfed83d5ea958367df9f966191a942351a2df77d6f0193e162b5febfc0COPY --from=builder /app/dist /app/dist# Le .npmrc n'est PAS dans l'image finaleNe jamais utiliser ARG pour les secrets
Section intitulée « Ne jamais utiliser ARG pour les secrets »ARG sert à paramétrer un build, pas à y injecter une valeur confidentielle : sa valeur est conservée dans les métadonnées de l'image et ressort telle quelle avec docker history. Le contournement fréquent qui consiste à écrire la valeur dans un fichier temporaire n'arrange rien, puisque ce fichier crée à son tour une couche.
# ❌ ARG est visible dans docker historyARG DB_PASSWORDRUN echo $DB_PASSWORD > /tmp/setup && ./setup.sh
# ✅ Utiliser --mount=type=secretRUN --mount=type=secret,id=db_password \ cat /run/secrets/db_password | ./setup.shBuild secrets vs runtime secrets
Section intitulée « Build secrets vs runtime secrets »La confusion entre ces deux besoins conduit à des montages inutiles ou dangereux. Un build secret n'existe que le temps d'une instruction RUN et disparaît avec elle ; un runtime secret doit être disponible à chaque démarrage du conteneur, pendant toute sa durée de vie. Aucun mécanisme ne couvre correctement les deux.
Scanner les images pour détecter les secrets
Section intitulée « Scanner les images pour détecter les secrets »La checklist "scanner avec Trivy ou Grype" est bien plus utile avec un exemple concret. Trivy dispose d'un scanner de secrets intégré qui analyse les layers de l'image :
# Scanner une image pour détecter des secrets dans les layerstrivy image --scanners secret my-image:latest
# Scanner un répertoire local (avant le build)trivy fs --scanners secret .Checklist Docker
Section intitulée « Checklist Docker »Ces cinq points se vérifient à la relecture d'un Dockerfile, sauf le dernier qui s'automatise en intégration continue. Le scan reste indispensable même si les quatre premiers sont respectés : il attrape les secrets arrivés par une dépendance ou un outil tiers.
- Multi-stage builds systématiques
-
--mount=type=secretpour les secrets de build - Jamais de
COPY .envouARG SECRET - Build cache nettoyé régulièrement
- Scan des images avec
trivy image --scanners secret
Fuites d'exécution : variables d'environnement
Section intitulée « Fuites d'exécution : variables d'environnement »La variable d'environnement s'est imposée comme le format d'injection par défaut, notamment sous l'influence des douze facteurs. Elle règle un problème de portabilité, pas un problème de sécurité : une fois posée, elle est lisible par tous les processus enfants et par les outils d'introspection du système. La traiter comme une protection est l'erreur de raisonnement la plus répandue sur ce sujet.
Le problème : visibilité élargie et persistance accidentelle
Section intitulée « Le problème : visibilité élargie et persistance accidentelle »Les variables d'environnement circulent facilement entre processus liés,
peuvent réapparaître dans /proc, les outils de debug, les dumps ou les
handlers d'erreur, et sont donc souvent plus exposées qu'on ne le pense.
Concrètement :
- Les processus enfants héritent de l'environnement au moment du
fork/exec /proc/<pid>/environexpose l'environnement initial du processus (les permissions, le namespace et les restrictionsptrace/procfs conditionnent l'accès réel)- Les outils de debug et handlers d'erreur peuvent sérialiser l'environnement complet
- Les dumps de crash capturent la mémoire, y compris les env vars
# Un processus avec les permissions suffisantes peut lire l'env initialcat /proc/1/environ | tr '\0' '\n'# DB_PASSWORD=SuperSecretExposition accidentelle dans les logs et erreurs
Section intitulée « Exposition accidentelle dans les logs et erreurs »Ces deux situations n'ont rien de théorique : la première vient d'un script de diagnostic laissé en place, la seconde d'un gestionnaire d'erreurs configuré pour joindre « tout le contexte » au rapport. Dans les deux cas, la valeur part sur la sortie standard et rejoint la chaîne de collecte des logs.
# ❌ printenv dans un script de debugprintenv# Affiche tous les secrets
# ❌ Error dump qui inclut l'environnementException: Connection failedEnvironment: {'DB_PASSWORD': 'SuperSecret', ...}Solutions
Section intitulée « Solutions »Les trois mesures qui suivent ne sont pas équivalentes. Passer par un fichier change réellement le modèle d'exposition : le contenu n'est plus hérité par les processus enfants et les permissions du système de fichiers s'appliquent. L'injection runtime va plus loin en confiant la distribution à un agent. La troisième, réduire la durée de vie en mémoire, relève de l'hygiène et ne remplace ni l'une ni l'autre.
1. Préférer les fichiers secrets :
# Kubernetes : volume plutôt que envvolumes: - name: db-creds secret: secretName: db-credentials defaultMode: 0400
containers: - name: app volumeMounts: - name: db-creds mountPath: /etc/secrets readOnly: true# Docker Compose : secrets montés en fichiersservices: app: image: my-app:latest secrets: - db_passwordsecrets: db_password: file: ./secrets/db_password.txt# Le secret est monté dans /run/secrets/db_password# Application : lire le fichierwith open('/etc/secrets/db-password') as f: db_password = f.read().strip()2. Injection runtime avec Vault Agent :
template { source = "/etc/vault/templates/db.tpl" destination = "/etc/secrets/db.env" perms = 0400}3. Réduire le temps d'exposition :
import os
db_password = os.environ.pop('DB_PASSWORD', None)connect(password=db_password)db_password = NoneChecklist variables d'environnement
Section intitulée « Checklist variables d'environnement »Les deux premiers points relèvent du déploiement, les trois suivants du code. Cette séparation compte au moment de l'audit : corriger le manifeste de déploiement ne protège pas d'un gestionnaire d'erreurs bavard resté dans l'application.
- Secrets critiques en fichiers plutôt qu'en env vars
- Permissions restrictives sur les fichiers secrets (
0400) - Pas de
printenvouenvdans les scripts - Error handlers qui n'affichent pas l'environnement
- Pas de sérialisation brute des env vars dans les logs d'erreur
Fuites d'exécution : dumps et debug
Section intitulée « Fuites d'exécution : dumps et debug »Un secret finit toujours par exister en clair dans la mémoire du processus, quel que soit son mode de distribution. Tout mécanisme capable d'écrire cette mémoire sur disque devient donc un vecteur de fuite : core dump après un crash, heap dump déclenché pour analyser une consommation, profil de performance exposé sur un port HTTP. Ces artefacts sont d'autant plus dangereux qu'ils circulent, on les joint à un ticket ou on les envoie à un éditeur.
Core dumps
Section intitulée « Core dumps »Un crash peut générer un core dump contenant toute la mémoire du processus, y compris les secrets.
Désactiver la génération de core dumps :
# Au niveau du processus/shellulimit -c 0
# Au niveau système (systemd-coredump)# /etc/systemd/coredump.conf[Coredump]Storage=noneProcessSizeMax=0Restreindre les capacités du conteneur :
# Kubernetes : réduire la surface d'attaquesecurityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"]Heap dumps (Java, Node.js)
Section intitulée « Heap dumps (Java, Node.js) »Contrairement au core dump, le heap dump est produit volontairement, souvent en pleine analyse d'incident et sous pression. Il contient l'intégralité des objets vivants du processus, donc les chaînes de connexion, les jetons et les mots de passe encore référencés.
# ❌ Le heap dump contient tous les secrets en mémoirejmap -dump:format=b,file=heap.hprof <pid>node --heapsnapshot-signal=SIGUSR2 app.jsLe vrai danger n'est pas seulement la génération du dump, mais ce qui en est fait ensuite. Un dump devient souvent un artefact partagé : copié hors de la machine, attaché à un ticket, transmis à un éditeur, stocké dans un bucket ou un espace partagé, et oublié là.
Solutions :
- Désactiver les endpoints de debug en production
- Restreindre l'accès aux outils de dump
- Ne pas persister les heap dumps dans des endroits accessibles
- Traiter les dumps comme des données sensibles (chiffrement, rétention limitée)
Pprof et profiles
Section intitulée « Pprof et profiles »En Go, l'import anonyme de net/http/pprof enregistre automatiquement ses routes de diagnostic sur le serveur HTTP par défaut. Si ce serveur est celui qui expose votre API, les profils deviennent accessibles à quiconque atteint le service, et le point d'entrée /debug/pprof/heap livre un instantané de la mémoire. La parade consiste à servir ces routes sur un écouteur séparé, lié à localhost.
// ❌ Endpoint pprof exposé sans authentificationimport _ "net/http/pprof"
// ✅ Endpoint pprof sur un port interne séparégo func() { http.ListenAndServe("localhost:6060", nil)}()Fuites d'exécution : métadonnées cloud (IMDS)
Section intitulée « Fuites d'exécution : métadonnées cloud (IMDS) »L'IMDS (Instance Metadata Service) est un service HTTP local, joignable à l'adresse 169.254.169.254, qui distribue aux applications les credentials du rôle attaché à l'instance. Sa particularité : il ne demande aucune authentification, la simple capacité à émettre une requête depuis la machine suffit. C'est ce qui en fait la cible privilégiée des failles SSRF, où l'on force une application à effectuer une requête pour le compte de l'attaquant.
Le risque
Section intitulée « Le risque »Sur AWS, GCP, Azure, tout processus sur une instance peut potentiellement récupérer des credentials via le service de métadonnées (IMDS) :
# AWS IMDSv1 : récupérer les credentials du rôle IAM (une seule requête)curl http://169.254.169.254/latest/meta-data/iam/security-credentials/my-roleDéfense en profondeur
Section intitulée « Défense en profondeur »La protection contre les fuites via IMDS repose sur plusieurs couches, pas sur un seul mécanisme.
1. IMDSv2 obligatoire (priorité absolue) :
IMDSv2 exige un token obtenu par une requête PUT préalable, ce qui rend
l'exploitation par SSRF beaucoup plus difficile.
# Configurer l'instance pour n'accepter que IMDSv2aws ec2 modify-instance-metadata-options \ --instance-id i-xxx \ --http-tokens required2. Restrictions réseau en complément :
# Kubernetes NetworkPolicy (complément, pas solution unique)apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: block-metadataspec: podSelector: {} egress: - to: - ipBlock: cidr: 0.0.0.0/0 except: - 169.254.169.254/323. Cloisonnement des workloads :
Restreindre quels pods ou processus ont réellement besoin d'accéder aux métadonnées.
4. Préférer les identités de workload :
Fichiers d'authentification locaux
Section intitulée « Fichiers d'authentification locaux »Une surface souvent oubliée : les fichiers d'authentification présents sur les postes de développement et les runners CI. Ces fichiers contiennent des tokens, des credentials ou des états sensibles :
| Fichier | Contenu sensible |
|---|---|
.npmrc | Token de registry npm privé |
.pypirc | Credentials PyPI |
~/.docker/config.json | Tokens de registries Docker |
~/.kube/config | Certificats et tokens Kubernetes |
terraform.tfstate | State Terraform (peut contenir des passwords, clés) |
~/.aws/credentials | Clés d'accès AWS |
~/.config/gcloud/ | Credentials Google Cloud |
Ce qu'il ne faut pas en conclure
Section intitulée « Ce qu'il ne faut pas en conclure »Ce guide montre que les secrets fuient bien au-delà du code source. Mais attention à ne pas en tirer de fausses conclusions :
- Ne pas mettre un secret dans Git ne suffit pas, il peut fuir par les logs, les images ou l'environnement
- Mettre un secret en variable d'environnement ne le sécurise pas, c'est un mode de distribution, pas une mesure de protection
- Mettre un secret dans un Secret Kubernetes n'en fait pas un coffre-fort, c'est un objet encodé en base64, pas chiffré par défaut
- Supprimer un fichier d'une image finale n'efface pas sa trace du build, les layers intermédiaires persistent
- Désactiver un endpoint de debug ne protège pas le dump déjà généré, les artefacts peuvent avoir été copiés ailleurs
Tableau récapitulatif
Section intitulée « Tableau récapitulatif »Voici les neuf surfaces du guide avec leur contre-mesure principale, à utiliser comme support de revue plutôt que comme lecture linéaire. Une remarque en le parcourant : aucune ligne ne se règle par un outil unique, chacune combine une décision de conception et un réglage d'exploitation.
| Famille | Surface | Risque | Contre-mesure |
|---|---|---|---|
| Observabilité | Logs | Archivage long terme | Masques, ne pas logger de secrets |
| Observabilité | Traces et stack traces | Sérialisation automatique | Filtrage des headers, masquage |
| Build | Docker layers | docker history, extraction | Multi-stage, --mount=type=secret |
| Build | Cache et artefacts | Persistence du secret | Nettoyage de cache, scan Trivy |
| Exécution | Variables env | /proc, héritage, dumps | Fichiers, injection runtime |
| Exécution | Core dumps | Mémoire complète | ulimit -c 0, Storage=none |
| Exécution | Heap dumps | Artefact partagé | Désactiver en prod, rétention |
| Exécution | IMDS | Credentials du nœud | IMDSv2, workload identity |
| Exécution | Fichiers d'auth | Tokens permanents | Permissions, auth éphémère |
À retenir
Section intitulée « À retenir »- Les fuites hors Git se répartissent en trois familles, observabilité, build/artefacts, exécution, chacune avec ses propres contre-mesures
- Distinguez build secrets et runtime secrets,
--mount=type=secretprotège le build, pas l'exécution - Les layers Docker conservent tout, multi-stage et secret mounts sont indispensables
- Les variables d'environnement sont plus exposées qu'on ne le croit, préférez les fichiers avec permissions restrictives
- Les dumps deviennent des artefacts partagés, traitez-les comme des données sensibles
- IMDS se protège en profondeur, IMDSv2, restrictions réseau, cloisonnement, et surtout identités de workload
- Les fichiers d'auth locaux sont une surface oubliée,
.npmrc,kubeconfig, state Terraform peuvent tous fuiter