Aller au contenu
Sécurité medium

Fuites de secrets : logs, Docker, variables

20 min de lecture

“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.

  • 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

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 :

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.

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.

Tout ce qui est accessible au runtime peut exposer un secret : variables d’environnement, /proc, métadonnées cloud (IMDS), mémoire du processus.

FamilleSurfaceComment le secret fuitQui y a accès
ObservabilitéLogs applicatifsAffiché en debug, traces HTTPOps, SIEM, archives
ObservabilitéTraces et stack tracesSérialisation d’objets, middlewareOps, APM, archives
ObservabilitéTickets et chatCopié-collé pour debugÉquipe, archives
ObservabilitéDumps partagésAttachés à un ticket ou envoyés à un éditeurSupport, archives
BuildImages Dockerdocker history, layers, cacheUtilisateurs du registry
BuildArtefacts CILogs de build, fichiers temporairesÉquipes CI/CD
ExécutionVariables d’environnement/proc/*/environ, dumps, enfantsProcessus liés, root
ExécutionDumps mémoireCore dumps, heap dumpsAdmins, debug
ExécutionMétadonnées cloudInstance metadata (IMDS)Tout processus sur l’instance
ExécutionFichiers d’auth locaux.npmrc, kubeconfig, state TerraformUtilisateurs, CI

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 logs
logger.info(f"Connecting to database with password: {db_password}")
# ❌ L'URL contient le mot de passe
logger.debug(f"Database URL: {connection_string}")
# Affiche: postgresql://user:SuperSecret@db:5432/mydb

Mais 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
Application → stdout → Container runtime → Node logs → Agrégateur (Loki, ELK)
Archivage S3
Backups

Le secret traverse toute la chaîne et persiste dans les archives.

1. Ne jamais logger de secrets :

# ✅ Logger sans le secret
logger.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 structlog
import 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 patterns
scrape_configs:
- pipeline_stages:
- replace:
expression: '(password|secret|token)=([^&\s]+)'
replace: '${1}=***REDACTED***'
  • Aucun logger.info/debug avec 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

Le risque ne se limite pas à docker history. Plusieurs mécanismes peuvent capturer un secret lors du build :

  • Commande visible dans l’historique : un ARG ou ENV avec 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 ci qui écrit un .npmrc dans un fichier de config final
  • Cache de build : les caches de layers peuvent conserver des traces

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édiaire
FROM alpine
COPY .env /app/.env
RUN cat /app/.env && ./setup.sh
RUN rm /app/.env # Le secret est TOUJOURS dans le layer précédent
Fenêtre de terminal
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"

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 layer
RUN --mount=type=secret,id=db_password \
cat /run/secrets/db_password | ./setup.sh
Fenêtre de terminal
docker build --secret id=db_password,src=./db_password.txt .
# ✅ Le secret n'est que dans le stage de build
FROM node:20 AS builder
COPY . .
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
FROM node:20-slim
COPY --from=builder /app/dist /app/dist
# Le .npmrc n'est PAS dans l'image finale
# ❌ ARG est visible dans docker history
ARG DB_PASSWORD
RUN echo $DB_PASSWORD > /tmp/setup && ./setup.sh
# ✅ Utiliser --mount=type=secret
RUN --mount=type=secret,id=db_password \
cat /run/secrets/db_password | ./setup.sh

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 :

Fenêtre de terminal
# Scanner une image pour détecter des secrets dans les layers
trivy image --scanners secret my-image:latest
# Scanner un répertoire local (avant le build)
trivy fs --scanners secret .
  • Multi-stage builds systématiques
  • --mount=type=secret pour les secrets de build
  • Jamais de COPY .env ou ARG 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>/environ expose l’environnement initial du processus (les permissions, le namespace et les restrictions ptrace/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
Fenêtre de terminal
# Un processus avec les permissions suffisantes peut lire l'env initial
cat /proc/1/environ | tr '\0' '\n'
# DB_PASSWORD=SuperSecret
Fenêtre de terminal
# ❌ printenv dans un script de debug
printenv
# Affiche tous les secrets
# ❌ Error dump qui inclut l'environnement
Exception: Connection failed
Environment: {'DB_PASSWORD': 'SuperSecret', ...}

1. Préférer les fichiers secrets :

# Kubernetes : volume plutôt que env
volumes:
- 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 fichiers
services:
app:
image: my-app:latest
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
# Le secret est monté dans /run/secrets/db_password
# Application : lire le fichier
with 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 = None
  • Secrets critiques en fichiers plutôt qu’en env vars
  • Permissions restrictives sur les fichiers secrets (0400)
  • Pas de printenv ou env dans les scripts
  • Error handlers qui n’affichent pas l’environnement
  • Pas de sérialisation brute des env vars dans les logs d’erreur

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 :

Fenêtre de terminal
# Au niveau du processus/shell
ulimit -c 0
# Au niveau système (systemd-coredump)
# /etc/systemd/coredump.conf
[Coredump]
Storage=none
ProcessSizeMax=0

Restreindre les capacités du conteneur :

# Kubernetes : réduire la surface d'attaque
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
Fenêtre de terminal
# ❌ Le heap dump contient tous les secrets en mémoire
jmap -dump:format=b,file=heap.hprof <pid>
node --heapsnapshot-signal=SIGUSR2 app.js

Le 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)
// ❌ Endpoint pprof exposé sans authentification
import _ "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) »

Sur AWS, GCP, Azure, tout processus sur une instance peut potentiellement récupérer des credentials via le service de métadonnées (IMDS) :

Fenêtre de terminal
# 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-role

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.

Fenêtre de terminal
# Configurer l'instance pour n'accepter que IMDSv2
aws ec2 modify-instance-metadata-options \
--instance-id i-xxx \
--http-tokens required

2. Restrictions réseau en complément :

# Kubernetes NetworkPolicy (complément, pas solution unique)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-metadata
spec:
podSelector: {}
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32

3. 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 :

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 :

FichierContenu sensible
.npmrcToken de registry npm privé
.pypircCredentials PyPI
~/.docker/config.jsonTokens de registries Docker
~/.kube/configCertificats et tokens Kubernetes
terraform.tfstateState Terraform (peut contenir des passwords, clés)
~/.aws/credentialsClés d’accès AWS
~/.config/gcloud/Credentials Google Cloud

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
FamilleSurfaceRisqueContre-mesure
ObservabilitéLogsArchivage long termeMasques, ne pas logger de secrets
ObservabilitéTraces et stack tracesSérialisation automatiqueFiltrage des headers, masquage
BuildDocker layersdocker history, extractionMulti-stage, --mount=type=secret
BuildCache et artefactsPersistence du secretNettoyage de cache, scan Trivy
ExécutionVariables env/proc, héritage, dumpsFichiers, injection runtime
ExécutionCore dumpsMémoire complèteulimit -c 0, Storage=none
ExécutionHeap dumpsArtefact partagéDésactiver en prod, rétention
ExécutionIMDSCredentials du nœudIMDSv2, workload identity
ExécutionFichiers d’authTokens permanentsPermissions, auth éphémère
  1. Les fuites hors Git se répartissent en trois familles — observabilité, build/artefacts, exécution — chacune avec ses propres contre-mesures
  2. Distinguez build secrets et runtime secrets--mount=type=secret protège le build, pas l’exécution
  3. Les layers Docker conservent tout — multi-stage et secret mounts sont indispensables
  4. Les variables d’environnement sont plus exposées qu’on ne le croit — préférez les fichiers avec permissions restrictives
  5. Les dumps deviennent des artefacts partagés — traitez-les comme des données sensibles
  6. IMDS se protège en profondeur — IMDSv2, restrictions réseau, cloisonnement, et surtout identités de workload
  7. Les fichiers d’auth locaux sont une surface oubliée.npmrc, kubeconfig, state Terraform peuvent tous fuiter

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn