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-based инструмент:

  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. Обновите и deploy 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-конфигурацию. Эту иллюзию он не разделяет.