IT-инфраструктура • инструкции • практикаПоиск по сайту

Серверы и системное администрирование

Найти материал →

Кастомный домен для saas: Cname, certbot и безопасная выдача Ssl-сертификатов

Кастомный домен для SaaS: CNAME, certbot и небольшой Go-сервис вместо ACME on-demand

Функция "разместить страницу сервиса на домене клиента" кажется простой только на уровне постановки задачи. Пользователь добавляет запись `status.client.ru → status.your-service.com`, после чего необходимо решить сразу несколько вопросов: как подтвердить владение доменом, кто выпустит сертификат, где будут храниться приватные ключи, как организовать продление и как защититься от злоумышленников, которые начнут отправлять случайные Host-заголовки.

На практике подключение домена клиента к сервису затрагивает DNS, TLS, веб-сервер, базу данных и безопасность приложения. Автор реализовывал такую схему для мониторинга с публичными статус-страницами. Веб-студиям требовался white-label-вариант: клиент должен был видеть страницу мониторинга на собственном домене, без упоминания исходного сервиса.

Архитектура получилась достаточно прагматичной: один сервер, nginx перед Go-приложением, без Kubernetes и лишних распределённых компонентов. Подробный разбор подобной реализации, включая практические нюансы, представлен в материале о том, как организовать кастомный домен для SaaS без передачи приложению чрезмерных полномочий.

Почему не стоит выпускать сертификат во время TLS-запроса

Первый очевидный вариант - использовать Caddy или OpenResty с автоматическим выпуском сертификатов. При поступлении запроса с неизвестным Host-сервер проверяет домен, обращается к Let's Encrypt, получает сертификат и сохраняет его для дальнейших запросов.

Такой подход может работать, но у него есть несколько серьёзных недостатков.

Во-первых, приватные ключи оказываются в распоряжении компонента, который завершает TLS-соединение. Если это само приложение, ему приходится предоставлять слишком широкие права. Если сертификатами занимается отдельный Caddy, появляется ещё один stateful-компонент: его состояние нужно сохранять, резервировать, мониторить и восстанавливать после сбоев.

Во-вторых, возникает риск исчерпания лимитов Let's Encrypt. Любой пользователь может направить CNAME на ваш IP-адрес или просто отправить запрос с произвольным Host-заголовком. Если система реагирует на каждый такой запрос попыткой выпуска сертификата, злоумышленник способен быстро израсходовать лимит новых заказов - до 300 заказов на аккаунт за три часа. В результате реальные клиенты останутся без сертификатов.

Конечно, on-demand-механизм можно ограничить списком доменов из базы данных. Но тогда выпуск уже не будет действительно "мгновенным": перед TLS-handshake всё равно потребуется проверка состояния домена. Логика усложняется, а место для неё - самый неудобный участок всей цепочки.

Наконец, первый посетитель страницы будет ждать завершения операции с центром сертификации. Если Let's Encrypt временно недоступен или DNS ещё не обновился, пользователь получит задержку либо ошибку соединения.

Гораздо надёжнее запускать выпуск сертификатов заранее. Домен уже хранится в базе после ввода в настройках, а между сохранением формы и созданием DNS-записи всё равно может пройти от нескольких минут до суток. Поэтому асинхронная обработка здесь естественнее синхронной.

Разделение системы на независимые компоненты

Схема строится из четырёх частей:

1. приложение принимает домен и сохраняет его настройки;
2. фоновый DNS-верификатор проверяет, указывает ли домен на нужный сервер;
3. небольшой helper с ограниченными правами запускает certbot;
4. nginx обслуживает HTTPS-трафик и маршрутизирует запросы в приложение.

Ключевой принцип - строгое разделение полномочий. Go-приложение не должно напрямую изменять конфигурацию nginx или работать с файлами сертификатов. Helper, напротив, не должен иметь доступа к базе данных и бизнес-логике. Для взаимодействия достаточно двух loopback-эндпоинтов на `127.0.0.1`.

Такая модель уменьшает последствия компрометации приложения. Даже если в веб-коде появится уязвимость, злоумышленник не получит автоматически возможность читать приватные ключи или выполнять произвольные команды от имени root.

Валидация домена начинается до DNS-проверки

Пользовательская строка может попасть сразу в несколько опасных мест: SQL-запрос, имя файла nginx-конфигурации и аргументы certbot. Поэтому нормализация и проверка должны выполняться до сохранения значения.

Практически полезны следующие правила:

- разрешать только доменные имена в ожидаемом формате;
- отклонять пробелы, слеши, управляющие символы и неожиданные разделители;
- ограничивать длину строки;
- не принимать IP-адреса, если функция предназначена именно для доменов;
- отдельно обрабатывать пустое значение.

Кириллические домены лучше не преобразовывать молча. Пользователь должен самостоятельно указать punycode, например `xn--...`. Иначе он будет сравнивать русскоязычную запись с ASCII-вариантом у регистратора и решать, что настройка CNAME для домена не работает.

Важно запретить собственные служебные домены. Иначе пользователь сможет указать, например, `app.your-service.com`: DNS действительно будет вести на ваш сервер, проверка пройдёт, а Host-маршрутизация потенциально направит его страницу вместо личного кабинета или другого системного раздела.

Пустая строка, наоборот, должна считаться допустимым значением. Это удобный способ удалить ранее подключённый домен. После сохранения объект получает статус `pending`, а вся последующая проверка выполняется в фоне.

DNS-верификация: CNAME, apex и задержки

Фоновый процесс запускается, например, раз в пять минут и обходит домены со статусами `pending` или `retry`. Для каждого имени он проверяет DNS-ответ и сравнивает его с разрешёнными адресами сервиса.

Проверка только CNAME недостаточна. Для поддомена вроде `status.client.ru` стандартная запись CNAME подходит, но для корневого домена `client.ru` она обычно невозможна: на apex уже присутствуют обязательные записи SOA и NS. Поэтому клиенту приходится использовать A-запись либо возможности DNS-провайдера вроде ALIAS или ANAME.

Надёжная логика должна учитывать как CNAME, так и A-записи. При проверке A-записей сравниваются IP домена клиента и IP-адреса целевого сервиса. Это одновременно покрывает apex-домены и DNS-провайдеров, которые разворачивают псевдо-CNAME на своей стороне.

Результат проверки нужно сохранять в базе вместе с понятным описанием причины. Пользователь должен увидеть не абстрактное `verification failed`, а сообщение вроде: "домен не резолвится" или "добавьте CNAME `status.client.ru` на `status.your-service.com`". Для интерфейса это не второстепенная деталь, а полноценная часть продукта.

После успешной DNS-проверки домен переводится в состояние `verified`, и только затем запускается получение сертификата. При ошибке лучше применять задержку между попытками, чтобы не нагружать DNS и не создавать бесконечный цикл обращений.

Сертификаты и права helper-процесса

Получение сертификата выполняется отдельным процессом с минимальным API. Приложение передаёт ему только заранее проверенное доменное имя, а helper запускает certbot с фиксированным набором параметров.

Это позволяет реализовать автоматическую выдачу SSL сертификатов, не предоставляя основному приложению root-доступ. Файлы certbot размещаются в предсказуемых каталогах, а права на чтение приватных ключей получает только nginx и процесс, которому они действительно нужны.

В результате SSL сертификат для пользовательского домена выпускается заранее, до первого реального посещения страницы. После успешного завершения certbot nginx перечитывает конфигурацию или сертификаты без полной остановки сервиса. Продление выполняется тем же механизмом по расписанию.

Отдельное внимание стоит уделить блокировке параллельных запусков. Если несколько воркеров одновременно решат, что сертификат отсутствует, они могут вызвать certbot одновременно и получить конфликт файлов или превышение лимитов. На уровне helper необходим mutex, lock-файл либо очередь заданий.

HostRouter и опасность loopback-запросов

После выпуска сертификата nginx принимает запрос по нужному домену и передаёт его приложению. Go-код извлекает Host и ищет соответствующую страницу в базе.

Здесь важно не доверять бездумно `X-Forwarded-Host` и `X-Forwarded-For`. Если nginx неправильно настроен и позволяет клиенту подставлять эти заголовки, внутренний endpoint может стать доступен снаружи. Особенно опасны служебные маршруты, которые приложение считает защищёнными только потому, что запрос пришёл с loopback-адреса.

Прокси должен сам перезаписывать forwarding-заголовки, а приложение - принимать их только от доверенного reverse proxy. Внутренние эндпоинты следует слушать исключительно на `127.0.0.1`, дополнительно ограничивая методы, формат тела запроса и допустимые значения.

Что стоит предусмотреть заранее

У схемы есть естественные ограничения. DNS-проверка не всегда завершается быстро: кеширование, TTL и особенности конкретного регистратора могут растянуть подключение на часы. Пользовательский интерфейс должен показывать промежуточные статусы, а не создавать впечатление, что система зависла.

Также необходимо ограничить количество доменов на аккаунт, добавить rate limit для операций подключения и вести журнал событий: кто добавил домен, когда прошла проверка, почему выпуск сертификата завершился ошибкой. Это помогает расследовать как обычные ошибки настройки, так и попытки злоупотребления сервисом.

Перед запуском в продакшене полезно протестировать удаление домена, смену DNS, истёкший сертификат, недоступность Let's Encrypt и восстановление после перезапуска сервера. Для пользователей важен не только успешный сценарий, но и предсказуемое поведение при каждом промежуточном сбое.

Главный вывод прост: подключение домена клиента к сервису не обязано превращаться в сложную платформу. Один сервер, nginx, фоновая проверка DNS, ограниченный helper и certbot закрывают задачу, если заранее разделить полномочия и не пытаться выпускать сертификаты внутри TLS-handshake. Такой подход чуть менее "магический", зато лучше контролируется, безопаснее масштабируется и понятнее сопровождается.

Прокрутить вверх