Free

Pozwól Claude'owi usunąć IP serwera źródłowego z publicznego DNS-a

IP serwera źródłowego za Cloudflare'em wycieka przez subdomenę deploy — trzy repo, czternaście sekund, naprawione za jednym zamachem


Tamtego popołudnia chwilę wpatrywałem się w GitHuba. Trzy repozytoria pozamykały PR-y o 16:20:31, 16:20:38 i 16:20:45 — w odstępie czternastu sekund.

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

Wszystkie trzy poprawki dotyczyły tej samej klasy buga. Pierwotnie rozmawiałem z Claude'em o nieistotnym drobiazgu deploya w how2claude. Mimochodem rzucił okiem na config/deploy.yml i powiedział mi, że IP serwera źródłowego tej maszyny stoi na widoku w publicznym DNS-ie.

Iluzja, która zwodziła mnie miesiącami

Wszystkie moje projekty siedzą za Cloudflare'em. Mała pomarańczowa chmurka w panelu Cloudflare oznacza „rekord proxiowany" — żądania HTTP najpierw lądują na węźle edge Cloudflare, a stamtąd idą do mojego origin-a. IP origin nigdy nie pojawia się w odpowiedziach DNS; DNS zwraca anycast IP Cloudflare. Ta część była zrobiona dobrze.

Ale deployuję Kamalem. Kamal robi SSH na serwer, żeby uruchomić docker. SSH nie przejdzie przez HTTP-proxy, więc potrzebowałem hostname'u nieproxiowanego przez Cloudflare, żeby się przez SSH dostać. Moja konfiguracja wyglądała tak:

# config/deploy.yml
servers:
  web:
    - deploy.how2claude.com   # ← rekord A wprost do IP origin, szara chmurka (DNS only)

W Cloudflare how2claude.com i www.how2claude.com były pomarańczowe. deploy.how2claude.com był szary. Musiał być szary, w przeciwnym razie SSH nie dotarłoby do maszyny.

Szary znaczy publiczny DNS. Każdy może wykonać:

$ dig deploy.how2claude.com +short
<moje IP origin>

A sama konwencja nazewnictwa deploy.<domena> jest już sama w sobie tropem — przeskanuj subdomenę deploy.* po liście popularnych domen SaaS i masz żniwo IP origin, które miały siedzieć schowane za Cloudflare'em.

Obejście Cloudflare WAF-a, obejście rate limitingu, obejście ochrony DDoS. W odległości jednego dig-a.

Dlaczego Claude to zauważył

Rozmawialiśmy o czymś innym. Poprosiłem go, żeby przejrzał świeżo napisany dokument deploya; otworzył config/deploy.yml do porównania, doszedł do linii 7 — - deploy.how2claude.com — zatrzymał się i powiedział mniej więcej tak:

Ten hostname rozwiązuje się przez publiczny DNS, prawda? To znaczy, że każdy z zapytaniem DNS dostaje to IP origin, a edge proxy Cloudflare jest pomijany.

Zamarłem na sekundę. To powinienem był zauważyć — wystarczyło spojrzeć drugi raz na ikonkę szarej chmurki przy konfiguracji Cloudflare i zapytać siebie, co ona oznacza — ale nie zrobiłem tego. Traktowałem ten hostname jako coś „wewnętrznego", tylko dlatego, że prefiks brzmiał deploy.. Mój mózg po cichu nadał mu prywatność, której nigdy nie miał.

Claude tego uprzedzenia nie ma. Czyta string w polu yaml i zadaje mechaniczne pytanie: jak ten string staje się IP-kiem? Odpowiedź: publiczny DNS. Wniosek: IP origin jest publiczne.

Fix: alias w /etc/hosts

Fix okazał się żenująco prosty. Kamal prosi lokalny klient SSH o rozwiązanie hostname'u, więc ten hostname musi rozwiązywać się tylko na mojej maszynie — nie w całym internecie.

Skróć hostname w yaml-u:

servers:
  web:
    - deploy.how2claude    # ← uwaga: bez .com

Potem dodaj linię do mojego /etc/hosts:

198.51.100.42  deploy.how2claude

Runnery CI, które deployują, potrzebują tej samej linii (zmienna środowiskowa albo wprost echo >> /etc/hosts w workflow).

Wynik:

  • W publicznym DNS-ie nie istnieje nigdzie rekord deploy.how2claude.*
  • Atakujący nie wyciągną IP przez DNS — przynajmniej ta droga jest zamknięta
  • Wszystkie kamal deploy / app exec / app logs dalej działają, bo /etc/hosts jest sprawdzany przed DNS-em
  • Cloudflare robi się czystszy: ta niezręczna szaro-chmurkowa subdomena po prostu znika

Akcesorium bazy danych wymaga tej samej zmiany:

accessories:
  db:
    image: postgres:17
    host: deploy.how2claude   # ← ten sam hostname

Claude przeniósł to na wszystkie trzy repozytoria

To jest część, którą chciałem zapisać.

Po wypchnięciu fixu how2claude już chciałem przełączać kontekst. Claude mnie zatrzymał: „W smarts i pickful też używasz Kamala, prawda? Sprawdźmy te dwa."

Sprawdził.

  • smarts: deploy.smarts.md — ten sam problem, publiczny rekord A
  • pickful: deploy.pickful.ai (production), deploy.pickful.xyz (alpha), plus dawno martwy deploy-test1.pickful.ai i deploy.staging.yml wskazujący na wycofaną domenę blockgeek.com

Pickful był najbardziej zagmatwany z całej trójki, bo miał kilka destinations (production / alpha / test2) i historyczny balast. Przy okazji Claude wymiótł martwe destinations staging i test1 — nie miały już racji bytu, czysty szum.

Końcowe commity:

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)

Czternaście sekund. Oczywiście to tylko GitHub opróżniający kolejkę merge'ów — prawdziwa robota była rozłożona na poprzednie dwie godziny. Ale efektywny kształt wyglądał tak: jedno znalezisko, trzy repozytoria załatwione.

Gdybym pracował sam, realistyczna wersja brzmi: poprawiam w how2claude, piszę TODO „zrób też smarts i pickful" i patrzę, jak to TODO leży na liście przez trzy miesiące. Tę dokładną sekwencję widziałem u siebie wielokrotnie.

Checklista do skopiowania

Jeśli deployujesz Kamalem, Capistrano albo dowolnym narzędziem opartym o SSH:

  1. Otwórz config/deploy*.yml i wykonaj grep po host: i servers:. Wymień każdy hostname, który się pojawia.
  2. Uruchom dig +short na każdym z nich. Cokolwiek zwraca IP origin zamiast pustki, wycieka.
  3. Przejdź panelem CDN (Cloudflare itp.) i wypatrz subdomeny z „szarą chmurką" — to zbiór kandydatów.
  4. Zmień ich nazwy na alias bez TLD (deploy.<project>) i włóż IP do /etc/hosts.
  5. Zaktualizuj też deploy job w CI. Na GitHub Actions: echo "$IP deploy.<project>" | sudo tee -a /etc/hosts.
  6. Usuń oryginalny publiczny rekord A.

Jeśli twój zespół prowadzi kilka projektów ze wspólnym wzorcem infrastruktury, napisz jednolinijkowego grepa, który przejedzie wszystkie deploy*.yml we wszystkich repozytoriach, — to pewniejsze niż sprawdzanie pojedynczo.

Prawdziwa lekcja

Cloudflare nie zawiódł. Kamal nie zawiódł. Obydwa narzędzia robiły dokładnie to, co miały robić.

Lekcja jest taka: wartości domyślne wprowadzają cię w błąd po cichu. deploy.<twoja-domena>.com brzmi jak wewnętrzna nazwa, ale DNS nie zna pojęcia „wewnętrzny" — rekord A to rekord A, i z chwilą opublikowania jest publiczny. Nazwa może dać ci iluzję prywatności, a ta mała ikonka szarej chmurki w panelu Cloudflare to tylko wizualna postać tej samej iluzji: siedzi sobie cicho, bez czerwonych ostrzeżeń, ale w istocie mówi: „ten rekord nie idzie przeze mnie".

Pozwól Claude'owi raz przejrzeć twoją konfigurację deploya. On tej iluzji nie podziela.