Free

Нехай Claude забере IP origin-сервера з публічного DNS

IP origin-сервера за Cloudflare витікає через піддомен deploy — три репозиторії за 14 секунд, виправлено гуртом


Того дня по обіді я якийсь час дивився на GitHub. Три репозиторії закрили PR о 16:20:31, 16:20:38 і 16:20:45 — з інтервалом у чотирнадцять секунд.

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

Усі три фікси були одного класу. Спочатку я просто говорив із Claude про сторонній невеличкий нюанс деплою в репозиторії how2claude. Між іншим він зазирнув у config/deploy.yml і сказав мені, що IP origin-сервера тієї машини відкрито висить у публічному DNS.

Ілюзія, яка обманювала мене кілька місяців

Усі мої проєкти стоять за Cloudflare. Помаранчева хмаринка в панелі Cloudflare означає «запис проксується» — HTTP-запити спершу потрапляють на edge-вузол Cloudflare, а звідти — до мого origin-сервера. IP origin не з'являється у DNS-відповідях; те, що повертає DNS, — це anycast-IP Cloudflare. Цю частину я зробив правильно.

Але я деплою через Kamal. Kamal заходить на сервер по SSH, щоб запускати docker. SSH не вміє ходити крізь HTTP-проксі, тож мені був потрібен hostname, не проксований Cloudflare, для SSH. Моя конфігурація була такою:

# config/deploy.yml
servers:
  web:
    - deploy.how2claude.com   # ← A-запис прямо до IP origin, сіра хмаринка (DNS only)

У Cloudflare how2claude.com і www.how2claude.com були помаранчевими. deploy.how2claude.com — сірим. Він мусив бути сірим, інакше SSH не зміг би дотягнутися до машини.

Сірий означає публічний DNS. Будь-хто може виконати:

$ dig deploy.how2claude.com +short
<мій IP origin>

А сама конвенція іменування deploy.<домен> — уже виразна позначка. Просканувавши піддомен deploy.* за списком популярних SaaS-доменів, ви збираєте гроно IP origin, що мали б ховатися за Cloudflare.

Обхід Cloudflare WAF, обхід rate limiting, обхід DDoS-захисту. На відстані одного dig.

Чому Claude помітив

Ми говорили про інше. Я попросив його переглянути документ із деплою, який щойно написав; він відкрив config/deploy.yml для звірки, дійшов до 7-го рядка — - deploy.how2claude.com — затримався і сказав приблизно ось таке:

Цей hostname резолвиться через публічний DNS, чи не так? Тобто будь-хто з DNS-запитом може отримати цей IP origin, і edge-проксі Cloudflare просто обходиться.

Я завмер на секунду. Це я мусив побачити — досить було вдруге придивитися до іконки сірої хмаринки під час налаштування Cloudflare й запитати себе, що вона означає, — але я не запитав. Я ставився до цього hostname як до чогось «внутрішнього» лише тому, що префікс — deploy.. Мій мозок тихо наділяв його приватністю, якої насправді не було.

Claude цієї упередженості не поділяє. Він читає рядок у полі yaml і ставить механічне питання: як цей рядок перетворюється на IP? Відповідь: публічний DNS. Висновок: IP origin публічний.

Фікс: alias у /etc/hosts

Фікс виявився соромно простим. Kamal просить локальний SSH-клієнт розв'язати hostname, тож цей hostname має резолвитися лише на моїй машині — не в усьому інтернеті.

Скоротіть hostname у yaml:

servers:
  web:
    - deploy.how2claude    # ← увага: без .com

Потім додайте рядок до мого /etc/hosts:

198.51.100.42  deploy.how2claude

CI-раннерам, що деплоять, потрібен той самий рядок (env-змінна або просто echo >> /etc/hosts у workflow).

Результат:

  • У публічному DNS ніде не існує запису deploy.how2claude.*
  • Атакувальники не можуть витягти IP через DNS — принаймні цей шлях закритий
  • Усі kamal deploy / app exec / app logs продовжують працювати, бо /etc/hosts зчитується раніше за DNS
  • Cloudflare стає чистіший: незручний сіро-хмарний піддомен просто зникає

Аксесуар бази даних потребує тієї ж зміни:

accessories:
  db:
    image: postgres:17
    host: deploy.how2claude   # ← той самий hostname

Claude переніс це у всі три репозиторії

Ось заради цього я й хотів написати.

Після того як ми відправили фікс how2claude, я вже збирався перемкнути контекст. Claude мене зупинив: «Ти ж і в smarts, і в pickful теж використовуєш Kamal? Дай-но я їх перевірю».

Перевірив.

  • smarts: deploy.smarts.md — та сама проблема, публічний A-запис
  • pickful: deploy.pickful.ai (production), deploy.pickful.xyz (alpha), плюс давно мертвий deploy-test1.pickful.ai і deploy.staging.yml, що посилався на знятий із вжитку домен blockgeek.com

Pickful був найзаплутанішим із трьох, бо мав кілька destinations (production / alpha / test2) та історичний баласт. Принагідно Claude вичистив мертві staging- й test1-destinations — їм не лишалося причин існувати, чистий шум.

Фінальні коміти:

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)

Чотирнадцять секунд. Звісно, це лише час, коли GitHub дочекався merge-черги — справжня робота була розкидана попередні дві години. Але ефективна форма така: одна знахідка, три репозиторії виправлено.

Якби я працював сам, реалістичний сценарій виглядав би так: пофіксив у how2claude, написав TODO «зробити smarts і pickful теж», і дивися, як те TODO лежить у списку три місяці. Я бачив цю послідовність на власні очі багато разів.

Чек-лист, який можна скопіювати

Якщо ви деплоїте через Kamal, Capistrano чи будь-який інший SSH-інструмент:

  1. Відкрийте config/deploy*.yml і зробіть grep по host: і servers:. Перерахуйте кожен hostname, що зустрівся.
  2. Запустіть dig +short на кожен. Усе, що повертає IP origin замість порожнечі, — це витік.
  3. Пройдіться панеллю свого CDN (Cloudflare тощо) і виловіть «сіро-хмарні» піддомени — це кандидати.
  4. Перейменуйте їх на приватний alias без TLD (deploy.<project>) і покладіть IP у /etc/hosts.
  5. Оновіть також деплой-job у CI. На GitHub Actions: echo "$IP deploy.<project>" | sudo tee -a /etc/hosts.
  6. Видаліть оригінальний публічний A-запис.

Якщо ваша команда веде кілька проєктів зі спільним інфраструктурним шаблоном, напишіть однорядковий grep, що пройдеться всіма deploy*.yml усіх репозиторіїв, — це надійніше, ніж перевіряти по одному.

Справжній урок

Cloudflare не підвів. Kamal не підвів. Обидва інструменти робили саме те, що мали робити.

Урок такий: значення за замовчуванням мовчки вводять вас в оману. deploy.<your-domain>.com звучить як внутрішнє ім'я, але DNS не знає поняття «внутрішній» — A-запис є A-записом, а в мить публікації він уже публічний. Ім'я може давати ілюзію приватності, а маленька іконка сірої хмаринки в панелі Cloudflare — лише візуальна форма тієї ж ілюзії: сидить там тихо, без червоних попереджень, але насправді говорить: «цей запис не йде через мене».

Дайте Claude хоч раз глянути на вашу deploy-конфігурацію. Він цієї ілюзії не поділяє.