В этом уроке разберём четыре механизма от простого к сложному: фильтр по IP (allow/deny), пароль (auth_basic), внешнюю авторизацию через бэкенд (auth_request - это и есть современный forward-auth для SSO) и клиентские сертификаты (mTLS). И, что важнее, научимся их комбинировать через satisfy, потому что в реальности один механизм почти никогда не хватает.
nginx allow deny: фильтр по IP и подсетям
Самый дешёвый способ закрыть доступ - пускать только с известных адресов. За это отвечает модуль ngx_http_access_module с директивами allow и deny. Синтаксис простой: адрес, подсеть в CIDR, all или unix-сокет.
Код: Выделить всё
location /admin/ {
allow 203.0.113.10; # конкретный IP офиса
allow 10.0.0.0/8; # вся внутренняя сеть
allow 2001:db8::/32; # IPv6 тоже работает
deny all; # все остальные - 403
proxy_pass http://admin_backend;
}
Директивы наследуются от http к server к location, но - внимание - если в location появилась хоть одна своя allow/deny, она ПОЛНОСТЬЮ перекрывает унаследованный набор, а не дополняет его. Это типичная грабля: ты добавил один allow в location, думая, что он плюсуется к глобальному, а на деле обнулил всю внешнюю политику.
Теперь самое важное и недооценённое. allow/deny смотрит на $remote_addr - адрес того, кто открыл TCP-соединение с nginx. Если перед nginx стоит балансировщик, CDN или ещё один прокси, то $remote_addr - это адрес ЭТОГО прокси, а не реального клиента. Твой allow по офисному IP не сработает никогда, потому что nginx видит только адрес фронтового LB. Лечится это модулем realip (отдельный большой урок), который подменяет $remote_addr на адрес из X-Forwarded-For. Но запомни связку прямо сейчас: без корректно настроенного realip любой контроль по IP за прокси - это иллюзия. И ещё опаснее обратное: если ты доверяешь X-Forwarded-For от кого попало (set_real_ip_from 0.0.0.0/0), клиент сам подставит себе нужный IP в заголовок и обойдёт твой allow. Доверяй realip только известным сетям своих прокси и CDN.

nginx auth_basic: пароль за тридцать секунд
IP-фильтр хорош, но адреса меняются, а сотрудники работают из дома. Тогда добавляем пароль. nginx basic auth - это модуль ngx_http_auth_basic_module, ему нужны две вещи: директива auth_basic с текстом подсказки и файл с логинами-паролями.
Файл создаём утилитой htpasswd из пакета apache2-utils (или httpd-tools на RHEL):
Код: Выделить всё
# создать файл и первого пользователя (-c создаёт файл, -B = bcrypt)
htpasswd -B -c /etc/nginx/.htpasswd alice
# добавить ещё одного БЕЗ -c, иначе файл перезапишется
htpasswd -B /etc/nginx/.htpasswd bob
Конфиг nginx auth_basic выглядит так:
Код: Выделить всё
location /admin/ {
auth_basic "Restricted Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://admin_backend;
}
nginx auth_request: внешняя авторизация и forward-auth для SSO
А вот это - самый мощный и современный механизм. Идея: перед тем как проксировать запрос на бэкенд, nginx делает внутренний субзапрос к отдельному сервису авторизации и спрашивает "этому пользователю можно?". Если сервис ответил 2xx - запрос идёт дальше. Если 401 или 403 - nginx сразу отдаёт отказ, бэкенд даже не трогается. Это паттерн forward-auth, на нём построены oauth2-proxy, Authelia, Authentik, Vouch - то есть весь современный self-hosted SSO.
Работает это через модуль ngx_http_auth_request_module (его нужно собрать в nginx, в стандартных пакетах он есть). Минимальная схема:
Код: Выделить всё
location / {
auth_request /_auth; # спросить разрешение
auth_request_set $user $upstream_http_x_user; # забрать заголовок из ответа
proxy_set_header X-Authenticated-User $user; # пробросить на бэкенд
proxy_pass http://app_backend;
}
# внутренний эндпойнт, наружу не доступен
location = /_auth {
internal;
proxy_pass http://127.0.0.1:4180/oauth2/auth;
proxy_pass_request_body off; # тело субзапросу не нужно
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
Теперь грабли, на которых горят все:
- Тело и метод субзапроса. auth_request всегда шлёт субзапрос методом GET (наследует метод исходного запроса для тела, но семантически это проверка) и по умолчанию тащит тело оригинального запроса. Для аутентификации тело не нужно и только мешает - поэтому proxy_pass_request_body off и обнуление Content-Length. Иначе большой POST-аплоад будет целиком гоняться ещё и в auth-сервис.
- Возврат заголовков. Сам auth_request НЕ пробрасывает заголовки из ответа авторизатора на бэкенд автоматически. Чтобы достать, например, имя пользователя, которое сервис вернул в заголовке X-User, используешь auth_request_set $user $upstream_http_x_user - префикс $upstream_http_ плюс имя заголовка в нижнем регистре с подчёркиваниями. Потом этот $user пробрасываешь через proxy_set_header.
- Только код ответа. auth_request смотрит исключительно на HTTP-статус субзапроса: 2xx - пускаем, 401/403 - блок, всё остальное (500, таймаут) - 500 клиенту. Тело ответа авторизатора игнорируется полностью.
Код: Выделить всё
error_page 401 = @signin;
location @signin {
return 302 https://auth.example.com/oauth2/start?rd=$scheme://$host$request_uri;
}
satisfy: комбинируем IP и пароль
Теперь склеим механизмы. Директива satisfy управляет логикой, когда на location навешано несколько проверок доступа (allow/deny + auth_basic или auth_request). Значений два:
- satisfy all (по умолчанию) - доступ только если пройдены ВСЕ проверки. И правильный IP, и правильный пароль. Логическое И.
- satisfy any - достаточно ОДНОЙ пройденной проверки. Или из доверенной сети, или ввёл пароль. Логическое ИЛИ.
Код: Выделить всё
location /admin/ {
satisfy any; # достаточно одного условия
allow 203.0.113.0/24; # офисная сеть - без вопросов
deny all; # остальным IP-проверка не пройдена
auth_basic "Admin";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://admin_backend;
}
nginx mtls: клиентские сертификаты для B2B и zero-trust
Пароль может утечь, IP можно подделать. Для партнёрского API, межсервисного трафика и zero-trust-архитектуры применяют mTLS (mutual TLS) - когда не только клиент проверяет сертификат сервера, но и сервер требует валидный сертификат от клиента. Без правильного клиентского сертификата TLS-рукопожатие просто не завершится. Это сильнейшая из рассмотренных проверок: подделать приватный ключ нельзя, а отозвать доступ можно, перевыпустив CA или добавив сертификат в CRL.
Настройка nginx mtls идёт в server-блоке поверх обычного TLS:
Код: Выделить всё
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
ssl_certificate /etc/nginx/tls/server.crt;
ssl_certificate_key /etc/nginx/tls/server.key;
# CA, которым подписаны клиентские сертификаты
ssl_client_certificate /etc/nginx/tls/partners-ca.crt;
ssl_verify_client on; # требовать клиентский сертификат
ssl_verify_depth 2; # глубина цепочки до доверенного CA
location /partner-api/ {
# передаём бэкенду, КТО пришёл
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_pass http://partner_backend;
}
}
- ssl_client_certificate - файл с сертификатом(ами) CA, которым ты доверяешь. Любой клиентский сертификат, подписанный этим CA, будет принят. Фактически ты сам становишься удостоверяющим центром для своих партнёров и выдаёшь им сертификаты.
- ssl_verify_client on - жёстко требовать валидный клиентский сертификат. Нет сертификата или он не прошёл проверку - рукопожатие обрывается с ошибкой TLS, до location дело не доходит. Есть ещё значение optional - сертификат запрашивается, но соединение не рвётся, а результат кладётся в переменную (полезно, когда mTLS нужен только на части путей).
- ssl_verify_depth 2 - максимальная длина цепочки от клиентского сертификата до доверенного CA. Если у партнёра промежуточный CA, глубины 1 не хватит.
Код: Выделить всё
location /partner-api/ {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_pass http://partner_backend;
}
Типичные грабли
- deny all первой строкой - закрывает доступ всем мгновенно, остальные allow не проверяются. Разрешённых пишем сверху.
- allow/deny за прокси без realip - фильтруешь адрес LB, а не клиента. И наоборот, доверять X-Forwarded-For всем подряд - дыра.
- htpasswd -c поверх существующего файла - затирает всех пользователей. -c только при создании.
- Basic Auth по HTTP - пароль уходит открытым текстом в base64. Только поверх TLS.
- auth_request с телом запроса - забыл proxy_pass_request_body off, и каждый POST дублируется в auth-сервис.
- Перепутал $upstream_http_ префикс в auth_request_set - заголовок из ответа авторизатора не подхватится, переменная будет пустой.
- mTLS optional без проверки $ssl_client_verify - думаешь, что закрыл, а пускаешь всех, потому что optional не рвёт соединение.
- Забыл ssl_verify_depth при промежуточном CA - валидные клиенты получают отказ.
Повтори руками по шагам на тестовом nginx.
- Создай файл паролей: htpasswd -B -c /etc/nginx/.htpasswd testuser (введи пароль).
- Добавь location в свой server-блок:
Код: Выделить всё
location /secret/ {
satisfy any;
allow 127.0.0.1; # localhost - без пароля
deny all;
auth_basic "Lab Area";
auth_basic_user_file /etc/nginx/.htpasswd;
return 200 "you are in\n";
}
- Проверь конфиг и перезагрузи: nginx -t и nginx -s reload.
- Запрос с localhost: curl http://127.0.0.1/secret/ - должен пустить без пароля (сработал allow при satisfy any).
- Запрос с другого IP без пароля - получишь 401. С паролем: curl -u testuser:ПАРОЛЬ http://СЕРВЕР/secret/ - пустит.
- Поменяй satisfy any на satisfy all, reload. Теперь даже с localhost попросят пароль - оба условия обязательны. Прочувствуй разницу.
- Почему deny all должна стоять ПОСЛЕ всех allow, а не до них? Что проверяет nginx, дойдя до первого совпадения?
- Чем satisfy any отличается от satisfy all, если на location висят и allow/deny, и auth_basic? Какой вариант для строгой админки?
- Зачем в auth_request ставят proxy_pass_request_body off, и как достать из ответа авторизатора заголовок X-User на бэкенд?
- Что лежит в $ssl_client_verify и почему при ssl_verify_client optional эту переменную обязательно проверять самому?
У тебя теперь четыре уровня контроля доступа на шлюзе. allow/deny - быстрый IP-фильтр, но помни про realip за прокси. auth_basic - пароль за тридцать секунд для служебных страниц, только поверх HTTPS. auth_request - современный forward-auth, фундамент SSO через oauth2-proxy и Authelia, где nginx спрашивает разрешение у внешнего сервиса до проксирования. mTLS - криптографическая личность клиента для B2B и zero-trust. И главное - satisfy, который позволяет комбинировать их в одну осмысленную политику: из офиса свободно, снаружи по паролю, а к партнёрскому API - только с сертификатом.