У арендаторов панели работают на своих доменах: клиент указывает домен, ядро поднимает под него тенанта, и покупатели ходят на его адрес, а не на мой. Статику при этом отдавал один сервер в Европе, и для посетителя из Азии или Латинской Америки это лишние сотни миллисекунд на каждой картинке. Cloudflare-прокси закрывает вопрос одним переключателем: css, js и изображения начинают отдаваться с ближайшего к посетителю узла, а origin остаётся только для динамики.
Переключатель, правда, ломает соседнюю вещь. У меня была проверка «домен подключён и ведёт к нам», от которой зависит статус панели в кабинете и часть автоматики на моей стороне. Ровно её прокси и обесценивает: домен больше не резолвится в мой сервер. Ниже - как я это переделал и на какие грабли наступил по дороге.
Почему старая проверка сломалась бы
Доказательством «домен наш» был DNS. Клиент делегирует домен или ставит CNAME, я смотрю, куда ведёт запись, и если она указывает на мой сервер - домен подключён. Логика прозрачная, и до прокси она работала.
За оранжевым облаком домен резолвится в адреса Cloudflare. Моего сервера в ответе DNS нет вообще. То есть проверка уверенно скажет «домен ведёт не к нам» у полностью рабочей панели, и чем лучше настроен CDN, тем громче она врёт.
Правило, которое я из этого вынес: проверка не должна опираться ни на что, что меняет CDN. IP-адрес, цепочка редиректов, транспортные заголовки - всё это теперь принадлежит прокси. Мне принадлежит только тело ответа.
Доказательство переехало в тело ответа
Ядро отдаёт на каждом домене тенанта служебный эндпоинт:
GET https://panel.example.com/health
{"status":"ok","tenant_id":"8f2c41ab..."}
tenant_id - идентификатор тенанта. Домен считается подключённым, только если ответ успешный и этот идентификатор совпадает с тем, что записан у панели в биллинге. Через прокси поле доходит без изменений: Cloudflare не переписывает тело ответа, и это единственное свойство, на которое здесь можно опереться.
Одного кода 200 мало, и это не теория. По домену может отвечать что угодно: заглушка регистратора, старый сайт клиента, чужая панель, страница хостера. Все они отдают 200, и ни один не значит, что панель работает.
Редиректы не проходим намеренно
Первая версия проверки шла по цепочке редиректов до конца, как браузер. Однажды панель числилась рабочей, хотя домен вёл на посторонний хост: цепочка заканчивалась кодом 200 на чужом сайте. Клиент вписал в поле домена не то, что нужно, а проверка это проглотила.
Http::timeout(8)->withoutRedirecting()->acceptJson()
->get('https://'.$panel->domain.'/health');
Ядро отвечает на этот адрес напрямую, без переадресаций. Значит любой редирект - сам по себе признак, что по домену отвечает не оно, и проходить его незачем.
Порядок включения облака, который нельзя переставить
Сертификаты для доменов арендаторов выпускаются автоматически: веб-сервер ядра видит новое имя в SNI и берёт под него сертификат Let's Encrypt на месте, без ручного выпуска. Схема красивая до момента, когда домен ставят за прокси заранее.
Тогда ACME-проверка идёт через прокси на origin, у которого сертификата на это имя ещё нет. Cloudflare не может подключиться к origin по https, проверка не проходит, сертификат не выпускается - выпуск упирается сам в себя. Наблюдаемый симптом: запись в DNS есть, https не встаёт, и выглядит это как поломка на моей стороне.
Поэтому порядок жёсткий: запись создаётся серой, домен впервые отвечает, зоне ставится режим SSL strict, и только после этого включается облако.
- Серая запись сначала - чтобы ACME-проверка дошла до origin напрямую и сертификат лёг на диск.
- strict, а не flexible - на origin валидный сертификат ровно на это имя, а flexible ходит на origin по http и вместе с моим редиректом 80 на 443 даёт петлю.
- Переключение висит на проверке доступности, а не на создании зоны: момент «домен впервые ответил» виден именно там. Синхронизация зон обрабатывает только незавершённые, так что зона, ставшая активной давно, осталась бы серой навсегда.
Две мелочи, которые стоили времени. Первая: отметка «уже проксирован» ставится панели сразу после переключения, иначе плановая проверка каждые пятнадцать минут дёргала бы API Cloudflare по кругу. Вторая: токену нужно право на изменение настроек зоны - без него режим SSL не переключается тихо, без ошибки, и облако просто не включается.
Пять вердиктов вместо «доступен или нет»
Пока проверка была бинарной, она отвечала на вопрос «работает ли панель». С проксёй понадобился другой ответ: «что именно не так». Особенно у клиентов, которые держат домен в собственном аккаунте Cloudflare и включают облако сами, без меня.
- нет в DNS - домен не резолвится, запись ещё не прописана;
- не встаёт https - домен резолвится, но соединение не устанавливается;
- ответ есть, но нерабочий - 5xx, 404 хостера, редирект;
- отвечает чужой тенант - 200 и валидный JSON, но идентификатор не тот;
- работает.
Разделение окупилось сразу: «не встаёт https» в девяти случаях из десяти означает ровно одно - облако включили раньше, чем выпустился сертификат. До этого случай лежал в общей корзине «недоступен» рядом с «домена вообще нет», и разбирать приходилось руками по каждому обращению.
Как понять, что домен за проксёй, не заходя в зону
Когда зона в моём аккаунте, состояние облака я знаю. Когда домен в аккаунте клиента - нет, а знать полезно: заметная часть обращений «панель не открывается» объясняется именно проксёй, включённой не вовремя.
Признак дешёвый: Cloudflare ставит заголовок cf-ray на каждый свой ответ, включая собственные страницы ошибок 521 и 525. То есть прокси видно даже тогда, когда до origin запрос не дошёл вовсе.
Сложнее случай, когда https не встаёт и заголовков нет никаких. Тогда проверка спрашивает тот же домен по http: за проксёй на 80-м порту отвечает Cloudflare и снова ставит cf-ray, а у прямого домена там мой редирект на https без её заголовков. Лишний запрос уходит только на неудачной проверке, и тело в нём не читается вовсе - нужны одни заголовки.
На том конце сервер клиента
Мысль, которую легко упустить: проверка ходит не к себе. По домену панели стоит машина, которой я не управляю, и ответить она может чем угодно.
Таймаут ограничивает время, но не объём. Пока тело читалось целиком, мок на домене клиента мог влить в память процесса проверки сколько захочет. Теперь ответ читается потоком с потолком в 64 КБ, а остаток остаётся в сокете и уходит вместе с соединением. Наш /health укладывается в сотни байт, поэтому обрезанный JSON не разбирается, и такой ответ получает вердикт «чужой», а не «работает».
Честный предел: идентификатор это не подпись
Идентификатор тенанта не секрет. Ядро отдаёт его любому, кто постучится по домену, - иначе проверка не работала бы вообще. Значит владелец домена, у которого панель хоть раз открывалась, может увести домен на свой сервер и отдавать тот же JSON руками.
Прежде чем это чинить, стоит посчитать, что подделка вообще даёт. Доступов, счёта и денег она не касается: их считает ядро по своим данным, а биллинг ходит туда по внутреннему каналу с операторским токеном, и на домен панели этот токен не уходит никогда. Остаётся влияние на служебные признаки состояния домена - неприятно, но несоразмерно усилиям.
Если понадобится настоящая проверка, дешевле всего челлендж: проверяющий шлёт одноразовое случайное значение, ядро возвращает HMAC на общем секрете. Подделать без секрета нельзя, а через прокси такой ответ проходит без изменений - то есть свойство, на котором держится вся конструкция, сохраняется.
Что осталось в голове
CDN перед сайтом - это не «то же самое, только быстрее». Он забирает себе транспорт: IP, TLS, часть заголовков, порядок установления соединения. Всё, что мониторинг выводил из транспорта, после включения прокси придётся выводить из чего-то другого, и лучше заметить это самому, а не по жалобе клиента с рабочей панелью и красным статусом в кабинете.
Работающий критерий простой: проверяй то, что может подтвердить только твой код, и передавай это там, где прокси ничего не меняет. У меня это идентификатор тенанта в теле ответа. Заодно проверка стала правдивее, чем была: раньше она доказывала, что домен указывает на нужный сервер, а теперь - что по домену действительно отвечает нужная панель.