Резервирование и балансировка (HAProxy)

ESIA-Adapter : 3.1. Резервирование и балансировка (HAProxy)


Два идентичных узла ESIA Adapter размещаются за балансировщиком HAProxy. Адаптер хранит состояние входа в памяти узла, поэтому запросы одного входа и выданные узлом токены направляются на тот же узел. Для этого каждому узлу задаётся уникальный NODE_ID, а балансировщик определяет узел по cookie и по значению nid в токенах.

Параметр NODE_ID

NODE_ID — необязательный идентификатор узла. Значение возвращается в JWT и используется балансировщиком. Значение задаётся переменной окружения службы и должно быть уникальным для каждого узла, например node-a и node-b.

Идентификатор узла содержится в:

ОбъектГде находится
Код авторизации (JWT)claim nid
Токен доступа access_token (JWT)claim nid
Токен обновления refresh_token (JWT)claim nid
id_token (JWT)claim nid
Ответ /oauth2/tokenполе node_id

Если NODE_ID не задан, nid и node_id пустые.

Состояние на узле

ДанныеГде хранятся
Сессия пользователя (cookie sid)память узла
Запрос к ЕСИА (параметр state)память узла
Данные пользователя (userinfo)память узла
Коды и токеныпамять узла; при заданном DB_URL — PostgreSQL

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

ЗапросОткуда определяется узел
/oauth2/authorize, /extidp/esia, /extidp/esia/callback, /oauth2/logout (от браузера)cookie балансировщика ESIA_NODE
/oauth2/token, обмен кода (code)claim nid кода авторизации в теле запроса
/oauth2/token, обновление (refresh_token)claim nid токена обновления в теле запроса
/oauth2/userinfoclaim nid токена доступа в заголовке Authorization
/oauth2/logout с id_token_hintclaim nid в параметре id_token_hint

Настройка узлов

На обоих узлах значения одинаковы:

ПараметрЗначение
ISSUER, BASE_URLадрес балансировщика (https://...)
KEYодин и тот же ключ подписи токенов: тогда /oauth2/public_keys возвращает одинаковый набор ключей на обоих узлах
CLIENTSодинаковый clients.json
EXTIDPextidp.json одной и той же информационной системы ЕСИА
SESSION_*, *_TTL, FORCE_PKCEодинаковые значения

Различаются NODE_ID и параметры криптопровайдера (контейнер и сертификат узла) в extidp.json.

Пример для службы systemd (установка):

# узел A
Environment="NODE_ID=node-a"
# узел B
Environment="NODE_ID=node-b"

Настройка балансировщика Nginx описана в разделе «Резервирование и балансировка (Nginx)».

Конфигурация HAProxy

Имена серверов в backend совпадают со значениями NODE_ID. Узел из токенов определяется стандартными средствами HAProxy: токен — JWT, его вторая часть (payload) кодируется base64url.

global
    log /dev/log local0
    stats socket /run/haproxy/admin.sock mode 660 level admin

defaults
    mode http
    log global
    option httplog
    option redispatch
    retries 2
    timeout connect 5s
    timeout client  60s
    timeout server  60s
    timeout http-request 10s

frontend esia_https
    bind *:443 ssl crt /etc/haproxy/certs/esia-adapter.pem
    option http-buffer-request
    http-request set-header X-Forwarded-Proto https

    # узел из access_token (/oauth2/userinfo и другие запросы с Bearer)
    http-request set-var(txn.nid) req.hdr(Authorization),word(2,.),ub64dec,json_query('$.nid') if { req.hdr(Authorization) -m beg -i "Bearer " }
    # узел из кода авторизации (обмен кода на токен)
    http-request set-var(txn.nid) req.body_param(code),word(2,.),ub64dec,json_query('$.nid') if { path /oauth2/token } { req.body_param(code) -m found } !{ var(txn.nid) -m found }
    # узел из токена обновления
    http-request set-var(txn.nid) req.body_param(refresh_token),word(2,.),ub64dec,json_query('$.nid') if { path /oauth2/token } { req.body_param(refresh_token) -m found } !{ var(txn.nid) -m found }
    # узел из id_token_hint (выход)
    http-request set-var(txn.nid) url_param(id_token_hint),word(2,.),ub64dec,json_query('$.nid') if { path /oauth2/logout } { url_param(id_token_hint) -m found } !{ var(txn.nid) -m found }
    # узел в журнале
    http-request capture var(txn.nid) len 32

    default_backend esia_nodes

backend esia_nodes
    balance roundrobin

    option httpchk
    http-check send meth GET uri /health/ready ver HTTP/1.1 hdr Host esia-adapter.corp
    http-check expect status 200

    # браузерный вход: cookie со значением NODE_ID
    cookie ESIA_NODE insert indirect nocache httponly secure

    # запросы с токеном: узел из claim nid
    use-server node-a if { var(txn.nid) -m str node-a }
    use-server node-b if { var(txn.nid) -m str node-b }

    default-server check inter 2s fall 2 rise 2
    server node-a 10.0.0.11:4010 cookie node-a
    server node-b 10.0.0.12:4010 cookie node-b
СтрокаНазначение
option http-buffer-requestHAProxy дожидается тела запроса: без этого req.body_param недоступен при выборе сервера
set-var(txn.nid) ... json_query('$.nid')извлекает nid из payload токена; подпись токена балансировщик не проверяет, её проверяет адаптер
cookie ESIA_NODE insert indirect nocacheпри первом запросе браузера выдаёт cookie со значением NODE_ID выбранного сервера; все запросы входа (authorize, callback, logout) возвращаются на тот же узел
use-server ... if { var(txn.nid) ... }запросы с токеном направляются на узел, выдавший токен; правило выполняется раньше проверки cookie
option redispatch, checkпри недоступности узла запрос передаётся на другой узел
http-check ... /health/readyузел считается доступным, если /health/ready возвращает 200

Для разбора токенов можно также использовать конвертер jwt_payload_query('$.nid') (HAProxy 2.8 и выше, сборка с OpenSSL).

Поведение при отказе узла

  • Правило use-server, указывающее на недоступный сервер, игнорируется: запрос передаётся на другой узел по обычным правилам балансировки.
  • Отказ узла обнаруживается проверкой состояния за время inter × fall (в примере около 4 секунд); запросы, пришедшие в этот интервал, могут завершиться ошибкой, и клиент повторяет запрос.
  • Состояние входа, сессии и данные пользователя находятся в памяти отказавшего узла. Пользователю, который находился в процессе входа, требуется повторная аутентификация.
  • Токены, выданные отказавшим узлом, на другом узле принимаются только при заданном DB_URL; при этом данные пользователя в памяти другого узла отсутствуют, и запросы /oauth2/userinfo и выдача id_token завершаются ошибкой до повторной аутентификации.
  • После перезапуска узла его память пуста: ранее выданные узлом коды и токены без DB_URL недействительны.

Для планового обслуживания узел переводится в режим drain: новые входы на него не направляются, а запросы уже начатых сессий продолжают обслуживаться.

echo "set server esia_nodes/node-a state drain" | socat stdio /run/haproxy/admin.sock

После обновления и проверки /health/ready узел возвращается командой set server esia_nodes/node-a state ready.

Проверка

  1. Получить токены и убедиться, что ответ /oauth2/token содержит node_id, совпадающий с NODE_ID узла.
  2. Декодировать payload токена и проверить nid:
echo "<access_token>" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null
  1. Повторить GET /oauth2/userinfo с токеном: в журнале HAProxy значение захваченного nid соответствует выбранному серверу.
  2. Остановить узел node-a: запрос с токеном этого узла передаётся на node-b, а /health/ready на остановленном узле возвращает ошибку; вход пользователя выполняется заново на node-b.

Обсуждение