L'IP di origine dietro Cloudflare trapela dal sottodominio deploy — tre repo, quattordici secondi, sistemati in una passata
Quel pomeriggio fissai GitHub per un attimo. Tre repo avevano chiuso PR alle 16:20:31, 16:20:38 e 16:20:45 — quattordici secondi di distanza.
smarts #38: deploy.smarts.md → deploy.smartshow2claude #13: deploy.how2claude.com → deploy.how2claudepickful #118: deploy.pickful.ai / deploy.pickful.xyz → deploy.pickful / deploy.pickful-alphaTutti e tre sistemavano la stessa classe di bug. Stavo parlando con Claude di un problemino di deploy non correlato in how2claude. Di passaggio, ha dato un'occhiata a config/deploy.yml e mi ha detto che l'IP di origine di quella macchina era esposto in piena vista nel DNS pubblico.
Tutti i miei progetti stanno dietro Cloudflare. La nuvoletta arancione nella dashboard di Cloudflare significa "record proxato" — le richieste HTTP atterrano prima su un nodo edge di Cloudflare, poi vanno al mio origin. L'IP di origine non appare mai nelle risposte DNS; quello che il DNS restituisce è un IP anycast di Cloudflare. Quella parte era a posto.
Ma faccio deploy con Kamal. Kamal fa SSH al server per girare docker. SSH non può passare attraverso un proxy HTTP, quindi mi serviva un hostname non proxato da Cloudflare per fare SSH. La mia configurazione era:
# config/deploy.yml
servers:
web:
- deploy.how2claude.com # ← record A diretto all'IP di origine, nuvola grigia (DNS only)
Su Cloudflare, how2claude.com e www.how2claude.com erano arancioni. deploy.how2claude.com era grigio. Doveva essere grigio, altrimenti SSH non poteva raggiungere la macchina.
Grigio significa DNS pubblico. Chiunque può eseguire:
$ dig deploy.how2claude.com +short
<il mio IP di origine>
E la convenzione di nomi deploy.<dominio> è di per sé un segnale chiaro — scansiona la sottosezione deploy.* su una lista di domini SaaS comuni e raccogli un mucchio di IP di origine che dovrebbero stare nascosti dietro Cloudflare.
WAF di Cloudflare bypassato, rate limiting bypassato, protezione DDoS bypassata. A un dig di distanza.
Stavamo parlando d'altro. Gli avevo chiesto di rivedere un documento di deploy che avevo appena scritto; ha aperto config/deploy.yml per fare il confronto, è arrivato alla riga 7 — - deploy.how2claude.com — ha fatto una pausa e ha detto più o meno questo:
Quel hostname si risolve via DNS pubblico, no? Significa che chiunque faccia una query DNS prende quell'IP di origine, e il proxy edge di Cloudflare viene aggirato.
Mi sono bloccato per un attimo. Questo avrei dovuto vederlo — sarebbe bastato guardare due volte l'icona della nuvola grigia mentre configuravo Cloudflare e chiedermi cosa significasse — ma non l'ho fatto. Trattavo quel hostname come qualcosa di "interno", solo perché il prefisso era deploy.. Il mio cervello gli concedeva in silenzio una privacy che non aveva mai avuto.
Claude non condivide quel bias. Legge una stringa in un campo yaml e fa la domanda meccanica: come si trasforma questa stringa in un IP? Risposta: DNS pubblico. Conclusione: l'IP di origine è pubblico.
/etc/hostsLa fix si è rivelata imbarazzantemente semplice. Kamal chiede al client SSH locale di risolvere l'hostname, quindi l'hostname deve risolversi solo sulla mia macchina — non su tutta internet.
Accorcia l'hostname nello yaml:
servers:
web:
- deploy.how2claude # ← attenzione: senza .com
Poi aggiungi una riga al mio /etc/hosts:
198.51.100.42 deploy.how2claude
I runner di CI che fanno deploy hanno bisogno della stessa riga (env var, o direttamente echo >> /etc/hosts nel workflow).
Risultato:
deploy.how2claude.* esiste nel DNS pubblico, da nessuna partekamal deploy / app exec / app logs continuano a funzionare, perché /etc/hosts viene consultato prima del DNSL'accessory del database vuole lo stesso cambio:
accessories:
db:
image: postgres:17
host: deploy.how2claude # ← stesso hostname
Questa è la parte che volevo mettere giù.
Una volta spedita la fix di how2claude, stavo per cambiare contesto. Claude mi ha fermato: "Usi Kamal anche su smarts e pickful, vero? Fammi controllare quei due."
L'ha fatto.
deploy.smarts.md — stesso problema, record A pubblicodeploy.pickful.ai (production), deploy.pickful.xyz (alpha), più un deploy-test1.pickful.ai morto da tempo e un deploy.staging.yml che puntava a un dominio in pensione, blockgeek.comPickful era il più ingarbugliato dei tre perché aveva più destinations (production / alpha / test2) e zavorra storica. Di passaggio Claude ha ripulito le destinations morte staging e test1 — non avevano più ragione di esistere, solo rumore.
Commit finali:
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)
Quattordici secondi. Certo, è solo GitHub che svuota una coda di merge — il lavoro vero era distribuito nelle due ore precedenti. Ma la forma effettiva era: una scoperta, tre repo sistemati.
Se avessi lavorato da solo, la versione realistica sarebbe stata: aggiusto in how2claude, scrivo un TODO "fare anche smarts e pickful", e guardo quel TODO restare nella lista per tre mesi. Ho vissuto esattamente quella sequenza molte volte.
Se fai deploy con Kamal, Capistrano o qualunque tool di deploy basato su SSH:
config/deploy*.yml e fa' il grep di host: e servers:. Elenca ogni hostname che compare.dig +short su ognuno. Tutto ciò che restituisce un IP di origine invece di niente sta perdendo.deploy.<project>) e metti l'IP in /etc/hosts.echo "$IP deploy.<project>" | sudo tee -a /etc/hosts.Se il tuo team gestisce più progetti che condividono lo stesso pattern infrastrutturale, scrivi un grep di una riga che spazzoli tutti i deploy*.yml di tutti i repo, è più affidabile di controllarli uno alla volta.
Cloudflare non ha fallito. Kamal non ha fallito. Entrambi gli strumenti facevano esattamente quello che dovevano fare.
La lezione è: i valori di default ti ingannano in silenzio. deploy.<tuo-dominio>.com suona come un nome interno, ma il DNS non ha il concetto di "interno" — un record A è un record A, e una volta pubblicato è pubblico. Un nome può darti l'illusione della privacy, e quell'iconcina di nuvola grigia nella dashboard di Cloudflare è solo la versione visiva della stessa illusione: se ne sta lì in silenzio, senza alert in rosso, ma in realtà ti sta dicendo "questo record non passa per me".
Fai leggere a Claude la tua config di deploy una volta. Non condivide l'illusione.