2026-09-15
Le mandataire qui effaçait l'adresse du client
Sur notre propre plateforme, la limite de tentatives de la page « mot de passe oublié » se déclenchait pour tous les visiteurs à la fois : cinq demandes depuis n'importe où, et la fonction se fermait pour tout le monde. Chaque appel arrivait à l'application avec la même adresse, celle du tunnel entre nos deux machines. Le premier diagnostic, le 18 août 2026, a désigné le mauvais coupable. Le second, le 31 août, a nommé le bon en trois requêtes.
Un diagnostic déduit d'une seule observation
Le 18 août, nous avons lu l'en-tête X-Forwarded-For tel qu'il arrivait au bout de la chaîne : une seule adresse, celle du tunnel. Nous en avons conclu que le serveur de bordure, le premier à recevoir la requête, jetait l'adresse du client, et que la correction lui appartenait. La note a été écrite, la correction reportée. Ce raisonnement était faux, et il a coûté une seconde session de travail.
Un marqueur planté à chaque saut
Le 31 août, nous avons envoyé un en-tête X-Forwarded-For reconnaissable à chaque saut de la chaîne, l'un après l'autre. Adressé directement à l'application, il survit. Adressé au service intermédiaire, il survit. Adressé au Caddy interne, celui qui reçoit le trafic du tunnel, il est jeté et remplacé par l'adresse du pair. Le coupable était nommé en trois commandes.
Le comportement de Caddy, correct et méconnu
Depuis sa version 2.7, Caddy remplace l'en-tête X-Forwarded-For au lieu de le compléter, sauf si le pair immédiat figure dans sa liste de mandataires de confiance. Notre Caddy interne, en version 2.11.4, ne faisait confiance à personne : il effaçait donc la chaîne que le serveur de bordure avait correctement construite. Ce serveur de bordure faisait exactement ce qu'il faut, puisque son pair est bien l'Internet public. La correction tient en une ligne, trusted_proxies static suivi de l'adresse du tunnel, et de rien d'autre.
La preuve, avant et après
Nous avons vérifié avec la mesure qui avait échoué deux semaines plus tôt : depuis notre poste, la limite se déclenche après cinq demandes ; au même moment, une demande depuis une autre adresse publique passe. Le journal des refus nomme désormais l'adresse réelle du client, là où il écrivait auparavant l'adresse du tunnel pour tout le monde. Nous avons ensuite rejoué l'attaque inverse : un client qui envoie son propre X-Forwarded-For à travers le serveur de bordure reste compté sous son adresse réelle, parce que la bordure écrase l'adresse que le client prétend avoir.
La règle que nous en gardons
Le premier diagnostic avait été déduit d'une seule observation, faite au bout de la chaîne. Quand un en-tête disparaît entre plusieurs mandataires, nous plantons désormais un marqueur à chaque saut avant de raisonner : trois requêtes nomment le coupable sans discussion. Un contrôle reste en place : toute requête dont la chaîne d'adresses ne contient aucune adresse publique est inscrite au journal. Sur une plateforme saine, cette ligne n'apparaît jamais, et son absence est la preuve qui continue.
Quand un en-tête disparaît entre plusieurs mandataires, un marqueur planté à chaque saut nomme le coupable en trois requêtes ; un raisonnement mené depuis le bout de la chaîne l'a manqué pendant deux semaines.