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-based инструмент:
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-конфигурацию. Эту иллюзию он не разделяет.