Публичные страницы SaaS могут оставаться открытыми для всех, пока административные операции за ними отказывают каждому вне VPN. Механизм, который это обеспечивает, — не скрытое меню и не отдельная страница входа; это проверка сетевого пути, по которому пришёл запрос. Ошибётесь с доказательством адреса — и проверка либо заблокирует настоящих администраторов, либо пропустит любого, кто умеет набрать заголовок. Начните с того, что источник действительно получает.

Что источник действительно получает после туннеля

Когда браузер попадает на публичный сайт через VPN, который несёт и интернет-трафик, шлюз преобразует соединение до того, как оно уйдёт. SaaS видит публичный исходящий адрес шлюза, обычно один адрес или небольшой диапазон, и никогда не видит адрес туннеля ноутбука. Поэтому внесение 10.90.0.42 в список разрешённых не срабатывает, и поэтому внесение всего RFC 1918 не срабатывает ещё громче: второе пропускает любую частную сеть, которая может до вас дотянуться. Единственный адрес, достойный условия, — тот, которым ваша организация действительно владеет на выходе.

curl -s https://ifconfig.me
dig +short admin.example.com

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

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

Доверяйте только прокси, который переписывает адрес

Большинство публичных сайтов стоят за CDN или обратным прокси, поэтому адрес сокета источника принадлежит этому фронтенду, а не посетителю. Фронтенд обычно передаёт исходный адрес в заголовке, и этот заголовок контролирует атакующий, если источник не принимает его только от известных прокси. Явно настройте список доверенных источников и сделайте источник недоступным откуда-либо ещё. Без второй половины атакующий обходит прокси и отправляет заголовок напрямую.

set_real_ip_from 192.0.2.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
curl -H 'X-Forwarded-For: 198.51.100.27' https://origin.example.com/admin

То же рассуждение относится к протоколу PROXY: он надёжен только когда слушатель привязан к интерфейсу, до которого прокси может дотянуться, а больше никто не может. Отсутствующий контекст адреса должен приводить к провалу защищённой проверки, а не к переходу в разрешение по умолчанию. Отсутствующий заголовок не является доказательством доверенного пути.

Привяжите условие к каждой защищённой операции

Поместите проверку на границу авторизации, через которую проходит каждая защищённая операция, а не в страницу, которая отрисовывает меню. Группы маршрутов, middleware или слой политик работают все; копия в каждом обработчике — нет, потому что у обработчика, добавленного в следующем квартале, её не будет.

user = requireAuthenticatedSession()
requirePermission(user, "admin")
requireVpnEgress(verifiedClientAddress(request))
performProtectedOperation(user, request)

Та же граница должна покрывать JSON API, экспорты, подписанные URL загрузки и рукопожатие WebSocket, поскольку ни одному из них не нужно, чтобы страница HTML запрашивалась первой. Ссылка на загрузку, созданная пока VPN работал, остаётся действительной и потом, если конечная точка загрузки не перепроверяет. Долгим сессиям тоже нужно правило переоценки, иначе один аутентифицированный запрос становится постоянным исключением. Отказ по умолчанию — единственная настройка, которая переживает смену персонала.

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

Проверьте обходы, которые вы только что закрыли

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

curl -sI https://admin.example.com/export.csv
curl -s -H 'X-Forwarded-For: 198.51.100.27' https://origin.example.com/admin
curl -s https://admin.example.com/api/me -o /dev/null -w '%{http_code}\n'

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

Исходящий адрес доказывает разрешённый сетевой путь, а не человека и не управляемое устройство. Держите его одним условием рядом с аутентификацией и разрешением, и административная зона остаётся узкой, не превращая весь продукт в сервис только для VPN. Это разделение и есть дизайн, а не его ограничение.

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