Настройка SSO для любого приложения через oauth2-proxy
Узнайте, как внедрить SSO в приложения без встроенной авторизации с помощью oauth2-proxy. Настройте Forward Auth в Nginx или Traefik, чтобы избежать ошибок с cookie и OIDC.
Forward auth: как добавить SSO в приложение без собственной системы авторизации
oauth2-proxy позволяет реализовать единый вход (SSO) для приложения, в котором нет встроенной системы авторизации. Это работает за счет того, что обратный прокси-сервер перед приложением перехватывает каждый запрос, запрашивает у oauth2-proxy информацию о наличии валидной сессии и передает запрос вышестоящему приложению только в случае положительного ответа. Код приложения при этом не меняется, так как оно не участвует в проверке.
Проверка представляет собой дополнительный HTTP-запрос. Прокси-сервер отправляет копию заголовков входящего запроса в /oauth2/auth и считывает код ответа. Код 202 означает, что у пользователя есть сессия, поэтому прокси пересылает исходный запрос в приложение. Код 401 означает отсутствие сессии, и в этом случае прокси перенаправляет браузер в /oauth2/sign_in, где инициируется процесс входа через OpenID Connect (OIDC) у вашего провайдера идентификации. OIDC — это уровень идентификации, построенный поверх OAuth 2.0, а в качестве провайдера выступает любая система, которую вы уже используете для авторизации.
У этого шаблона есть свое название в каждом обратном прокси. В Nginx эта директива называется auth_request. В Traefik это middleware forwardAuth. В Caddy используется forward_auth. Сервис, отвечающий на подзапросы, также может быть заменен. oauth2-proxy является наиболее распространенным выбором, так как он поддерживает стандартный OIDC и не требует собственной базы данных.
Определите границы доверия перед написанием конфигурации
После успешной проверки oauth2-proxy возвращает данные пользователя в заголовках ответа, а обратный прокси копирует их в запрос к upstream-серверу. При включенном set_xauthrequest вы получаете X-Auth-Request-User и X-Auth-Request-Email. Приложение считывает эти заголовки и доверяет им.
Это вся модель безопасности, поэтому важно четко понимать последствия. Любой субъект, способный установить TCP-соединение с портом приложения, может самостоятельно задать эти заголовки и выдать себя за любого пользователя. Одиночный curl -H "X-Auth-Request-Email: admin@example.com" http://app-host:3000/ означает полный обход защиты, если запрос достигает приложения напрямую.
Следовательно, приложение не должно быть доступно ниоткуда, кроме как через прокси. В Docker Compose удалите маппинг ports: из сервиса приложения и оставьте его только во внутренней сети, чтобы только контейнер прокси мог подключаться к нему. На обычном сервере привяжите приложение к 127.0.0.1:3000 вместо 0.0.0.0:3000. Затем проверьте, что именно вы выставили наружу:
sudo ss -tlnp | grep 3000Строка с 0.0.0.0:3000 означает, что приложение отвечает на публичном IP-адресе и ваш шлюз является лишь декорацией. 127.0.0.1:3000 — это то, что вам нужно. Правило межсетевого экрана является полезным вторым уровнем защиты, но адрес привязки — это то, что останется в силе, даже если другой инструмент сбросит ваш набор правил.
Установка oauth2-proxy
По состоянию на август 2026 года актуальным релизом является v7.15.3, выпущенный в июне 2026 года. Установите бинарный файл и проверьте загрузку:
cd /tmp
curl -fsSLO https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.15.3/oauth2-proxy-v7.15.3.linux-amd64.tar.gz
curl -fsSLO https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.15.3/oauth2-proxy-v7.15.3.linux-amd64.tar.gz-sha256sum.txt
sha256sum -c oauth2-proxy-v7.15.3.linux-amd64.tar.gz-sha256sum.txt
tar -xzf oauth2-proxy-v7.15.3.linux-amd64.tar.gz
sudo install -m 755 oauth2-proxy-v7.15.3.linux-amd64/oauth2-proxy /usr/local/bin/oauth2-proxy
oauth2-proxy --versionsha256sum -c должен вывести строку, заканчивающуюся на OK. Если выводится FAILED, остановитесь и загрузите файл заново, вместо того чтобы запускать бинарный файл.
В Docker используется образ quay.io/oauth2-proxy/oauth2-proxy, и вам следует зафиксировать тег: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3. Использование latest превращает плановую операцию docker compose pull в незапланированное обновление процесса, который защищает все приложения на сервере.
Генерация секрета для cookie
Сессионная cookie шифруется, а cookie_secret выступает в роли ключа. Его длина должна составлять ровно 16, 24 или 32 байта, так как он используется в качестве ключа AES (Advanced Encryption Standard). При любой другой длине oauth2-proxy откажется запускаться и выдаст ошибку при старте с указанием на cookie secret.
openssl rand -base64 32 | tr -- '+/' '-_'Использование tr не является формальностью. Эта команда преобразует стандартный base64 в алфавит, безопасный для URL, что позволяет значению корректно передаваться через оболочку, файл переменных окружения и HTTP-заголовки без проблем с экранированием.
Существует два правила для этого значения. Используйте уникальный секрет для каждого развертывания. Если вы запускаете несколько экземпляров oauth2-proxy для одного домена, задайте им одинаковый секрет, так как cookie, зашифрованная одним экземпляром, должна быть доступна для чтения остальным.
Настройка oauth2-proxy
Храните параметры в файле, а не в длинной командной строке, чтобы client secret не отображался в выводе ps.
# /etc/oauth2-proxy/oauth2-proxy.cfg
http_address = "127.0.0.1:4180"
reverse_proxy = true
provider = "oidc"
oidc_issuer_url = "https://id.example.com/application/o/myapp/"
client_id = "REPLACE_ME"
client_secret = "REPLACE_ME"
redirect_url = "https://app.example.com/oauth2/callback"
cookie_secret = "REPLACE_ME"
cookie_secure = true
cookie_domains = [".example.com"]
whitelist_domains = [".example.com"]
email_domains = ["*"]
set_xauthrequest = true
upstreams = ["static://202"]Параметр reverse_proxy = true указывает oauth2-proxy доверять заголовкам X-Forwarded-* от прокси, стоящего перед ним. Без этого oauth2-proxy воспринимает адрес самого прокси как адрес клиента и может неверно определить, был ли запрос выполнен по HTTPS.
Параметр upstreams = ["static://202"] заставляет oauth2-proxy отвечать кодом 202 на аутентифицированный запрос и не проксировать трафик, что идеально подходит для forward auth, так как проксированием занимается reverse proxy. Другой вариант развертывания предполагает размещение oauth2-proxy непосредственно в пути запроса с использованием upstreams = ["http://127.0.0.1:3000"] и без auth_request. Это проще для одного приложения, но не масштабируется до десяти.
Параметр email_domains = ["*"] разрешает доступ для всех адресов, которые аутентифицирует ваш провайдер. Ограничьте его своим доменом или, что еще лучше, настройте ограничение доступа через привязку к группе на стороне провайдера, так как именно там вы уже управляете пользователями.
Запустите его через systemd от имени отдельного пользователя:
# /etc/systemd/system/oauth2-proxy.service
[Unit]
Description=oauth2-proxy
After=network-online.target
Wants=network-online.target
[Service]
User=oauth2-proxy
Group=oauth2-proxy
ExecStart=/usr/local/bin/oauth2-proxy --config=/etc/oauth2-proxy/oauth2-proxy.cfg
Restart=on-failure
ProtectSystem=strict
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetsudo useradd --system --no-create-home --shell /usr/sbin/nologin oauth2-proxy
sudo install -d -m 750 /etc/oauth2-proxy
sudo chown -R oauth2-proxy:oauth2-proxy /etc/oauth2-proxy
sudo chmod 600 /etc/oauth2-proxy/oauth2-proxy.cfg
sudo systemctl daemon-reload
sudo systemctl enable --now oauth2-proxy
curl -s http://127.0.0.1:4180/pingВывод /ping с OK означает, что процесс запущен и загрузил конфигурацию. Это встроенный health-endpoint сервиса oauth2-proxy, который никогда не запрашивает сессию. Если ответа нет, изучите journalctl -u oauth2-proxy -n 50, так как неверный URL issuer или неверная длина cookie secret приводят к сбою при запуске, о чем сервис сообщает в логах.
Регистрация URI перенаправления у вашего провайдера
Создайте OIDC-приложение у вашего провайдера и установите его URI перенаправления в точном соответствии с redirect_url из конфигурации: https://app.example.com/oauth2/callback. «В точном соответствии» означает, что схема, хост, порт и путь должны совпадать символ в символ. Наличие или отсутствие завершающего слэша делает URI другим.
Это самая частая причина сбоев во всей настройке, и она проявляется еще до того, как в процесс включается oauth2-proxy. Провайдер отклоняет запрос на авторизацию и выводит собственную страницу ошибки, поэтому в логах oauth2-proxy ничего не появляется. Признак проблемы находится в адресной строке: браузер всё ещё находится на домене вашего провайдера, а строка запроса содержит error=invalid_request или прямо указывает на redirect_uri. Если вы видите это, исправляйте запись приложения у провайдера, а не конфигурацию прокси.
Скопируйте URL издателя (issuer URL) из панели провайдера, вместо того чтобы вводить его вручную. oauth2-proxy добавляет /.well-known/openid-configuration к oidc_issuer_url и при запуске запрашивает этот документ обнаружения. Проверьте его самостоятельно:
curl -s https://id.example.com/application/o/myapp/.well-known/openid-configuration | head -c 400JSON, содержащий ключ authorization_endpoint, означает, что URL издателя указан верно. Ошибка 404 или HTML-страница с ошибкой означают, что URL неверный, и oauth2-proxy не сможет запуститься из-за той же ошибки 404. Если вы еще не выбрали провайдера, сравнение Keycloak, Authentik и Zitadel поможет оценить плюсы и минусы, а руководство по развертыванию Authentik в качестве собственного SSO-сервера подробно описывает настройку провайдера для данного сценария.
Nginx: auth_request
Nginx выполняет предварительную аутентификацию с помощью auth_request, который инициирует внутренний подзапрос и выполняет ветвление в зависимости от его кода состояния.
# in the http context, next to your other maps
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name app.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 {
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-Forwarded-Uri $request_uri;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
location / {
auth_request /oauth2/auth;
error_page 401 = @oauth2_signin;
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $email $upstream_http_x_auth_request_email;
proxy_set_header X-User $user;
proxy_set_header X-Email $email;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location @oauth2_signin {
return 302 /oauth2/sign_in?rd=$scheme://$host$request_uri;
}
}Три детали в этой конфигурации имеют важное значение. proxy_pass_request_body off с пустым значением Content-Length предотвращает копирование Nginx тела каждого POST-запроса в подзапрос, что важно, так как oauth2-proxy не считывает его. При загрузке файла значение по умолчанию привело бы к отправке файла дважды.
Пара auth_request_set $auth_cookie и add_header Set-Cookie передаёт обновлённый cookie сессии обратно в браузер. Если их не указать, cookie_refresh будет молча бездействовать, так как Nginx отбрасывает заголовки Set-Cookie из подзапроса, и браузер сохранит старое значение до истечения срока действия сессии.
error_page 401 = @oauth2_signin превращает неудачную проверку в перенаправление на страницу входа. Без этой директивы неавторизованный посетитель получит пустую страницу 401 Authorization Required без возможности продолжить работу.
Всегда проверяйте конфигурацию перед перезагрузкой:
sudo nginx -t && sudo systemctl reload nginxЕсли приведённые директивы вам незнакомы, анатомия конфигурации reverse proxy в nginx описывает базовый уровень, на котором строится эта настройка.
Traefik: middleware forwardAuth
Для выполнения этой задачи Traefik требует два middleware. Первый выполняет проверку. Второй преобразует ответ 401 в редирект браузера.
# dynamic configuration
http:
middlewares:
oauth-auth:
forwardAuth:
address: https://oauth.example.com/oauth2/auth
trustForwardHeader: true
oauth-errors:
errors:
status:
- "401-403"
service: oauth-backend
query: "/oauth2/sign_in?rd={url}"
statusRewrites:
"401": 302Подключите оба к роутеру, который стоит перед вашим приложением, и опубликуйте oauth2-proxy на отдельном роутере по адресу oauth.example.com. Это необходимо, так как браузер должен иметь доступ к /oauth2/sign_in и /oauth2/callback, не проходя проверку.
statusRewrites — это отображение кода 401 на 302, которое часто упускают из виду. Без него Traefik возвращает редирект на страницу входа со статусом 401, браузер не переходит по нему, и пользователь видит страницу, содержащую только слово Found..
trustForwardHeader: true передает исходный хост и URI в oauth2-proxy, которому они нужны для формирования значения rd, возвращающего пользователя на запрошенную страницу. Настройте whitelist_domains так, чтобы он охватывал и этот хост, иначе oauth2-proxy отбросит параметр rd как риск открытого редиректа, и после входа все будут попадать на /. Обычно один экземпляр Traefik обслуживает сразу несколько приложений, а в статье маршрутизация нескольких приложений Docker Compose через один экземпляр Traefik показана схема роутера, в которую встраивается данное решение.
Caddy: forward_auth
app.example.com {
handle /oauth2/* {
reverse_proxy oauth2-proxy.internal:4180 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Uri {uri}
}
}
handle {
forward_auth oauth2-proxy.internal:4180 {
uri /oauth2/auth
header_up X-Real-IP {remote_host}
copy_headers X-Auth-Request-User X-Auth-Request-Email
@error status 401
handle_response @error {
redir * /oauth2/sign_in?rd={scheme}://{host}{uri}
}
}
reverse_proxy upstream.internal:3000
}
}Порядок имеет значение. Блок /oauth2/* должен идти первым и не содержать forward_auth, так как неавторизованный пользователь должен иметь доступ к путям входа и обратного вызова. Если разместить проверку перед этими путями, произойдет бесконечный цикл перенаправлений на страницу входа, пока браузер не прервет соединение.
copy_headers передает данные об идентификации в вышестоящий (upstream) запрос; этот механизм работает, только если oauth2-proxy запущен с флагом set_xauthrequest = true. Выбор между прокси-серверами — отдельная задача, и сравнение Nginx, Caddy и Traefik поможет в ней разобраться.
Почему происходит зацикливание на странице входа?
Вы авторизуетесь у провайдера, он перенаправляет вас обратно, а oauth2-proxy сразу отправляет вас к провайдеру снова. Зацикливание означает, что запрос обратного вызова (callback) поступил без cookie, который oauth2-proxy установил при отправке. В логах указана причина:
No cookies were found in OAuth callback.или, если другой cookie дошел, а нужный — нет:
Cookies were found in OAuth callback, but none was a CSRF cookie.CSRF — это межсайтовая подделка запроса (cross-site request forgery). Этот cookie нужен, чтобы связать обратный вызов с процессом входа, который его инициировал. В браузере эта ошибка выглядит так:
Login Failed: Unable to find a valid CSRF token. Please try again.Проверьте эти четыре причины по порядку.
cookie_secure = trueпри обращении к сайту по обычному HTTP. Браузер не сохранит cookie с флагомSecureдля источникаhttp://, поэтому он не будет отправлен обратно. Настройте TLS (transport layer security) должным образом или используйтеcookie_secure = falseтолько при тестировании на localhost.- Значение
cookie_domainsне охватывает имя хоста в адресной строке..example.comохватываетapp.example.com, но никак не влияет наapp.example.net. - Браузер отбрасывает cookie. Строгие настройки приватности или блокировщики сторонних cookie могут удалять
_oauth2_proxy_csrfмежду исходящим редиректом и обратным вызовом. - Рассинхронизация времени. Если системное время сервера сильно отличается от времени провайдера, значения
iatиexpв ID-токене выходят за пределы допустимого окна, и сессия отклоняется при получении.timedatectlдолжен сообщать оSystem clock synchronized: yes.
Вместо догадок отследите процесс на стороне сервера:
sudo journalctl -u oauth2-proxy -fОткройте приложение в режиме инкогнито. Каждый запрос логируется с указанием статуса, поэтому обратный вызов, за которым сразу следует новый редирект к провайдеру, будет зафиксирован как зацикливание.
Пути, которые должны обходить авторизацию: API, вебхуки и вебсокеты
Forward auth предполагает наличие у браузера cookie. Клиенты без браузера не смогут пройти проверку.
API-клиент, отправляющий Authorization: Bearer <token>, не имеет cookie, поэтому он получает редирект 302 на страницу входа вашего провайдера и затем пытается распарсить HTML как JSON. Есть два корректных решения. Настройка skip_jwt_bearer_tokens = true позволяет oauth2-proxy принимать валидный JWT (JSON web token) bearer-токен от того же эмитента, что подходит, если ваши API-клиенты уже получают токены от провайдера. В противном случае исключите путь:
skip_auth_routes = [
"^/api/",
"POST=^/webhook/",
"GET=^/healthz$"
]Каждое значение — это регулярное выражение, которое сопоставляется с нормализованным путем, при необходимости с префиксом HTTP-метода и =. POST=^/webhook/ оставляет обработчик вебхука открытым для POST-запросов, в то время как пользователь, переходящий по тому же пути в браузере, всё равно попадает на страницу входа. Каждая такая запись — это брешь в защите, поэтому ограничивайте выражения символом ^ и делайте их максимально узкими, насколько позволяет клиент.
Вебсокеты — это случай, в котором часто ошибаются. Запрос на upgrade — это обычный HTTP GET, содержащий те же cookie, что и любой другой запрос, поэтому он проходит проверку в штатном режиме и не требует исключений. Проблема возникает при проксировании. Без заголовков Upgrade и Connection в защищенной локации процесс upgrade никогда не завершается, и клиент приложения бесконечно повторяет попытки, выводя сообщение WebSocket connection ... failed в консоль браузера. Исключение пути здесь не поможет, так как запрос уже был авторизован.
Существует одно реальное ограничение. Проверка выполняется один раз, в момент upgrade. Вебсокет, который остается открытым часами, не перепроверяется, поэтому удаление пользователя у провайдера не приведет к закрытию уже установленного соединения. Чтобы прервать активные подключения, перезапустите приложение.
Хранилище сессий и проблема чрезмерного размера cookie
По умолчанию вся сессия хранится внутри cookie, зашифрованного с помощью cookie_secret. Это позволяет oauth2-proxy оставаться stateless и не требует дополнительных сервисов. У этого подхода есть ограничение: браузеры ограничивают размер cookie примерно 4 KB. Когда ID token содержит длинный список групп (group claims), oauth2-proxy разбивает сессию на части _oauth2_proxy_0, _oauth2_proxy_1 и так далее. Если частей становится много, заголовки запроса увеличиваются настолько, что nginx возвращает 400 Request Header Or Cookie Too Large еще до того, как приложение получит запрос.
В таких случаях перенесите хранение сессий на сторону сервера:
session_store_type = "redis"
redis_connection_url = "redis://127.0.0.1:6379"В этом случае браузер хранит только короткий тикет, а зашифрованная сессия находится в Redis. Платой за это становится необходимость поддерживать работу дополнительного сервиса: если Redis выйдет из строя, все сессии станут недействительными, и все пользователи будут принудительно разлогинены. У cookie store есть свой недостаток: два запроса, одновременно обновляющие одну и ту же сессию, могут конфликтовать, что приведет к необходимости повторного входа в систему.
Чего не дает forward auth
Это пропуск на входе. Он не является авторизацией внутри приложения, и эта разница определяет, подходит ли данный подход для вашего случая.
Как только пользователь проходит проверку, приложение видит то же, что и всегда. Если у приложения есть собственные роли, forward auth не заполняет их, если только приложение не поддерживает аутентификацию на основе заголовков и не умеет сопоставлять заголовок с учетной записью. Grafana умеет это делать через настройки auth.proxy. Большинство self-hosted приложений этого не умеют, поэтому для приложения каждый, кто прошел через «ворота», является одной и той же личностью, и эта личность часто обладает правами администратора.
Этот метод также не защищает собственные API-токены приложения. Личный токен доступа, выданный приложением, проходит аутентификацию в приложении, а не в oauth2-proxy, поэтому токен перестает работать, как только перед ним оказывается «шлюз». Исключите путь API из проверки, чтобы вернуть его работоспособность, и этот токен станет единственным средством защиты данного пути. Теперь вы используете две системы аутентификации для одного сервиса, и SSO покрывает только одну из них.
Отзыв доступа — третий пробел. Удаление пользователя у вашего провайдера останавливает новые входы и обновление токена, которое выполняет cookie_refresh, но существующий сессионный cookie остается действительным до истечения срока его действия. cookie_expire по умолчанию составляет 168 часов, что дает неделю доступа тому, кого вы только что удалили. Установите cookie_refresh на меньшее значение, например, на один час, чтобы отзыв доступа произошел в рамках этого окна.
Аудиторский след также обрывается на входе. oauth2-proxy регистрирует, кто и когда прошел через него. Приложение регистрирует безымянную сессию. Если вам нужно ответить на вопрос, кто изменил настройку, идентификация через заголовки в приложении — это минимум, а реальные учетные записи для каждого пользователя — это честный ответ.
Когда оплата SSO-тарифа является лучшим решением
Forward auth — подходящий инструмент, если у приложения вообще нет системы входа или используется один общий пароль, а вам нужно централизованное управление доступом. Настройка занимает несколько часов и требует запуска одного дополнительного процесса, при этом решение работает с любым приложением, поддерживающим HTTP.
Это неподходящий инструмент, если разным пользователям нужны разные права внутри одного приложения. Прокси-шлюз не может реализовать логику вида «Ана может редактировать дашборды, а Бо может только просматривать их». Если вендор предлагает SSO в составе платного тарифа, вы фактически покупаете механизм сопоставления групп и ролей. Попытка воссоздать этот функционал с помощью заголовков и правил проксирования будет менее надежной, чем покупка подписки. Стоит ознакомиться с моделью ценообразования для SSO-тарифов, прежде чем принимать решение.
Есть еще две ситуации, указывающие на необходимость покупки SSO. Во-первых, требования комплаенса, требующие ведения аудита действий каждого пользователя внутри приложения: логи доступа прокси не будут приняты в качестве доказательства. Во-вторых, любые мобильные или десктопные клиенты, которые не поддерживают передачу браузерных cookies, будут конфликтовать с механизмом аутентификации при каждом запросе.
FAQ
Что такое forward auth?
Forward auth — это шаблон, при котором обратный прокси-сервер запрашивает у отдельного сервиса аутентификации разрешение для каждого входящего запроса перед тем, как передать его дальше. Прокси отправляет заголовки запроса на эндпоинт, такой как /oauth2/auth, и считывает код ответа. Код 202 означает разрешение, поэтому исходный запрос передаётся приложению. Код 401 означает отсутствие сессии, поэтому прокси перенаправляет браузер на страницу входа. Nginx реализует это с помощью директивы auth_request, Traefik — через middleware forwardAuth, а Caddy — с помощью forward_auth.
Почему oauth2-proxy перенаправляет меня на страницу входа в бесконечном цикле?
Запрос обратного вызова (callback) поступил в oauth2-proxy без CSRF-куки, поэтому oauth2-proxy начинает процесс заново. В логе сервера отображается No cookies were found in OAuth callback., а в браузере — Login Failed: Unable to find a valid CSRF token. Please try again.. Чаще всего причина кроется в cookie_secure = true на сайте, к которому браузер обратился по обычному HTTP, так как браузер не сохранит куку Secure для источника http://. Следующая по частоте причина — значение cookie_domains, которое не охватывает имя хоста в адресной строке.
Как пропустить API-клиент или вебхук через oauth2-proxy?
Используйте skip_auth_routes с регулярным выражением, привязанным к началу строки, при необходимости ограничив его одним HTTP-методом, например POST=^/webhook/. Если ваши API-клиенты уже имеют JWT, выданные тем же провайдером, skip_jwt_bearer_tokens = true примет эти токены вместо куки и сохранит защиту пути. Всё, что указано в skip_auth_routes, доступно без аутентификации для всех, поэтому делайте каждое выражение максимально узким, насколько это позволяет вызывающая сторона.
Предоставляет ли oauth2-proxy приложению права доступа для конкретных пользователей?
Нет. Это шлюз, а не система авторизации. Он решает, кто получит доступ к приложению, но для самого приложения все пользователи выглядят одинаково, если только оно не считывает заголовки идентификации и не сопоставляет их с учётными записями. Grafana умеет это делать через настройки auth.proxy. Большинство self-hosted приложений не обладают такой функцией, поэтому каждый человек, прошедший через шлюз, использует ту единственную учётную запись, под которой запущено приложение.
Может ли кто-то обойти oauth2-proxy, самостоятельно установив заголовок идентификации?
Да, если у него есть прямой доступ к приложению. Идентификационные данные приходят в виде обычного заголовка, например X-Auth-Request-Email, и приложение доверяет всему, что получает. Любой, кто может установить соединение с портом приложения, может отправить этот заголовок и представиться любым пользователем. Привязывайте приложение к 127.0.0.1 или держите его во внутренней сети Docker без опубликованных портов, проверяя это с помощью sudo ss -tlnp.