Proxy protocol на практике при использовании Nginx и ELB

7 мая 2015

Клиенты нередко приходят в наш сервер сайд или вебчик через какие-то прокси, Nginx, HAProxy, Elastic Load Balancer и другие. Прокси эти очень полезны, так как обеспечивают сжатие и шифрование трафика, балансировку нагрузки и так далее. Проблема однако заключается в том, что прокси скрывают настоящий IP пользователя, из-за чего становится невозможно определить, например, из какой страны он пришел, и соответственно, интерфейс на каком языке ему отобразить. В случае с HTTP эта проблема давно решена путем проставления заголовков с IP пользователя. Но что делать при использовании других протоколов, вебсокетов там или просто чего-то самописного поверх TCP? Вот для решения именно этой проблемы разработчиками HAProxy и был придуман proxy protocol.

Допустим, вы используете конфигурацию, похожую на ту, что была описана в заметке Использование Elastic Load Balancer и Auto Scaling Groups. То есть, трафик идет через ELB в Nginx, а из него — в локально крутящийся сервис. Для простоты в рамках этой заметки мы будем работать с протоколом HTTP, но для вебсокетов (мы ведь с вами уже умеем пробрасывать вебсокеты через Nginx) все работает точно так же, я проверял.

Идем на source instance, из которого мы потом создаем AMI для поднятия машин в auto scaling group. Поддержка proxy protocol появилась в Nginx версии 1.5.12, однако в репозиториях Ubuntu 14.04 LTS на момент написания этих строк был доступен только Nginx 1.4.6. Поэтому мы воспользуемся PPA для установки Nginx посвежее:

sudo add-apt-repository ppa:nginx/stable
sudo apt-get update
sudo apt-get install nginx-full

Сразу поправим файл /etc/nginx/nginx.conf. Находим строчку:

access_log /var/log/nginx/access.log

… и заменяем на:

log_format elb_log '$proxy_protocol_addr - $remote_user [$time_local] '
                   '"$request" $status $body_bytes_sent '
                   '"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log elb_log;

Конфиг нашего сайта в /etc/nginx/sites-enabled/ меняем примерно так:

listen 80;
listen 81 proxy_protocol;

location / {
  proxy_pass http://127.0.0.1:8080;
  proxy_set_header Host            $host;
  proxy_set_header X-Real-IP       $proxy_protocol_addr;
  proxy_set_header X-Forwarded-For $proxy_protocol_addr;
}

Самое главное здесь — это добавление прослушивания нового порта (81) с поддержкой proxy_protocol, а также пробрасывания в заголовках X-Real-IP и X-Forwarded-For переменной $proxy_protocol_addr.

Говорим Nginx перечитать конфиг:

sudo service nginx reload
tail /var/log/nginx/error.log

Если видим что-то вроде:

[emerg] 29050#0: unknown "proxy_protocol_addr" variable

… значит вы меня не послушали и пытались использовать старый Nginx.

Работу всего этого хозяйства можно проверить telnet’ом:

PROXY TCP4 12.34.56.78 10.0.0.111 56789 80
GET / HTTP/1.0

HTTP/1.1 200 OK
Server: nginx/1.6.2
Date: Thu, 26 Mar 2015 08:42:03 GMT
Content-Length: 0
Connection: close

В логах Nginx должны увидеть запись о запросе с IP 12.34.56.78.

Теперь, когда вы посмотрели на proxy protocol в действии, становится довольно очевидно, что обычные HTTP клиенты смогут нормально работать только при хождении на 80-ый порт, но не 81-ый, где включена поддержка протокола. В целях отладки, а также бесшовного обновления с простого проксирования на проксирование с использованием proxy protocol, следует использовать разные порты. Из соображений безопасности вам может захотеться зафаерволить порт 81, чтобы пускать трафик только из балансировщика, иначе пользователь сможет без труда подделать свой IP.

Напоминаю, что все это мы делали с source instance. Сейчас его можно использовать для обновления автоскелинг группы и все будет работать, как раньше. Осталось настроить ELB. К сожалению, в веб-интерфейсе Амазона на момент написания этих строк не было возможности включить поддержку proxy protocol в ELB, поэтому придется воспользоваться CLI утилитами.

Смотрим свойства балансера, чтобы потом понять, какие изменения были нами сделаны:

aws elb describe-load-balancers --load-balancer-name test-balancer

Создаем новое полиси у балансировщика:

aws elb create-load-balancer-policy \
  --load-balancer-name test-balancer \
  --policy-name EnableProxyProtocol \
  --policy-type-name ProxyProtocolPolicyType \
  --policy-attributes AttributeName=ProxyProtocol,AttributeValue=True

Теперь обновим список полиси балансировщика для порта 81, добавив в него только что созданный EnableProxyProtocol. При выполнении команды describe-load-balancers у меня список BackendServerDescriptions был пустой, поэтому я просто указываю полиси EnableProxyProtocol. Если вы уже использовали какие-то полиси, то должны указать и их тоже, чтобы полиси не потерялись. То есть, это операция set, а не add. Собственно, команда:

aws elb set-load-balancer-policies-for-backend-server \
  --load-balancer-name test-balancer \
  --instance-port 81 \
  --policy-names EnableProxyProtocol

Если теперь повторить describe-load-balancers, то увидим примерно такое изменение:

diff a.json b.json
47c47,54
<             "BackendServerDescriptions": [],
---
>             "BackendServerDescriptions": [
>                 {
>                     "InstancePort": 81,
>                     "PolicyNames": [
>                         "EnableProxyProtocol"
>                     ]
>                 }
>             ],
60c67,69
<                 "OtherPolicies": []
---
>                 "OtherPolicies": [
>                     "EnableProxyProtocol"
>                 ]

Заметьте, что порт 81 — это InstancePort, а не LoadBalancerPort. То есть, использовать или нет proxy protocol в ELB задается для того порта, на который будет проброшен трафик, а не для того, который слушает балансировщик. Что, на самом деле, очень логично, так как proxy protocol поддерживается или нет на определенном порту именно сервера.

Если по ошибке повесили не тот полиси или не на тот порт, так можно очистить список полиси:

aws elb set-load-balancer-policies-for-backend-server \
  --load-balancer-name test-balancer \
  --instance-port 81 \
  --policy-names '[]'

Далее находим секьюрити группу, которую вы используете в своем launch configuration, разрешаем хождение на порт 81 — либо откуда угодно, либо, что предпочтительнее, только внутри вашей VPC. Раскатываем приложение, используя модифицированный source instance, если вы этого все еще не сделали, несмотря на то, что соответствующий шаг был назван выше. В свойствах балансера меняем листенер так, чтобы он ходил на порт 81 вместо 80. Убедитесь, что пробрасываете TCP, а не HTTP! Проверяем, что все работает. В секьюрити группе после этого можно запретить хождение на порт 80 напрямую.

В качестве отладочного приема еще можно порекомендовать временно остановить сервис, сказать nc -l -p 8080 и послать запрос через балансирощик. Должны увидеть проставленные заголовки с IP пользователя:

GET / HTTP/1.0
Host: example.ru
X-Real-IP: 12.34.56.78
X-Forwarded-For: 12.34.56.78
Connection: close
User-Agent: curl/7.35.0
Accept: */*

Ссылки по теме:

А используете ли вы proxy protocol? Если да, то с какими сервисами и какими протоколами?

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.