Haproxy: трюки с авторизацией — Fastenv

Fastenv - системное администрирование от профессионалов индустрии. Работаем с linux, уважаем opensource.

Haproxy: трюки с авторизацией
Сложность: Высокая | Автор: fastenv | March 26, 2016

Haproxy — очень мощный и гибкий инструмент. В этой заметке мы продемонстрируем его применение в двух достаточно экзотических задачах.

Задача №1

Маршрутизировать запрос клиента в зависимости от авторизации.
Однажды в fastenv мы столкнулись с такой задачей: есть некий веб ресурс, где авторизуется клиент. Разным клиентам надо показывать разные web-странички. С самого рождения этой задачи меня не покидало ощущение — должно существовать простое решение админскими инструментами. Поиск «auth vhost», «auth based route» к готовому решению не привел, тогда я взял любымый haproxy и сделал на нём:

userlist server-access
        group private users fastenv
        group public users public1,test
 
        user fastenv insecure-password fastenv
        user public1 insecure-password public1
        user test insecure-password test
 
 
frontend example-vhost
        bind *:8080
 
        acl auth_private1 http_auth_group(server-access) private
        acl auth_public1 http_auth_group(server-access) public
 
        use_backend auth-private if auth_private1
        use_backend auth-public if auth_public1
 
        default_backend auth-null
 
backend auth-private
        errorfile 503 /etc/haproxy/private.http
 
backend auth-public
        errorfile 503 /etc/haproxy/public.http
 
backend auth-null
        errorfile 503 /etc/haproxy/null.http

Рассмотрим ключевые моменты:

  1. Директивой userlist создается список доступа. Это просто имя, как и имя бекенда например. Таких списков может быть множество.
  2. Директива group создает группу внутри текущего userlist и указывает, какие пользователи входят в эту группу.
  3. Наконец user создает пользователя и присваевает ему пароль.
  4. Далее во frontetd`е acl http_auth_group(userlist) group_name возвращает TRUE в том и только в том случае, если в текущем запросе есть заголовок Authorization с валидными данными принадлежащеми пользователю входящему в group_name.
  5. Для нашего эксперимента я отдаю проверочные страницы просто как 503 ошибку. Сами странички сгенерированы таким образом:
    for backend in public private null; do echo "${backend^} backend" > ${backend}.http; done

Проверяем:

curl http://example.local:8080/                        
Null backend # Авторизационных данных нет.
 
curl --user fastenv:test http://example.local:8080/ 
Null backend # Авторизационные данные неверны.
 
curl --user fastenv:fastenv http://example.local:8080/
Private backend # Прекрасно! Попали в нужный бекенд.
 
curl --user public1:public1 http://example.local:8080/       
Public backend
 
curl --user test:test http://example.local:8080/       
Public backend # Великолепно!

Далее концепцию развить по вкусу. Например в бекенде auth-null можно отправить запрос на красивую страничку авторизации. Всё что от нее требуется — проставить заголовок Authorization c Basic кредами. В простейшем случае можно попросить сам haproxy требовать авторизацию. Для этого приведем auth-null к виду:

backend auth-null
        errorfile 503 /etc/haproxy/null.http
        http-request auth realm Fastenv

Проверяем:

curl --user test:test2 http://example.local:8080/
 
<h1>401 Unauthorized</h1>
 
You need a valid user and password to access this content.

Браузер отреагирует на 401 код и выведет запрос авторизации.

Задача №2

Дать временный доступ к 22 порту для определенного ip.
Наверное все администраторы публичных linux серверов сталкивались с следующей ситуацией: Стоит выставить сервер наружу, как спустя несколько дней доброжелатели начинают перебирать пароли к ssh. Каждый для себя решает эту проблему по-своему. Кто-то настраивает fail2ban, кто-то настраивает access листы, кто-то вообще брутфорс проблемой не считает, ну а я просто переношу ssh на нестандартный порт. А однажды захотелось экзотики: Открывать 22 порт для клиентов внешней сети, сделавших определенный http запрос. Конечно же с авторизацией =)
Haproxy, its your turn:

userlist ssh-external-access
        user artem insecure-password fastenv
 
frontend ssh-auth
        bind *:9090
 
        acl auth_ok http_auth(ssh-external-access)
 
        use_backend ssh-allow if auth_ok
        default_backend ssh-deny
 
backend ssh-allow
        errorfile 503 /etc/haproxy/ssh-allow.http
        stick-table type ip size 100 expire 10m store conn_cnt
        tcp-request content track-sc0 src
 
backend ssh-deny
        errorfile 403 /etc/haproxy/ssh-denied.http
        http-request deny
 
frontend ssh
        bind *:22
        mode tcp
        option tcplog
 
        acl auth_ok src_conn_cnt(ssh-allow) gt 0
        use_backend ssh if auth_ok
 
backend ssh
        mode tcp
        server localhost 127.0.0.1:10522

Проверяем:

echo | nc example.local 22 | awk 'NR==1 {print $1}'
# Ничего в ответ - всё верно.
 
curl --user artem:google http://example.local:9090/
Try later..
 
echo | nc example.local 22 | awk 'NR==1 {print $1}'
# Тишина.
 
curl --user artem:fastenv http://example.local:9090/
Now this door is open for you.. # =)
 
echo | nc example.local 22 | awk 'NR==1 {print $1}' 
SSH-2.0-OpenSSH_6.6.1p1 # Всё работает. У нас есть 10 минут что бы залогиниться.)

Теперь разберем, как это работает:

  1. При успешной авторизации управление попадает в бекенд ssh-allow, там у нас создана sticky-таблица на 100 адресов. В ней хранится id,src(ip),conn_cnt — количество соединений с этого ip.
  2. Опция «track-sc0 src» включает раннее обновление счетчиков в таблице. (Это, вообще говоря, нестандартное поведение. В большинстве случаев счетчики обновляются после обработки запроса сервером, что нам не подходит, т.к. у нас нет сервера(мы сразу кидаем 503 ошибку)).
  3. Наконец в ssh фронтенде проверяется, отлично ли от нуля количество «успешных» подключений.

P.S.
Конфиги носят минимальный характер. В реальном деле вам захочется:

  1. Использовать ssl для авторизационных данных.
  2. Использовать функцию password вместо insecure-password.
  3. Подкрутить таймауты для ssh.