Un WAF qui bloque mon client Go : diagnostiquer une empreinte TLS

· TLS, Go, WAF

le symptome

Mon application interrogeait un site de recettes public. Depuis mon poste comme depuis le serveur, curl obtenait un 200 impeccable. Le même appel depuis mon client Go : HTTP 400, systématiquement, sans corps d'erreur exploitable.

eliminer les suspects un par un

La méthode qui a marché : ne changer qu'une variable à la fois.

IP du serveur ?     curl depuis le VPS → 200. Ce n'est pas l'IP.
User-Agent ?        UA de Chrome copié dans le client Go → 400. Ce n'est pas l'UA.
En-têtes ?          jeu d'en-têtes identique à curl → 400. Ce n'est pas ça non plus.
Le protocole ?      c'est la dernière variable qui reste.

Quand IP, URL et en-têtes sont identiques et que le résultat diffère, la différence est sous HTTP. Restait la poignée de main TLS.

le fingerprinting TLS

Chaque client TLS envoie un ClientHello qui décrit ses capacités : versions supportées, suites de chiffrement, courbes elliptiques, extensions — et surtout leur ordre. Cette signature est stable par implémentation : le ClientHello de Go ne ressemble pas à celui d'OpenSSL, qui ne ressemble pas à celui de Chrome. C'est ce que résume une empreinte de type JA3.

Un WAF comme DataDome n'a donc pas besoin de lire l'en-tête User-Agent : il constate qu'un client prétendant être Chrome présente le ClientHello de la bibliothèque standard de Go, et bloque. Aucun en-tête ne peut corriger ça, puisque le verdict tombe avant le premier octet de HTTP.

la tentative de contournement

Il existe une réponse technique : uTLS, un fork de crypto/tls qui permet de reproduire le ClientHello d'un navigateur. Le déroulé :

  1. ClientHello « Chrome » via uTLS → plus de 400, mais un EOF.
  2. Cause : le profil Chrome annonce h2 dans l'extension ALPN, alors que mon transport ne parlait que HTTP/1.1. Le serveur négociait HTTP/2 et je lui répondais en HTTP/1.1.
  3. ALPN forcé à http/1.1 → retour du 400 : en modifiant l'extension, j'avais changé l'empreinte, qui ne correspondait plus à un vrai Chrome.
  4. Étape suivante : monter un http2.Transport par-dessus la connexion uTLS.

pourquoi j'ai arrete la

J'ai arrêté avant cette dernière étape, et c'est la partie de l'histoire qui compte le plus.

Techniquement, la course était gagnable — pour quelques semaines. Un profil uTLS se périme à chaque version de navigateur ; le WAF ajoute des signaux ; la maintenance devient permanente. Surtout, la question de fond n'était pas technique : un site qui déploie un WAF exprime une intention. Contourner activement une protection pour récupérer du contenu, c'est franchir une ligne que je ne veux pas franchir dans un projet personnel.

J'ai supprimé le code, retiré la dépendance uTLS, et remplacé la fonctionnalité par une base de recettes locale plus fournie, complétée par un import de données structurées schema.org/Recipe — que les sites publient volontairement pour les moteurs de recherche.

ce que je retiens