DNS, HTTPS и обратный прокси: как запрос попадает в приложение
В предыдущей главе мы запустили API на сервере. Приложение работает в контейнере, а на самом сервере доступно по адресу 127.0.0.1:8000. Если выполнить запрос прямо на сервере, оно отвечает.
Теперь нужен адрес https://api.example.com. Пользователь должен открыть его из интернета, получить защищённое соединение и попасть в то же приложение. Между этими двумя состояниями не хватает нескольких независимых механизмов.
- DNS сообщает клиенту, к какому IP-адресу обращаться.
- TLS позволяет проверить сервер и защитить данные при передаче.
- Обратный прокси принимает публичный запрос и передаёт его приложению.
Их часто настраивают одновременно. Поэтому при ошибке «сайт не открывается» трудно понять, где именно оборвался путь. Разберём каждый участок, затем соберём работающую конфигурацию и научимся проверять её по шагам.
Сначала представим весь путь запроса
Допустим, клиент обращается к https://api.example.com/orders/42. Упрощённый маршрут выглядит так:
Имя домена -> IP-адрес сервера -> соединение с портом 443 -> проверка TLS-сертификата -> обратный прокси -> 127.0.0.1:8000 -> обработчик /orders/42 в API.
При этом имя api.example.com участвует сразу в нескольких проверках:
- DNS связывает имя с IP-адресом.
- TLS проверяет, что предъявленный сертификат действителен для этого имени.
- HTTP передаёт имя сайта серверу, чтобы тот выбрал нужное приложение.
Это три разные операции. Корректная DNS-запись не создаёт сертификат. Действующий сертификат не запускает приложение. Работающее приложение не обязано быть доступным из интернета.
В нашем примере обратный прокси будет работать на сервере как системный сервис, а API останется за ним на 127.0.0.1:8000. Это важно: адрес 127.0.0.1 означает «этот же компьютер». Если позже поместить прокси в отдельный контейнер, его 127.0.0.1 уже будет указывать на сам контейнер, и адрес приложения придётся изменить.
Короткий вывод: публичный адрес возникает из согласованной работы нескольких компонентов. Проверять их удобнее в том же порядке, в котором проходит запрос.
Что делает DNS и чего он не делает
DNS хранит записи об именах. Для нашей задачи наиболее важны:
| Запись | Что содержит | Пример |
|---|---|---|
A |
IPv4-адрес | api.example.com -> 203.0.113.10 |
AAAA |
IPv6-адрес | api.example.com -> 2001:db8::10 |
CNAME |
Другое доменное имя, к которому нужно обратиться за адресом | api.example.com -> origin.example.net |
Адреса здесь демонстрационные. В реальной записи нужен публичный адрес вашего сервера.
Когда клиенту требуется IP для api.example.com, он обычно спрашивает свой рекурсивный DNS-резолвер. Тот либо возвращает ответ из кеша, либо получает его у серверов, отвечающих за нужную DNS-зону. Эти серверы называют авторитативными: именно у них хранится опубликованная владельцем домена запись. Подробнее последовательность описана в объяснении DNS от Cloudflare.
После получения IP DNS свою работу выполнил. Он не сообщает браузеру, какой процесс запущен на сервере, не открывает порт 443 и не знает путь /orders/42. Порт и путь содержатся в URL и обрабатываются другими участниками цепочки.
Почему изменение записи видно не сразу
У DNS-записи есть TTL, time to live. Это срок, в течение которого резолвер может использовать закешированный ответ без повторного запроса к авторитативному серверу.
Предположим, TTL равен 300 секундам. Резолвер получил старый IP за секунду до того, как вы поменяли запись. Он ещё некоторое время может возвращать прежний адрес. Поэтому после изменения DNS разные клиенты временно способны попадать на разные серверы. TTL помогает планировать перенос, но не гарантирует одновременное переключение всех клиентов. Поведение кеша и смысл TTL описаны также в справке ICANN.
Отсюда практическое правило: при переносе приложения не выключайте старый сервер сразу после изменения записи. Сначала убедитесь, что новый отвечает, и учитывайте время жизни прежних записей.
Ловушка с IPv6 и проксируемым DNS
Если у имени опубликованы и A, и AAAA, оба адреса должны вести к правильно настроенному сервису. Рабочий IPv4 не спасёт клиента, который попытался подключиться по опубликованному, но неработающему IPv6. Если IPv6 на сервере пока не настроен, не добавляйте AAAA «на всякий случай».
Есть и другая ситуация. У некоторых DNS-провайдеров запись можно перевести в режим проксирования. Тогда DNS-запрос возвращает IP провайдера, а не адрес вашего сервера. Трафик сначала идёт через его сеть, и в нашей схеме появляется ещё один TLS- и HTTP-участок. Например, так работает режим Proxied у Cloudflare. Ниже мы используем прямую DNS-запись, чтобы видеть исходный маршрут без этого дополнительного слоя.
Короткий вывод: DNS отвечает на вопрос «куда подключаться по имени». Он не отвечает на вопросы «защищено ли соединение» и «исправно ли приложение».
Что происходит при HTTPS-соединении
Получив IP, клиент устанавливает соединение с портом 443 и начинает обмен TLS. HTTPS означает, что HTTP-запрос передаётся внутри защищённого TLS-соединения.
Для читателя сайта в этом процессе важны три результата:
- Проверка стороны соединения. Сервер предъявляет сертификат для
api.example.com. Клиент проверяет имя, срок действия и цепочку доверия к удостоверяющему центру. - Согласование ключей. Клиент и сервер получают ключи для защиты дальнейшего обмена. В TLS 1.3 для этого используется протокол рукопожатия; подробности определены в RFC 8446.
- Защита HTTP-трафика. После рукопожатия запросы и ответы передаются в зашифрованном виде на этом участке соединения.
Сертификат доказывает право предъявлять данное доменное имя в рамках принятой системы доверия. Он не доказывает, что код приложения безопасен, база данных настроена правильно или ответ API достоверен.
Почему сертификат связан именно с доменом
Представим, что DNS направил api.example.com на нужный IP, но сервер предъявил сертификат только для shop.example.com. Подключение к правильному IP ещё не означает, что сертификат подходит для запрошенного имени. Клиент должен отклонить такое соединение.
Это свойство полезно и при ошибках, и при атаках: одного изменения DNS-ответа недостаточно, чтобы незаметно выдать чужой сервер за нужный сайт. Клиент по-прежнему проверяет сертификат. Обратное тоже верно: сертификат для домена не исправит неверную DNS-запись.
Как появляется сертификат
Удостоверяющий центр должен проверить, что оператор сервера контролирует домен. Один из распространённых способов, HTTP-01, требует разместить проверочный ответ по специальному URL и доступен через порт 80. Другой способ, DNS-01, требует добавить проверочную TXT-запись в DNS; он применим, в частности, для wildcard-сертификатов. У этих способов разные требования к инфраструктуре, подробно они описаны в документации Let’s Encrypt.
В нашем примере выдачей и обновлением сертификата займётся Caddy. Для стандартного сценария ему нужны корректный публичный DNS и доступные снаружи порты 80 и 443. Порт 80 также используется для перенаправления обычных HTTP-запросов на HTTPS. Возможности автоматического HTTPS описаны в документации Caddy.
Есть важная граница защиты: TLS-соединение в нашей конфигурации заканчивается на Caddy. От Caddy к приложению запрос пойдёт обычным HTTP через локальное соединение. На одном сервере это разумный вариант, если порт приложения не открыт наружу. Если прокси и приложение расположены на разных машинах или между ними недоверенная сеть, этот второй участок тоже нужно защищать, например HTTPS или взаимной TLS-аутентификацией.
Короткий вывод: HTTPS защищает конкретный участок между клиентом и конечной точкой TLS. Нужно понимать, где именно заканчивается шифрование и что происходит с запросом дальше.
Зачем нужен обратный прокси
Обратный прокси стоит перед одним или несколькими приложениями. Клиент подключается к прокси, а тот решает, как обработать запрос: передать его приложению, вернуть статический файл, ограничить доступ или выбрать другой сервер.
В нашем случае он решает сразу четыре задачи:
- слушает публичные порты 80 и 443;
- принимает HTTPS и управляет сертификатом;
- передаёт запросы API на
127.0.0.1:8000; - возвращает ответ приложения клиенту через защищённое соединение.
Термин «обратный» отличает его от обычного клиентского прокси. Клиентский прокси помогает клиентам выходить к разным серверам. Обратный прокси представляет серверное приложение перед разными клиентами.
Важно понимать, что проксирование не создаёт обработчик запроса. Если приложение не умеет отвечать на /orders/42, прокси не исправит это. Он передаст путь приложению и вернёт полученный ответ.
У Caddy обычный reverse_proxy по умолчанию сохраняет метод и URI запроса, передаёт исходный Host и устанавливает заголовки семейства X-Forwarded-*, которые сообщают приложению данные о внешнем запросе. Эти детали и настройки для более сложных маршрутов приведены в документации reverse_proxy.
Собираем конфигурацию на одном сервере
Возьмём конкретные условия:
- сервер имеет публичный IPv4
203.0.113.10; - домен
api.example.comнаходится под нашим управлением; - API запущен в Docker Compose;
- на сервере порт API опубликован только на
127.0.0.1:8000; - Caddy устанавливается и запускается на самом сервере.
Шаг 1. Проверяем приложение без DNS и HTTPS
Сначала на сервере проверяем непосредственный ответ API:
curl -i http://127.0.0.1:8000/health/ready
Здесь /health/ready служит примером маршрута проверки готовности. Его нужно реализовать в приложении. Ответ 200 OK должен означать, что сервис действительно способен обслуживать запросы. Если для работы каждого запроса требуется база данных, проверка готовности должна учитывать и её доступность. Простое «процесс жив» для этой цели недостаточно.
В Compose публикация порта может выглядеть так:
services:
api:
ports:
- "127.0.0.1:8000:8000"
Первая часть означает адрес и порт на сервере, последняя 8000 означает порт в контейнере. Само приложение внутри контейнера должно слушать доступный контейнеру интерфейс, обычно 0.0.0.0:8000. При этом публикация на 127.0.0.1 ограничивает доступ к порту со стороны сети сервера. Формат публикации портов описан в спецификации Docker Compose.
Если первый curl уже не отвечает, настройка домена и сертификата пока не поможет. Сначала нужно восстановить работу API.
Шаг 2. Публикуем DNS-запись
Создаём запись:
Тип: A
Имя: api
Значение: 203.0.113.10
У разных панелей управления поле «Имя» может ожидать api либо полное api.example.com. Итоговая запись должна относиться именно к api.example.com.
Проверяем ответ:
dig +short A api.example.com
dig +short AAAA api.example.com
Первый запрос должен вернуть публичный IPv4 сервера. Второй нужен, чтобы заметить лишнюю или устаревшую IPv6-запись. Если DNS-провайдер работает в режиме проксирования, результат может содержать адрес провайдера, а не нашего сервера: это отдельная архитектура, которую нужно учитывать при диагностике.
Шаг 3. Открываем вход к прокси
На сервере и во внешних правилах облачного провайдера разрешаем входящие подключения к TCP-портам 80 и 443. Порт 8000 снаружи открывать не нужно. Проверяем, что другие процессы не заняли 80 и 443: одновременно принимать соединения на одном и том же адресе и порту два независимых веб-сервера не смогут.
Также Caddy нужен постоянный доступ к своему хранилищу сертификатов. Если удалить его данные при каждом перезапуске или не дать процессу права на запись, автоматическое управление сертификатами будет работать неправильно.
Шаг 4. Настраиваем Caddy
В /etc/caddy/Caddyfile достаточно такого сайта:
api.example.com {
reverse_proxy 127.0.0.1:8000
}
Здесь:
api.example.comзадаёт публичное имя сайта;- Caddy принимает соединение для этого имени и управляет HTTPS;
reverse_proxyпередаёт запрос локальному API.
Базовый вариант такой конфигурации есть в примерах Caddyfile. Прежде чем применить изменения, проверяем файл:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Команда проверки валидирует конфигурацию; перезагрузка применяет её к работающему сервису. Конкретный способ управления сервисом зависит от того, как Caddy установлен на сервере. Команды Caddy описаны в официальной справке.
После настройки проверяем оба публичных входа:
curl -i http://api.example.com/health/ready
curl -i https://api.example.com/health/ready
Для HTTP ожидается перенаправление на HTTPS. Для HTTPS ожидается ответ маршрута приложения. Проверку намеренно выполняем без -k: этот флаг отключает проверку сертификата и может скрыть именно ту ошибку, которую мы ищем.
Короткий вывод: порядок настройки соответствует зависимостям. Сначала работает API, затем DNS ведёт к серверу, затем прокси принимает HTTPS и передаёт запрос в API.
Проследим один запрос до ответа
Теперь клиент вызывает https://api.example.com/orders/42.
- DNS возвращает адрес сервера.
- Клиент устанавливает соединение с портом 443.
- Во время TLS-рукопожатия Caddy предъявляет сертификат для
api.example.com; клиент его проверяет. - Клиент отправляет HTTP-запрос внутри защищённого соединения.
- Caddy выбирает сайт
api.example.comи отправляет запрос на127.0.0.1:8000. - API находит заказ, обращается к своим зависимостям при необходимости и формирует ответ.
- Caddy получает ответ API и передаёт его клиенту через TLS-соединение.
Для HTTP/1.1 часть запроса можно представить так:
GET /orders/42 HTTP/1.1
Host: api.example.com
Host важен, когда на одном IP работают несколько сайтов: прокси должен понять, к какому из них относится запрос. В HTTP/2 для этой роли используется поле :authority; принцип остаётся тем же. TLS-сертификат выбирается ещё до чтения HTTP-запроса, поэтому имя сайта передаётся и на этапе установки TLS-соединения.
Из последовательности видно, почему нельзя считать DNS, HTTPS и маршрутизацию одной настройкой. Они происходят в разное время и отвечают на разные вопросы.
Какие данные приложение узнаёт о клиенте
После передачи запроса через Caddy приложение видит непосредственное соединение от прокси, а не от браузера. Кроме того, участок Caddy -> API работает по HTTP, хотя клиент использовал HTTPS. Если приложение строит абсолютные ссылки, перенаправления или ведёт журнал клиентских IP, ему может потребоваться информация об исходном запросе.
Для этого прокси передаёт специальные заголовки:
X-Forwarded-Forсообщает исходный адрес клиента;X-Forwarded-Protoсообщает внешнюю схему, напримерhttps;X-Forwarded-Hostсообщает исходное имя хоста.
Но такой заголовок технически может прислать и обычный клиент. Поэтому приложение должно доверять ему только если запрос поступил от доверенного прокси. В FastAPI и Uvicorn это настраивается через разрешённые адреса прокси; иначе возможны неверные ссылки, неправильные перенаправления и подмена адресов в журналах. В документации FastAPI и Uvicorn отдельно описаны forwarded-заголовки и параметр --forwarded-allow-ips.
В нашем варианте Caddy расположен на сервере, а API находится в контейнере. Для Uvicorn непосредственный сетевой адрес Caddy может выглядеть как адрес шлюза Docker, а не как 127.0.0.1. Поэтому нельзя механически скопировать настройку доверия для двух процессов без контейнеров: сначала выясняют, с какого адреса запрос виден приложению, затем доверяют именно этому источнику. Значение * допустимо лишь тогда, когда к API действительно могут обращаться только доверенные компоненты.
Это не обязательный шаг для ответа простого обработчика, но обязательная проверка перед тем, как использовать исходный IP или схему запроса для решений приложения.
Короткий вывод: прокси знает внешний адрес клиента и HTTPS-схему; приложение получает эти сведения через заголовки. Доверие к ним определяется сетевой границей, а не самим наличием заголовка.
Как понять, где оборвался запрос
Один и тот же симптом в браузере может возникнуть на разных участках. Диагностику удобно вести от внешнего имени к приложению.
| Наблюдение | Где искать причину | Что проверить |
|---|---|---|
NXDOMAIN |
DNS | Существует ли запись; верно ли указано имя |
| Возвращается старый или чужой IP | DNS и кеши | Авторитативную запись, TTL, A и AAAA |
| Подключение к 443 не устанавливается | Сеть или прокси | Правила firewall, порт 443, запущен ли Caddy |
| Ошибка имени или доверия к сертификату | TLS | Для какого имени выдан сертификат; завершилась ли его выдача |
| HTTP перенаправляет, HTTPS не работает | TLS или порт 443 | Журнал Caddy, сертификат, доступность 443 |
Caddy отвечает 502 |
Связь прокси с API | Работает ли 127.0.0.1:8000, правильный ли адрес upstream |
API отвечает 404 |
Маршрут приложения | Существует ли путь; не переписал ли его прокси |
API отвечает 500 |
Приложение или зависимости | Журнал API, состояние БД и внешних сервисов |
API создаёт ссылки с http:// вместо https:// |
Доверие к прокси | X-Forwarded-Proto и настройки Uvicorn |
502 обычно означает, что прокси не получил нормальный ответ от upstream, например не смог к нему подключиться. Но приложение тоже может само вернуть код 502 в другой ситуации, поэтому окончательный вывод делают по журналам и прямой проверке upstream. Типичное поведение Caddy при недоступном backend описано в его примерах конфигураций.
Полезно выполнять проверки в строгой последовательности:
# На сервере: работает ли само приложение?
curl -i http://127.0.0.1:8000/health/ready
# На любой машине: куда ведёт имя?
dig +short A api.example.com
dig +short AAAA api.example.com
# Снаружи: доступен ли публичный HTTPS?
curl -v https://api.example.com/health/ready
Если нужно проверить конкретный IP, сохранив в запросе имя домена, помогает curl --resolve:
curl --resolve api.example.com:443:203.0.113.10 \
-i https://api.example.com/health/ready
Эта опция подставляет адрес для клиентского подключения, но сохраняет доменное имя в URL и TLS-проверке. Она удобна при сравнении старого и нового сервера. При этом она не заменяет публичную DNS-запись для удостоверяющего центра: если Caddy ещё не смог получить сертификат, тест с --resolve сам по себе эту проблему не устранит. Работа опции описана в документации curl.
Где у этой схемы границы
Конфигурация с одним Caddy и одним API подходит для первого публичного запуска. Позже требования могут измениться.
Несколько приложений на одном сервере. Прокси может выбирать upstream по домену: api.example.com направлять в API, а admin.example.com в административный интерфейс. При маршрутизации по пути, например /api/*, нужно заранее решить, получает ли приложение путь с префиксом /api или без него. Иначе документация API, ссылки и маршруты могут расходиться.
Несколько экземпляров API. Обратный прокси может распределять запросы между ними. Но само добавление второго upstream не делает систему отказоустойчивой: нужны проверки здоровья, понятная стратегия повторов, общее или согласованное состояние и план обновления экземпляров. Особенно осторожно следует повторять запросы, которые изменяют данные.
Прокси и API на разных серверах. Тогда локального 127.0.0.1 уже нет. Появляются отдельная сеть, её правила доступа и вопрос защиты участка между прокси и API. Шифрование соединения «браузер -> прокси» не распространяется автоматически на следующий участок.
Внешний CDN или балансировщик перед Caddy. В цепочке появляется ещё один прокси. Нужно понять, где завершается TLS, какой компонент записывает исходный IP и каким forwarded-заголовкам разрешено доверять. Если всё принимать на веру, адрес клиента в журналах и правилах доступа можно исказить.
HSTS. Заголовок HSTS просит браузер в дальнейшем обращаться к домену только по HTTPS. Его имеет смысл включать после проверки стабильной работы HTTPS для нужных имён: длительный срок действия затрудняет откат ошибочной конфигурации. Автоматическое перенаправление HTTP на HTTPS и HSTS решают связанные, но разные задачи.
Что нужно уметь объяснить после настройки
Когда пользователь открывает https://api.example.com, происходят три принципиально разные проверки.
DNS находит сетевой адрес. TLS проверяет имя сервера и защищает соединение до точки завершения шифрования. Обратный прокси принимает HTTP-запрос и передаёт его нужному приложению.
Поэтому рабочую конфигурацию проверяют снизу вверх: приложение отвечает локально, домен указывает туда, куда нужно, порты доступны, сертификат подходит имени, прокси достигает upstream, а само приложение корректно обрабатывает маршрут. Такой порядок превращает неопределённое «сайт не работает» в конкретную неисправность на одном участке пути.