Допустим, на VPS живёт небольшая веб-панель: мониторинг, админка внутреннего сервиса или консоль для очереди задач. Своей авторизации у неё нет вовсе, или есть только общий пароль, который давно висит в чате. Компания при этом пользуется корпоративной учётной записью, и хочется, чтобы доступ к панели шёл через неё. Менять код панели нельзя или незачем. Остаётся поставить перед ней проверку. Именно для такой роли и подходит связка nginx и OAuth2 Proxy.

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

Два маршрута к одной панели

Для примера возьмём условную картину. Панель запущена как процесс, который слушает порт 8080. Пока никакой защиты нет, она доступна по адресу вида http://IP-сервера:8080/. Всё работает, но любой, кто узнал адрес и порт, видит панель целиком. Сканеры находят такие порты быстро, потому что именно за этим они и ходят.

Новый маршрут выглядит иначе. Пользователь открывает https://panel.example.com/. Этот адрес обслуживает nginx. Перед тем как отдать страницу панели, nginx спрашивает у OAuth2 Proxy: «этот запрос пришёл от человека, который уже вошёл?». Если да, запрос уходит к панели. Если нет, человека отправляют на вход к провайдеру идентификации.

Здесь полезно понять устройство вопроса. В документации OAuth2 Proxy описана интеграция с nginx через auth_request: nginx может обращаться к endpoint /auth прокси для проверки доступа, и этот endpoint возвращает только результат проверки, не проксируя сам запрос приложения (документация OAuth2 Proxy). Следствие важное: решение принимает OAuth2 Proxy, а трафик к панели по-прежнему несёт nginx. Поэтому в этой схеме три участника с тремя разными задачами: nginx отвечает за маршрутизацию, OAuth2 Proxy за ответ «пускать или нет», панель за собственную работу.

Минимальный набросок конфигурации nginx для такого сценария выглядит так. Это иллюстрация логики, а не готовый файл: адреса, порты и имена нужно подставить свои.

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

    location /oauth2/ {
        proxy_pass http://127.0.0.1:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Auth-Request-Redirect $request_uri;
    }

    location = /oauth2/auth {
        internal;
        proxy_pass http://127.0.0.1:4180;
        proxy_set_header Host $host;
        proxy_set_header Content-Length "";
        proxy_pass_request_body off;
    }

    location / {
        auth_request /oauth2/auth;
        error_page 401 = /oauth2/sign_in;
        proxy_pass http://127.0.0.1:8080;
    }
}

Обратите внимание на деталь, которая понадобится дальше: панель здесь указана как 127.0.0.1:8080. Это адрес, доступный только с самого сервера. В исходной картине панель слушала внешний интерфейс. Различие между этими двумя адресами и есть главное содержание статьи.

Что происходит в браузере при входе

Проследим путь человека шаг за шагом, потому что именно на этих шагах обычно и ломаются настройки cookie и redirect.

  1. Человек открывает https://panel.example.com/dashboard. У него нет cookie сессии.
  2. nginx делает подзапрос к проверке. Прокси отвечает отказом (код 401).
  3. Через error_page человек попадает на /oauth2/sign_in, а оттуда уходит на страницу провайдера идентификации.
  4. Провайдер просит войти и подтвердить доступ. После этого браузер возвращается на адрес обратного вызова вида https://panel.example.com/oauth2/callback.
  5. OAuth2 Proxy обменивает код на токены, создаёт свою сессию и выставляет cookie.
  6. Браузер возвращается на исходный /dashboard, уже с cookie. Теперь проверка проходит, и nginx отдаёт страницу панели.

Все три «капризных» места этого потока привязаны к одному и тому же домену. Адрес обратного вызова, который вы регистрируете у провайдера, должен совпадать с тем, который использует прокси. Cookie выставляется на домен, с которого пришёл запрос, поэтому панель и прокси должны жить под одним именем. Возврат в нужное место зависит от того, что nginx передал прокси исходный адрес запроса: в наброске выше это заголовок X-Auth-Request-Redirect. Если этот шаг потерян, человек после входа окажется на главной странице, а не там, куда шёл, и это один из самых частых симптомов.

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

Отдельно стоит сказать про схему за обратным прокси. Если TLS завершается на nginx, а прокси получает запросы по обычному HTTP, ему нужно знать настоящую внешнюю схему и хост. Иначе он соберёт адрес возврата с неправильной схемой, и провайдер его отвергнет. Поэтому в конфигурации nginx передают Host и сведения об исходном запросе, а сам прокси настраивают так, чтобы он учитывал эти заголовки только от доверенного nginx на локальном адресе. Это та же тема доверия, к которой мы вернёмся ниже, только с другой стороны.

Кого пускать: вход ещё не есть разрешение

Самая частая ошибка новичков в этой схеме не техническая, а смысловая. Человек успешно вошёл через провайдера, значит он «свой». Но успешный вход говорит лишь о том, что провайдер подтвердил личность. Он ничего не говорит о том, должен ли этот человек видеть именно вашу панель.

Если провайдер общий, например публичный сервис с миллионами аккаунтов, то без дополнительных условий в панель сможет войти любой владелец такого аккаунта. Представьте, что вы повесили на дверь замок, ключ от которого раздают всем желающим на улице. Формально замок есть. Практически толку от него немного.

Поэтому настройка допуска делится на две части, и обе нужны:

  • Кто в принципе может войти. Ограничение по домену почты, по организации у провайдера, по списку конкретных адресов или по группе. Конкретные параметры зависят от выбранного провайдера и версии прокси, поэтому их имена нужно сверять с документацией той версии, которую вы ставите.
  • Что этот человек может делать внутри. Здесь прокси уже не помощник, и это важно понимать честно.

Второй пункт стоит пояснить. Если у панели нет собственных ролей, то все, кто прошёл проверку, получают одинаковые права. Для панели, где можно только смотреть графики, это нормально. Для панели, где можно перезапускать сервисы или удалять данные, это означает, что вы расширили круг лиц с правом удаления на всю группу, которая допущена на входе. Поэтому группы стоит делать узкими и привязывать их к смыслу: отдельная группа «видят панель» и, если провайдер позволяет, отдельная для тех, кто может в ней что-то менять. Вход через прокси не заменяет внутренние права панели, а только добавляет перед ней внешний рубеж. Подробнее о границах такой схемы говорится в статье про то, чего OAuth2 Proxy не умеет вместо полноценного SSO.

Приведём условный пример рассуждения. В команде десять человек, из них трое отвечают за инфраструктуру. Панель управляет очередью задач. Если пустить всех десятерых, семеро получат возможность остановить обработку. Разумнее пустить на уровне прокси только группу инфраструктуры. Тогда остальные семеро просто не пройдут проверку и не увидят даже форму панели. Число здесь условное, оно нужно лишь затем, чтобы показать порядок мышления: сначала спросите, кому панель нужна, и только потом настраивайте провайдера.

Заголовки пользователя: кому можно верить

Многие панели умеют принимать имя пользователя из заголовка, который выставляет прокси: логин, почту, группы. Это удобно: панель показывает, кто вошёл, и пишет имя в журнал действий. Но тут и прячется вторая обходная дверь.

Заголовок в HTTP может поставить кто угодно. Если панель доверяет заголовку вроде «имя пользователя» без оговорок, то клиент, который достучался до неё напрямую, может написать туда что захочет, например имя администратора. Никакого взлома не нужно, достаточно одной строки в запросе. Поэтому правило звучит так: заголовки с личностью допустимы только тогда, когда они приходят от доверенного прокси, а запрос, пришедший не через него, до панели дойти не может.

На стороне nginx это выражается двумя действиями. Во-первых, nginx сам задаёт значения этих заголовков из результата проверки, а не передаёт то, что прислал клиент. Если клиент прислал свой заголовок с тем же именем, nginx должен его заменить или обнулить, а не пропустить дальше. Во-вторых, панель слушает только локальный адрес, так что подделать заголовок могут лишь те, кто уже находится на самом сервере.

Практическая проверка проста. Отправьте запрос к домену с поддельным заголовком пользователя и без cookie. Правильный результат: вас перенаправят на вход или откажут, а подделанное имя нигде не появится. Если же панель показывает вас под выдуманным именем, значит, nginx пересылает чужие заголовки, и это нужно исправлять до всего остального.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Закрываем прямой адрес

Вернёмся к главному. Пока панель слушает внешний интерфейс, всё, что мы построили, обходится одним адресом: http://IP-сервера:8080/. Ни cookie, ни провайдер, ни группы в этом маршруте не участвуют, потому что он просто не проходит через nginx.

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

  1. Привязка панели к локальному адресу. Самый надёжный шаг. Приложение настраивают так, чтобы оно слушало 127.0.0.1, а не все интерфейсы. Тогда снаружи порт 8080 просто не принимает соединений. Как именно это задаётся, зависит от конкретной панели: обычно это параметр адреса прослушивания в её конфигурации или аргумент запуска. Если панель запущена в контейнере, внимание стоит уделить тому, как публикуется порт: публикация на все интерфейсы создаёт ту же проблему, а привязка публикации к локальному адресу решает её.
  2. Правила межсетевого экрана. Даже если приложение слушает всё, экран на сервере может закрыть порт панели от внешнего мира, оставив открытыми только те порты, которые нужны: обычно 443, а для администрирования SSH. Это второй рубеж на случай, если настройка приложения когда-нибудь изменится при обновлении.
  3. Отказ nginx по умолчанию. Часто на том же IP отвечает nginx и для запросов с неизвестным именем хоста, и он может отдать совсем не то. Полезно иметь блок сервера по умолчанию, который закрывает соединение или возвращает пустой ответ для любых имён, кроме вашего. Тогда обращение по голому IP на 443 не покажет ни панель, ни чужой сайт.

Эти меры дополняют друг друга. Привязка к локальному адресу убирает саму возможность соединения, межсетевой экран страхует от ошибки в настройке, а блок по умолчанию убирает случайные утечки. Для VPS с root-доступом, какие предлагает MAATRIX, всё это делается стандартными средствами самой системы, никаких особых функций платформы для этого не требуется: полный контроль над сервером даёт возможность выбрать, какие порты слушает каждый процесс.

Ресурсы для такой схемы скромные. Прокси и nginx сами по себе невелики, основная нагрузка идёт от панели. Для небольшой внутренней панели обычно хватает минимального плана, например Ba с 1 vCPU и 2 ГБ памяти, но это только ориентир. Реальную потребность определяет сама панель и число одновременных пользователей, поэтому сначала измерьте, а потом решайте, нужен ли план побольше: Uab, Heka и выше.

Проверка без доверия: три запроса снаружи

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

  1. Прямой порт. Обратитесь к IP-сервера:8080. Ожидаемый результат: соединение отклонено или время ожидания истекло. Любой ответ с содержимым панели означает, что обход жив.
  2. Домен без cookie. Запросите https://panel.example.com/ без сессии, например утилитой командной строки. Ожидаемый результат: перенаправление на вход, а не страница панели.
  3. Домен с поддельным заголовком. Повторите предыдущий запрос, добавив заголовок с чужим именем пользователя. Ожидаемый результат тот же: вход, а не доступ.

Для примера команды могут выглядеть так:

curl -i --max-time 5 http://IP-сервера:8080/
curl -i https://panel.example.com/
curl -i -H "X-Forwarded-User: admin" https://panel.example.com/

Имя заголовка в третьей команде подставьте то, которому доверяет ваша панель. Если вы не знаете, какое это имя, загляните в её документацию: от него зависит, насколько проверка осмысленна.

Есть ещё одна тонкость. Проверяйте не только IPv4. Если у сервера есть публичный IPv6-адрес, а правила экрана и привязка настроены только для IPv4, панель может оказаться открытой по IPv6. Это классическая ситуация: человек закрыл один адрес и забыл, что у машины их два.

Что не повторяет браузерный поток

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

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

  • Webhook от внешнего сервиса. Он отправляет запрос и не умеет входить через провайдера. Такой маршрут нужно либо вынести из-под проверки на отдельный путь с другой защитой, например секретом в адресе или подписью запроса, либо принимать на отдельном адресе.
  • Проверки доступности. Мониторинг, который опрашивает панель, получит перенаправление и может решить, что всё работает, хотя в действительности видит только страницу входа. Для него полезно сделать отдельный узкий путь, который возвращает простой ответ о состоянии и не раскрывает данных.
  • Скрипты администратора. Здесь есть выбор: ходить к панели локально, на её внутренний адрес, с самого сервера по SSH, или использовать механизм, который поддерживает ваша версия прокси для токенов. Если вы рассчитываете на второе, сверьтесь с документацией установленной версии: поддержка и параметры отличаются.

Каждое исключение из-под проверки является новой дверью, и относиться к нему нужно так же строго, как к основной. Правило простое: исключение должно быть узким, то есть один путь и один метод, а не «весь API», и защищено иным способом. Список таких исключений стоит держать коротким и записанным в одном месте, иначе через год никто не вспомнит, почему путь открыт.

Потоковые ответы и таймауты: почему панель то зависает

Если панель показывает живые журналы или события, то после добавления nginx вы можете заметить, что обновления приходят пачками или обрываются. Причина обычно в буферизации. В документации nginx сказано, что proxy_buffering управляет буферизацией ответов от upstream, и при её отключении данные передаются клиенту по мере получения; а ответ с заголовком X-Accel-Buffering тоже может управлять буферизацией, если это не запрещено конфигурацией (документация nginx). Для страницы с живым потоком журнала разумно отключить буферизацию именно на нужном пути, а не на всём сайте.

Про таймауты стоит помнить ещё одну деталь: proxy_read_timeout относится к интервалу между двумя последовательными чтениями, а не к общей длительности ответа (там же). Поэтому долгий поток, в котором данные идут регулярно, не оборвётся по этому таймауту, а молчащее соединение оборвётся. Если панель присылает события редко, например раз в минуту, а таймаут стоит по умолчанию, соединение закроется между событиями. Тогда либо увеличивают значение для этого пути, либо заставляют панель посылать периодические пустые сообщения.

Какое это имеет отношение к безопасности? Прямое: когда панель «глючит», люди начинают искать обходные пути, и самый короткий из них известен, это порт панели напрямую. Работающая схема надёжнее, чем та, которую хочется обойти, поэтому поведение потоков стоит настроить сразу, а не после жалоб.

Таблица симптомов

Типичные признаки и их вероятные причины в схеме nginx + OAuth2 Proxy
Что видноВероятная причинаГде смотреть
Бесконечные перенаправления после входаCookie не сохраняется: не совпал домен или схемаВкладка сети в браузере, настройки cookie прокси
Провайдер жалуется на адрес возвратаЗарегистрированный и фактический адрес различаютсяНастройки приложения у провайдера, внешний адрес прокси
После входа открывается главная, а не нужная страницаnginx не передал исходный адрес запросаЗаголовок с адресом возврата в конфигурации nginx
Вошёл человек, которому доступ не положенНе заданы ограничения на допустимых пользователей или группыПараметры допуска прокси и настройки провайдера
Панель открывается по IP и портуПриложение слушает внешний интерфейс или порт открытАдрес прослушивания, правила экрана, IPv6
Живой журнал приходит пачкамиБуферизация ответа в nginxНастройки буферизации на конкретном пути
Скрипт получает HTML вместо данныхКлиент не проходит браузерный входОтдельный узкий маршрут или иной способ защиты

Порядок работы, который не оставляет дыр

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

Если порядок обратный, то есть панель сначала закрыли, а прокси ещё не готов, люди теряют доступ и начинают просить его вернуть, а в спешке порт открывают обратно и забывают закрыть. Лучше пройти путь спокойно, чем потом разбираться, кто и зачем открыл порт.

Если вы только планируете разместить такую связку, подойдёт обычный VPS с root-доступом: на нём вы сами выбираете, какие процессы и на каких адресах слушают. Платформа MAATRIX предлагает NVMe-диски, процессоры AMD EPYC и размещение в нескольких странах, но наличие нужной локации и образа системы стоит уточнить в форме заказа. Сам по себе сервер ничего не закрывает за вас: закрытый порт появляется только тогда, когда вы его закрыли.

Последняя проверка: старый адрес

Вся эта работа сводится к одной небольшой проверке, и её полезно делать не только в день настройки, но и после каждого обновления панели, контейнера или правил экрана. Возьмите тот самый адрес, которым пользовались до внедрения защиты: http://IP-сервера:8080/, ту самую закладку, которая лежит у половины команды. Откройте его с чужой сети, например с телефона на мобильном интернете, не из офиса и не по VPN.

Если страница не открывается, соединение отклонено, а домен требует вход, работа закончена. Если же панель послушно показывает себя, значит, всё остальное было репетицией: красивый вход, группы, cookie и перенаправления защищают лишь тех, кто ходит парадным входом. Старый IP отвечает на единственный вопрос, который имеет значение: можно ли попасть внутрь, не встречаясь с проверкой. Пока ответ «можно», задача не решена, сколько бы настроек ни было сделано.