Free

Laissez Claude effacer l'IP d'origine de votre DNS public

L'IP d'origine derrière Cloudflare fuit par le sous-domaine deploy — trois dépôts, quatorze secondes, corrigés en une passe


Cet après-midi-là, j'ai fixé GitHub un moment. Trois dépôts ont fermé des PR à 16:20:31, 16:20:38 et 16:20:45 — quatorze secondes d'écart.

  • smarts #38 : deploy.smarts.mddeploy.smarts
  • how2claude #13 : deploy.how2claude.comdeploy.how2claude
  • pickful #118 : deploy.pickful.ai / deploy.pickful.xyzdeploy.pickful / deploy.pickful-alpha

Les trois corrections appartenaient à la même classe de bug. Je discutais avec Claude d'un détail de déploiement sans rapport sur how2claude. En passant, il a jeté un œil à config/deploy.yml et m'a dit que l'IP d'origine de cette machine était à découvert dans le DNS public.

Une illusion qui m'avait dupé pendant des mois

Tous mes projets sont derrière Cloudflare. Le petit nuage orange dans le tableau de bord Cloudflare signifie « enregistrement proxifié » — les requêtes HTTP atterrissent d'abord sur un nœud edge Cloudflare puis vers mon origine. L'IP d'origine n'apparaît jamais dans les réponses DNS ; ce que renvoie le DNS est une IP anycast Cloudflare. Cette partie était bien faite.

Mais je déploie avec Kamal. Kamal fait du SSH sur le serveur pour lancer docker. SSH ne peut pas passer par un proxy HTTP, donc il me fallait un hostname non proxifié par Cloudflare pour le SSH. Ma configuration était :

# config/deploy.yml
servers:
  web:
    - deploy.how2claude.com   # ← enregistrement A direct vers l'IP d'origine, nuage gris (DNS only)

Dans Cloudflare, how2claude.com et www.how2claude.com étaient orange. deploy.how2claude.com était gris. Il devait être gris, sinon SSH ne pouvait pas atteindre la machine.

Gris signifie DNS public. N'importe qui peut lancer :

$ dig deploy.how2claude.com +short
<mon IP d'origine>

Et la convention de nommage deploy.<domaine> est en elle-même une indication — scannez le sous-domaine deploy.* sur une liste de domaines SaaS courants et vous récolterez des IP d'origine censées se cacher derrière Cloudflare.

WAF Cloudflare contourné, rate limiting contourné, protection DDoS contournée. À un dig près.

Pourquoi Claude l'a remarqué

Nous parlions d'autre chose. Je lui ai demandé de relire un document de déploiement que je venais d'écrire ; il a ouvert config/deploy.yml pour recouper, est arrivé ligne 7 — - deploy.how2claude.com — a marqué une pause, puis a dit à peu près ceci :

Ce hostname se résout via le DNS public, n'est-ce pas ? Donc n'importe qui qui exécute une requête DNS récupère cette IP d'origine, et le proxy edge de Cloudflare est contourné.

Je suis resté figé une seconde. J'aurais dû voir ça — il aurait suffi de regarder une seconde fois l'icône de nuage gris en configurant Cloudflare et de me demander ce qu'elle signifiait — mais je ne l'ai pas fait. Je traitais ce hostname comme quelque chose d'« interne », simplement parce que le préfixe était deploy.. Mon cerveau lui accordait silencieusement une intimité qui n'a jamais existé.

Claude ne partage pas ce biais. Il lit une chaîne dans un champ yaml et pose la question mécanique : comment cette chaîne devient-elle une IP ? Réponse : DNS public. Conclusion : l'IP d'origine est publique.

Le correctif : un alias dans /etc/hosts

Le correctif s'est révélé d'une simplicité gênante. Kamal demande au client SSH local de résoudre le hostname, donc le hostname n'a besoin de se résoudre que sur ma machine — pas sur tout l'internet.

Raccourcir le hostname dans le yaml :

servers:
  web:
    - deploy.how2claude    # ← attention : pas de .com

Puis ajouter une ligne dans mon /etc/hosts :

198.51.100.42  deploy.how2claude

Les runners CI qui déploient ont besoin de la même ligne (variable d'environnement, ou directement echo >> /etc/hosts dans le workflow).

Résultat :

  • Aucun enregistrement deploy.how2claude.* n'existe dans le DNS public, nulle part
  • Les attaquants ne peuvent pas extraire l'IP via DNS — au moins cette voie est fermée
  • Tous les kamal deploy / app exec / app logs continuent de fonctionner, parce que /etc/hosts est consulté avant le DNS
  • Cloudflare devient plus propre : ce sous-domaine de nuage gris embarrassant disparaît tout simplement

L'accessory de la base de données a besoin du même changement :

accessories:
  db:
    image: postgres:17
    host: deploy.how2claude   # ← même hostname

Claude a porté ça sur les trois dépôts

C'est la partie que je voulais consigner.

Une fois le correctif how2claude livré, j'allais changer de contexte. Claude m'a arrêté : « Tu utilises aussi Kamal sur smarts et pickful, non ? Laisse-moi vérifier ces deux-là. »

Il l'a fait.

  • smarts : deploy.smarts.md — même problème, enregistrement A public
  • pickful : deploy.pickful.ai (production), deploy.pickful.xyz (alpha), plus un deploy-test1.pickful.ai mort depuis longtemps et un deploy.staging.yml qui pointait vers un domaine retiré, blockgeek.com

Pickful était le plus encombré des trois parce qu'il avait plusieurs destinations (production / alpha / test2) et un poids historique. Claude a balayé au passage les destinations staging et test1 mortes — elles n'avaient plus aucune raison d'être, juste du bruit.

Commits finaux :

smarts       6482472   2026-04-27 16:20:31  Use private deploy.smarts alias instead of public deploy.smarts.md (#38)
how2claude   ccc0344   2026-04-27 16:20:38  Use private deploy.how2claude alias instead of public deploy.how2claude.com (#13)
pickful      e6bf9af   2026-04-27 16:20:45  Deploy config hygiene + privatize hostnames (#118)

Quatorze secondes. Bien sûr, c'est juste GitHub qui vide une file de merge — le travail réel s'est étalé sur les deux heures précédentes. Mais la forme effective était : un constat, trois dépôts corrigés.

Si j'avais travaillé seul, la version réaliste aurait été : corriger sur how2claude, écrire un TODO « faire smarts et pickful aussi », et regarder ce TODO traîner dans la liste pendant trois mois. J'ai vécu cette séquence exacte de nombreuses fois.

Une checklist à recopier

Si vous déployez avec Kamal, Capistrano ou n'importe quel outil de déploiement basé sur SSH :

  1. Ouvrez config/deploy*.yml et grep sur host: et servers:. Listez chaque hostname qui apparaît.
  2. Lancez dig +short sur chacun. Tout ce qui renvoie une IP d'origine plutôt que rien fuit.
  3. Parcourez le tableau de bord de votre CDN et repérez les sous-domaines en « nuage gris » — c'est l'ensemble candidat.
  4. Renommez-les en alias sans TLD (deploy.<project>) et mettez l'IP dans /etc/hosts.
  5. Mettez aussi à jour le job de deploy en CI. Sur GitHub Actions : echo "$IP deploy.<project>" | sudo tee -a /etc/hosts.
  6. Supprimez l'enregistrement A public original.

Si votre équipe gère plusieurs projets partageant le même schéma d'infrastructure, écrivez un grep d'une ligne qui balaie tous les deploy*.yml de tous les dépôts ; c'est plus fiable que de les vérifier un par un.

La vraie leçon

Cloudflare n'a pas échoué. Kamal n'a pas échoué. Les deux outils faisaient exactement ce qu'ils devaient faire.

La leçon est : les valeurs par défaut vous induisent en erreur en silence. deploy.<votre-domaine>.com sonne comme un nom interne, mais le DNS n'a pas la notion d'« interne » — un enregistrement A est un enregistrement A, et dès qu'il est publié, il est public. Un nom peut vous donner l'illusion d'intimité, et cette petite icône de nuage gris dans le tableau de bord Cloudflare n'est que la version visuelle de la même illusion : elle reste là tranquillement, sans avertissement rouge, mais ce qu'elle dit en réalité, c'est « cet enregistrement ne passe pas par moi ».

Faites lire votre config de deploy à Claude une fois. Il ne partage pas l'illusion.