“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 »| 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 »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 »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 »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é »- 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 »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 »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 layerRUN --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 secret n'est que dans le stage de buildFROM node:20 AS builderCOPY . .RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci
FROM node:20-slimCOPY --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 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 »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 »- 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 »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 »# ❌ 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 »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 »- 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 »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) »# ❌ 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 »// ❌ 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) »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 »| 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