Un secret dans les logs : réflexes d'une fiche d'incident
Un jeton écrit en clair par un client HTTP dans les logs d'un conteneur : silence à la source, masquage au handler et rotation du secret.
1. Contexte & Objectif de Terrain : le registre qui recopie le badge
À l’entrée d’une usine, le registre d’accès note qui passe, à quelle heure, avec quel badge. Imaginez que ce registre recopie le numéro du badge en clair, et que n’importe quel visiteur puisse le feuilleter. Le registre fait son travail. Le problème est ce qu’il consigne.
C’est ce qui s’est passé avec un petit bot que je fais tourner sur un serveur. Il vit dans un conteneur Docker et écrit des journaux (des logs) pour que je comprenne ce qu’il fait. L’objectif de la session était simple : qu’aucun secret n’apparaisse dans ces journaux.
Je ne l’avais pas vérifié avant la mise en production. Je l’ai constaté après.
2. Anatomie de l’Échec & Fausses Pistes : l’adresse de chaque requête, jeton compris
Le coupable n’est ni un bug ni une faille. C’est un comportement par défaut.
Le client HTTP du bot (la bibliothèque httpx) écrit l’adresse de chaque requête dans les journaux, au niveau d’importance INFO. Or l’API de Telegram place le jeton du bot dans l’adresse elle-même, sous la forme bot<identifiant>:<jeton>.
Résultat : à chaque interrogation, le jeton partait en clair dans les journaux du conteneur.
Deux faits aggravaient la situation :
- Le constat est venu après la mise en production, pas avant.
- Les anciens journaux (sur le serveur et chez l’ancien hébergeur) contenaient déjà le jeton. Une correction n’efface rien de ce qui a été écrit.
Pourquoi personne ne le voit. Un journal range ses messages par niveau d’importance : du détail technique (DEBUG) à l’erreur grave (ERROR), en passant par l’information courante (INFO) et l’avertissement (WARNING). Par défaut, un programme affiche le niveau INFO et tout ce qui est plus grave. La bibliothèque qui envoie les requêtes juge l’adresse d’une requête utile à l’information courante. Du point de vue du programmeur, c’est raisonnable : il veut savoir ce que son programme contacte.
Rien n’alerte donc personne. Aucune erreur n’est signalée, aucun test ne casse. Le seul moyen de découvrir le problème est de relire le journal en se demandant ce qu’il contient qu’il ne devrait pas contenir. C’est une question d’audit, pas de test.

3. Décryptage Méthodologique par l’Analogie QSE : contenir, corriger, vérifier, traiter l’exposition
Face à un incident, on ne répare pas seulement la cause visible. On suit des temps bien connus d’une fiche d’incident.
Contenir : le silence à la source. On demande au registre de ne plus noter les entrées. Ici, httpx et httpcore passent au niveau WARNING : le message qui contenait l’adresse n’est plus émis.
Corriger durablement : la gommette sur le badge. Même sans la ligne indiscrète, un autre réglage peut un jour la réactiver. Un filtre posé sur le handler (le point d’écriture des journaux) remplace les jetons et les clés avant l’écriture, dans le message, dans les arguments et dans les messages d’erreur.
Vérifier l’efficacité. Une correction se prouve par un comptage, pas par une intention. Après redéploiement, on compte les motifs de jeton dans les journaux du serveur.
Traiter l’exposition passée : changer le badge. Un badge dont le numéro a été lu est un badge compromis. On le change. Pour le jeton, la régénération est laissée à la décision de l’utilisateur, par hygiène : un secret vu dans un journal est réputé compromis.

4. Le Pivot Architectural & L’Implémentation Déterministe : deux couches et trente lignes
Le masquage tient dans un petit module de 30 lignes. Voici le cœur, sans les commentaires de contexte :
_PATTERNS = (
re.compile(r"bot\d+:[A-Za-z0-9_-]+"), # jeton Telegram dans les URL de l'API
re.compile(r"AIza[0-9A-Za-z_-]{35}"), # clé d'API Google
)
_REDACTED = {_PATTERNS[0]: "bot<REDACTED>", _PATTERNS[1]: "<REDACTED_KEY>"}
def redact_secrets(text: str) -> str:
for pattern in _PATTERNS:
text = pattern.sub(_REDACTED[pattern], text)
return text
class RedactSecretsFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
record.msg = redact_secrets(record.getMessage())
record.args = None
if record.exc_info and not record.exc_text:
record.exc_text = logging.Formatter().formatException(record.exc_info)
if record.exc_text:
record.exc_text = redact_secrets(record.exc_text)
return True
Trois détails comptent.
D’abord, le message est assemblé avant masquage (getMessage()), puis les arguments sont supprimés : un argument de formatage ne peut plus réintroduire le secret.
Ensuite, le message d’erreur (la trace d’exécution) est fabriqué une fois, puis masqué : une exception qui contiendrait l’adresse ne contourne pas le filtre.
Enfin, la limite : le filtre ne connaît que deux formes de secrets (un jeton Telegram, une clé Google). Un nouveau type de secret n’est pas masqué tant qu’on ne l’a pas ajouté à la liste.
S’ajoute une rotation des journaux du conteneur : trois fichiers de 10 Mo au plus, pour que le volume des journaux reste borné.
5. Données Chiffrées, Benchmarks & Résultats Mesurés : trois zéros après redéploiement
Après le redéploiement sur le serveur, les journaux du conteneur ont été comptés :
- 0 motif de jeton ;
- 0 clé ;
- 0 ligne
httpxde niveau INFO.
Le module de masquage fait 30 lignes. La rotation des journaux est limitée à 3 fichiers de 10 Mo.
Je n’ai pas de chiffre « avant » : je n’ai pas compté combien de lignes contenaient le jeton, seulement constaté qu’elles étaient présentes à chaque interrogation. Les anciens journaux, eux, ne sont pas corrigés par ce comptage. Le résultat mesuré dit que la fuite est fermée pour l’avenir, pas que le passé est propre.

6. Déploiement Zéro-Terminal (Le Mode d’Emploi pour Marc) : le registre des secrets dans Excel
Aucune console n’est nécessaire pour appliquer ces réflexes à votre propre activité. Un tableur suffit.
Créez un « registre des secrets » avec six colonnes : nom du secret, où il est utilisé, qui y a accès, où il peut fuiter (journaux, e-mails, captures d’écran, tableurs partagés), date de dernière rotation, date de la prochaine.
Exemple de ligne : « mot de passe de la messagerie de l’atelier ; utilisé par le tableau de bord ; deux personnes ; journaux et captures d’écran ; 1er septembre ; 1er décembre ». Le registre ne contient jamais les secrets eux-mêmes, seulement leur description : un registre des secrets qui contient les secrets est un nouveau point de fuite.
Trois règles l’accompagnent :
- Tout secret visible dans un document partagé est changé dans la journée. Un mot de passe collé dans un e-mail ou un tableur est un badge dont le numéro a été lu.
- Un contrôle par trimestre. Ouvrez un fichier partagé ou un journal et utilisez la recherche (Ctrl+F) sur le début de format de vos secrets, par exemple le préfixe d’une clé.
- La date de rotation est une colonne, pas un souvenir. Une colonne vide est un écart à traiter.
7. Les 3 Règles d’Or Actionnables & Conclusion : un secret lu est un secret à changer
- Un secret qui voyage dans une adresse finit dans un journal. Ne comptez pas sur le niveau de journalisation seul : ajoutez un masquage au point d’écriture.
- Un secret vu dans un journal est compromis. Régénérez-le, même après la correction.
- Une correction se prouve par un comptage après coup. Zéro motif, mesuré, vaut mieux qu’une intention de bien faire.
Cette session n’a pas rendu le bot invulnérable. Elle a fermé une fuite précise, prouvé sa fermeture et laissé une décision ouverte : régénérer le jeton. C’est le fonctionnement d’une fiche d’incident qui se clôt vraiment : une cause traitée, une efficacité vérifiée, un reliquat assumé.
Pour une PME, le réflexe à retenir tient en une question à poser à chaque outil que l’on branche : que va-t-il écrire, et où ? Un journal est utile précisément parce qu’il garde tout. C’est aussi pour cela qu’il faut décider, avant la mise en service et non après, ce qu’il n’a pas le droit de garder.