Die Origin-IP hinter Cloudflare leakt über die deploy-Subdomain — drei Repos, vierzehn Sekunden, in einem Rutsch gefixt
An jenem Nachmittag starrte ich kurz auf GitHub. Drei Repos schlossen PRs um 16:20:31, 16:20:38 und 16:20:45 — vierzehn Sekunden auseinander.
smarts #38: deploy.smarts.md → deploy.smartshow2claude #13: deploy.how2claude.com → deploy.how2claudepickful #118: deploy.pickful.ai / deploy.pickful.xyz → deploy.pickful / deploy.pickful-alphaAlle drei Fixes betrafen dieselbe Bug-Klasse. Ursprünglich hatte ich mit Claude über ein nicht zusammenhängendes Deployment-Detail in how2claude gesprochen. Im Vorbeigehen warf es einen Blick auf config/deploy.yml und sagte mir, die Origin-IP dieser Maschine liege im öffentlichen DNS offen sichtbar.
Alle meine Projekte sitzen hinter Cloudflare. Die kleine orangefarbene Wolke im Cloudflare-Dashboard bedeutet „Eintrag wird proxied" — HTTP-Requests landen zuerst auf einem Cloudflare-Edge-Knoten und gehen von dort an meinen Origin. Die Origin-IP taucht nie in DNS-Antworten auf; was DNS zurückliefert, ist eine Cloudflare-Anycast-IP. Dieser Teil war richtig.
Aber ich deploye mit Kamal. Kamal SSHt in den Server, um docker zu starten. SSH kann nicht durch einen HTTP-Proxy, also brauchte ich einen Hostnamen, der nicht von Cloudflare proxied wird, um darauf SSH zu machen. Mein Setup war:
# config/deploy.yml
servers:
web:
- deploy.how2claude.com # ← A-Record direkt zur Origin-IP, graue Wolke (DNS only)
In Cloudflare waren how2claude.com und www.how2claude.com orange. deploy.how2claude.com war grau. Es musste grau sein, sonst hätte SSH die Maschine nicht erreicht.
Grau bedeutet öffentliches DNS. Jeder kann ausführen:
$ dig deploy.how2claude.com +short
<meine Origin-IP>
Und die Namenskonvention deploy.<domain> ist an sich schon ein deutliches Signal — scanne die deploy.*-Subdomain über eine Liste gängiger SaaS-Domains, und du erntest Origin-IPs, die eigentlich hinter Cloudflare verborgen sein sollten.
Cloudflare-WAF umgangen, Rate-Limiting umgangen, DDoS-Schutz umgangen. Ein dig weit weg.
Wir sprachen eigentlich über etwas anderes. Ich bat es, ein gerade geschriebenes Deployment-Dokument zu prüfen; es öffnete config/deploy.yml zum Abgleich, kam zu Zeile 7 — - deploy.how2claude.com — hielt kurz inne und sagte sinngemäß:
Dieser Hostname wird über öffentliches DNS aufgelöst, oder? Das heißt, wer eine DNS-Abfrage macht, bekommt diese Origin-IP, und der Cloudflare-Edge-Proxy ist umgangen.
Ich saß eine Sekunde wie erstarrt da. Das hätte ich sehen müssen — es hätte gereicht, beim Konfigurieren von Cloudflare das graue Wolkensymbol noch einmal anzusehen und zu fragen, was es eigentlich bedeutet — aber ich tat es nicht. Ich behandelte den Hostnamen als irgendwie „intern", einzig weil das Präfix deploy. lautete. Mein Gehirn sprach ihm leise eine Privatheit zu, die er nie hatte.
Claude teilt diese Voreingenommenheit nicht. Es liest einen String in einem yaml-Feld und stellt die mechanische Frage: Wie wird dieser String zu einer IP? Antwort: öffentliches DNS. Schlussfolgerung: Origin-IP ist öffentlich.
/etc/hosts-AliasDer Fix erwies sich als peinlich einfach. Kamal lässt den lokalen SSH-Client den Hostnamen auflösen, also muss der Hostname nur auf meiner Maschine auflösbar sein — nicht im gesamten Internet.
Den Hostnamen in der yaml verkürzen:
servers:
web:
- deploy.how2claude # ← Achtung: kein .com
Dann eine Zeile in mein /etc/hosts:
198.51.100.42 deploy.how2claude
CI-Runner, die deployen, brauchen dieselbe Zeile (Env-Variable, oder direkt echo >> /etc/hosts im Workflow).
Ergebnis:
deploy.how2claude.*-Eintragkamal deploy / app exec / app logs-Befehle laufen weiter, weil /etc/hosts vor DNS konsultiert wirdDer Datenbank-Accessory braucht dieselbe Änderung:
accessories:
db:
image: postgres:17
host: deploy.how2claude # ← derselbe Hostname
Das ist der Teil, den ich aufschreiben wollte.
Nachdem wir den how2claude-Fix ausgeliefert hatten, wollte ich den Kontext wechseln. Claude hielt mich an: „Du benutzt Kamal auch für smarts und pickful, oder? Lass mich die mal anschauen."
Es hat sie angeschaut.
deploy.smarts.md — gleiches Problem, öffentlicher A-Recorddeploy.pickful.ai (production), deploy.pickful.xyz (alpha), dazu ein längst totes deploy-test1.pickful.ai und ein deploy.staging.yml, das auf eine ausgemusterte blockgeek.com-Domain verwiesPickful war von den dreien das verworrenste, weil es mehrere Destinations (production / alpha / test2) und historischen Ballast hatte. Claude räumte nebenbei die toten staging- und test1-Destinations auf — sie hatten keine Daseinsberechtigung mehr, nur Lärm.
Finale Commits:
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)
Vierzehn Sekunden. Klar, das ist nur GitHub, das eine Merge-Queue leert — die echte Arbeit verteilte sich auf die zwei Stunden davor. Aber die effektive Form war: ein Befund, drei Repos behoben.
Hätte ich allein gearbeitet, wäre die realistische Variante: in how2claude fixen, ein TODO mit „smarts und pickful auch" notieren und zusehen, wie dieses TODO drei Monate in der Liste schlummert. Ich habe genau diese Abfolge schon viele Male erlebt.
Wenn du mit Kamal, Capistrano oder einem beliebigen SSH-basierten Deploy-Tool deployst:
config/deploy*.yml und grep auf host: und servers:. Liste jeden Hostnamen, der vorkommt.dig +short auf jeden los. Was eine Origin-IP statt nichts zurückgibt, leakt.deploy.<project>) um und stecke die IP in /etc/hosts.echo "$IP deploy.<project>" | sudo tee -a /etc/hosts.Wenn dein Team mehrere Projekte mit demselben Infra-Muster betreibt, schreib lieber ein einzeiliges grep, das alle deploy*.yml aller Repos durchwühlt, statt sie eins nach dem anderen zu prüfen.
Cloudflare hat nicht versagt. Kamal hat nicht versagt. Beide Tools taten genau, was sie tun sollten.
Die Lektion lautet: Defaults führen dich leise in die Irre. deploy.<deine-domain>.com klingt nach einem internen Namen, aber DNS kennt das Konzept „intern" nicht — ein A-Record ist ein A-Record, und sobald er veröffentlicht ist, ist er öffentlich. Ein Name kann dir die Illusion von Privatheit geben, und jenes kleine graue Wolkensymbol im Cloudflare-Dashboard ist nur die visuelle Form derselben Illusion: Es sitzt da still, ohne rote Warnung, sagt aber in Wahrheit: „Dieser Eintrag läuft nicht über mich."
Lass Claude einmal in deine Deploy-Konfig schauen. Es teilt die Illusion nicht.