IP origin-сервера за Cloudflare витікає через піддомен deploy — три репозиторії за 14 секунд, виправлено гуртом
Того дня по обіді я якийсь час дивився на GitHub. Три репозиторії закрили PR о 16:20:31, 16:20:38 і 16:20:45 — з інтервалом у чотирнадцять секунд.
smarts #38: deploy.smarts.md → deploy.smartshow2claude #13: deploy.how2claude.com → deploy.how2claudepickful #118: deploy.pickful.ai / deploy.pickful.xyz → deploy.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.
Ми говорили про інше. Я попросив його переглянути документ із деплою, який щойно написав; він відкрив config/deploy.yml для звірки, дійшов до 7-го рядка — - deploy.how2claude.com — затримався і сказав приблизно ось таке:
Цей hostname резолвиться через публічний DNS, чи не так? Тобто будь-хто з DNS-запитом може отримати цей IP origin, і edge-проксі Cloudflare просто обходиться.
Я завмер на секунду. Це я мусив побачити — досить було вдруге придивитися до іконки сірої хмаринки під час налаштування Cloudflare й запитати себе, що вона означає, — але я не запитав. Я ставився до цього hostname як до чогось «внутрішнього» лише тому, що префікс — deploy.. Мій мозок тихо наділяв його приватністю, якої насправді не було.
Claude цієї упередженості не поділяє. Він читає рядок у полі yaml і ставить механічне питання: як цей рядок перетворюється на IP? Відповідь: публічний DNS. Висновок: IP origin публічний.
/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).
Результат:
deploy.how2claude.*kamal deploy / app exec / app logs продовжують працювати, бо /etc/hosts зчитується раніше за DNSАксесуар бази даних потребує тієї ж зміни:
accessories:
db:
image: postgres:17
host: deploy.how2claude # ← той самий hostname
Ось заради цього я й хотів написати.
Після того як ми відправили фікс how2claude, я вже збирався перемкнути контекст. Claude мене зупинив: «Ти ж і в smarts, і в pickful теж використовуєш Kamal? Дай-но я їх перевірю».
Перевірив.
deploy.smarts.md — та сама проблема, публічний A-записdeploy.pickful.ai (production), deploy.pickful.xyz (alpha), плюс давно мертвий deploy-test1.pickful.ai і deploy.staging.yml, що посилався на знятий із вжитку домен blockgeek.comPickful був найзаплутанішим із трьох, бо мав кілька 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-інструмент:
config/deploy*.yml і зробіть grep по host: і servers:. Перерахуйте кожен hostname, що зустрівся.dig +short на кожен. Усе, що повертає IP origin замість порожнечі, — це витік.deploy.<project>) і покладіть IP у /etc/hosts.echo "$IP deploy.<project>" | sudo tee -a /etc/hosts.Якщо ваша команда веде кілька проєктів зі спільним інфраструктурним шаблоном, напишіть однорядковий grep, що пройдеться всіма deploy*.yml усіх репозиторіїв, — це надійніше, ніж перевіряти по одному.
Cloudflare не підвів. Kamal не підвів. Обидва інструменти робили саме те, що мали робити.
Урок такий: значення за замовчуванням мовчки вводять вас в оману. deploy.<your-domain>.com звучить як внутрішнє ім'я, але DNS не знає поняття «внутрішній» — A-запис є A-записом, а в мить публікації він уже публічний. Ім'я може давати ілюзію приватності, а маленька іконка сірої хмаринки в панелі Cloudflare — лише візуальна форма тієї ж ілюзії: сидить там тихо, без червоних попереджень, але насправді говорить: «цей запис не йде через мене».
Дайте Claude хоч раз глянути на вашу deploy-конфігурацію. Він цієї ілюзії не поділяє.