Проследите один запрос от телефона в поезде до бэкенда, который существует только в приватной сети. Телефон разрешает публичное имя, подключается к публичному адресу на порту 443 и выполняет обычное TLS-рукопожатие. Nginx принимает это соединение, а затем открывает второе, полностью отдельное, к адресу, до которого добирается только через интерфейс VPN. На телефоне ничего не меняется, и телефон никогда не узнаёт, что участвовал туннель.

Проследить один запрос через два соединения

Входящий участок — это публичная HTTP-транзакция. У неё есть публичное имя, публичный сертификат и обратный путь, который должен работать для любого адреса, какой есть у посетителя в этот момент. Любое изменение, которое нарушает этот путь, ломает всех посетителей сразу. Поэтому публичный участок — тот, на котором вы меньше всего хотите экспериментировать.

Исходящий участок приватный. Nginx становится клиентом, разрешает внутреннее имя или использует фиксированный адрес и доходит до службы, которая отклонила бы соединение из интернета.

Public client  -> frontend public address:443   (TLS, public name)
Frontend       -> VPN private backend 10.40.0.20:8443  (TLS, private name)

Держите их отдельно в голове, и конфигурация станет очевидной. Смешайте их, и вы в конце концов попытаетесь заставить публичного клиента участвовать в туннеле, и именно отсюда происходит большая часть путаницы в этом дизайне. Приватный участок может быть хрупким. Публичный — нет.

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

Имя шлюза должно пережить туннель

В разрешении имён есть тонкая ловушка. Если приватный DNS перекрывает то же имя, которое профиль VPN использует для собственного шлюза, повторное подключение может в итоге попытаться достичь этого шлюза через туннель, который он сам должен установить. Клиенту нужно, чтобы это имя разрешилось до того, как туннель существует. Как только оно разрешается внутри туннеля, соединение уже невозможно построить.

Используйте отдельные одобренные имена для шлюза и бэкенда и ограничьте политику резолвера так, чтобы каждое имя разрешалось в адрес, нужный его потребителю. Изменение заголовка Host приложения здесь не поможет, потому что поиск выполняет клиент VPN.

Затем подумайте о масштабе. Frontend нуждается в маршруте к адресу бэкенда и при этом должен продолжать отвечать публичным клиентам. Полный туннель на этом хосте может удовлетворить первому требованию и сломать второе.

Там, где политика это позволяет, туннель к конкретному назначению — более чистый дизайн. Там, где политика требует полного туннеля, frontend принадлежит отдельному сетевому контексту, а не латается ad hoc маршрутами для известных посетителей.

Написать блок сервера так, чтобы оба участка проверяли

Предположим, что бэкенд предъявляет сертификат для backend.corp.example и отвечает на приватном адресе. Подключение к адресу с проверкой имени — это именно то, для чего предназначены опции SNI и имени модуля прокси.

upstream private_app {
    server 10.40.0.20:8443;
}

server {
    listen 443 ssl;
    server_name app.example.com;

    ssl_certificate     /etc/nginx/tls/app-fullchain.pem;
    ssl_certificate_key /etc/nginx/tls/app-key.pem;

    location / {
        proxy_pass https://private_app;
        proxy_ssl_server_name on;
        proxy_ssl_name backend.corp.example;
        proxy_ssl_trusted_certificate /etc/nginx/trust/backend-ca.pem;
        proxy_ssl_verify on;
        proxy_ssl_verify_depth 3;

        proxy_set_header Host app.example.com;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

Каждый путь и адрес выше — это заполнитель. Важны те части, которые держат проверку включённой: имя для проверки, пакет доверия, с которым его сверять, и глубина, соответствующая одобренной цепочке.

proxy_set_header Host app.example.com — это намеренно. Бэкенд следует настроить принимать это публичное происхождение, и он должен доверять пересланным заголовкам только от этого frontend. Перезапись X-Forwarded-For адресом клиента правильна для одного прокси и ошибочна для цепочки, поэтому развёртывание с несколькими прокси требует собственного упорядочения доверенных адресов.

Заставить публичное происхождение вести себя как таковое

Приложение должно выдавать абсолютные URL, перенаправления и cookie для публичного происхождения. Если оно перенаправляет посетителя на backend.corp.example, этот посетитель попадает на имя, которое не может разрешить.

Исправьте это через поддерживаемую настройку внешнего URL приложения и проверенную политику перенаправлений, а не через переписывание имён хостов в телах ответов. Замена строк в произвольном содержимом ломает подписанные полезные нагрузки и создаёт ошибки, которые трудно отследить.

Cookie заслуживают такой же заботы. Cookie, привязанный к приватному имени, никогда не будет отправлен браузером, который знает только публичное.

Также подтвердите, что Nginx действительно может владеть публичным сокетом. Существующая веб-служба на 443 должна переехать, или frontend должен жить в другом месте, потому что два процесса не могут занять один и тот же адрес и порт.

Проверить каждый участок там, где он действительно работает

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

Затем проверьте участок бэкенда с самого хоста frontend, в том же сетевом контексте, который использует Nginx. Ping через туннель доказывает доступность и ничего про TLS, авторизацию или приложение.

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

Сохраните оба наблюдения вместе с развёрнутой конфигурацией и запишите, что происходит, когда туннель недоступен. Frontend должен возвращать контролируемую ошибку upstream, а не тихо переключаться на другой бэкенд, который никто не задокументировал. 502 с ясной строкой в логе — хороший результат. Закешированная страница из нежелательного источника — нет.

Похожие статьи