суббота, 31 июля 2021 г.

Балансировка исходящего трафика

Балансировка NAT-серверов.

Расскажу, как мы раньше разделяли нагрузку между несколькими NAT-серверами.
Проблемы с исчерпанием ipv4-адресов у нас тогда не стояло, но мы решили заблаговременно настроить NAT для абонентов. Недолго думая, запустили NAT-сервер на x86 сервере, Centos 6.
Железо обычное, Xeon E3-12xx, v2 или что там было в то время. Памяти гигабайт 8. Двухпортовая десятигигабитная сетевая карта.


Сервер воткнули в бордер двумя концами и принудительной маршрутизацией (PBR) зарулили трафик тестовых серых абонентов.

Сам nat описывать нет особого смысла, там было всё штатно, NAT силами conntrack/netfilter, настроенный через iptables. Какой-то обычный тюнинг sysctl. Да драйвер для сетевой карты взяли с сайта производителя, благо он собирался в RPM пакет одной командой (производитель позаботился).

Самое интересное настало, когда мы решили запускать туда настоящих абонентов, а значит нужен, как минимум, еще один сервер (для отказоустойчивости).

Воткнули второй сервер во второй бордер на другом узле, PBR заменили на динамическую маршрутизацию: сверху бордер "вещает" дефолтный маршрут в NAT, а тот его передаёт абонентам (на самом деле в тот же бордер, но уже в другой VRF, в котором обитают "серые" абоненты).
Снизу VRF с абонентами вещает в NAT серые сети абонентов, NAT'ы же анонсируют наверх в бордер свои публичные сети.

Отказоустойчивость какую-никакую, но обеспечили. А вот с балансировкой нагрузки тяжелее. Маршрутизацией её добиться довольно сложно, это ручные игры с метриками и "нарезкой" абонентов на микро VRF (чтобы анонсировать разным порциям разный дефолт -- через первый нат или через второй).

А тут уже и третий сервер пора ставить. Воткнули в первый бордер. Теперь бордер "видит" два маршрута по умолчанию. Но тут нам не удалось настроить корректную работу балансировки. В чем была беда: многие сайты держат web-интерфейс на одном сервере, а контент на другом. И эти сайты считали, что контент нужно получать с тем же SRC IP, с которым авторизовался на web-интерфейсе.

И роутер был слишком умным, чтобы выбирать маршрут основываясь только лишь на адресе абонента. Этот роутер использовал в вычислении хэша для балансировки и адрес назначения, и отключить это нам не удалось. В результате абоненты начали страдать от сайтов, потому что авторизовались на сайте с использованием одного SRC IP (через первый нат), а за контентом пришли со второго IP (через другой нат).

В тот момент у нас не было возможности поставить другой (промежуточный) роутер, который бы выбирал один из восьми (да, мы довели количество натов до восьми штук на каждом бордере) маршрут, основываясь лишь на SRC IP абонента.

Поэтому мы сделали всё сами, с помощью Linux. Мы, как и любой провайдер, знали профиль нашего трафика и решили, что на узле достаточно одного 10G балансировщика исходящего трафика. Т.е. разница между входящим и исходящим у нас была более, чем в восемь раз.

Значит маршрутизацией заманиваем исходящий от абонента трафик на такой же сервер, как и остальные NAT'ы (всё воткнуто в бордер, через промежуточный 10G-коммутатор, конечно же), на нём происходит магия, трафик от одного абонента всегда попадает на один и тот же NAT, а с NAT'а возвращается к абонентам минуя балансировщик (ведь никто же не заставляет маршрутизацию быть симметричной).

Что за магия на балансировщике? Тот же Centos и те же аппаратные характеристики.

Восемь таблиц маршрутизации (ip route table), каждая со своим статическим маршрутом по умолчанию, смотрящим в нужный нат.

Восемь правил маршрутизации (ip rule), заворачивающих трафик с определенными метками в соответствующие таблицы.

Восемь пустых сетов ipset (NAT1, NAT2, .. NAT8).

Если ip-адрес абонента уже есть в каком-то сете, то такой трафик отправляется в соответствующую сету цепочку NATx таблицы MANGLE:
-A PREROUTING -i eth3 -m set --match-set NAT1 src -j NAT1
-A PREROUTING -i eth3 -m set --match-set NAT2 src -j NAT2
-A PREROUTING -i eth3 -m set --match-set NAT3 src -j NAT3
-A PREROUTING -i eth3 -m set --match-set NAT4 src -j NAT4
-A PREROUTING -i eth3 -m set --match-set NAT5 src -j NAT5
-A PREROUTING -i eth3 -m set --match-set NAT6 src -j NAT6
-A PREROUTING -i eth3 -m set --match-set NAT7 src -j NAT7
-A PREROUTING -i eth3 -m set --match-set NAT8 src -j NAT8
Если же ip-адреса еще нет, то пакет дойдёт до этих правил и с равной вероятностью (1/8=0.125; 1/7=0.143; 1/6=0.167 и т.д.) окажется в одной из цепочек:
-A PREROUTING -i eth3 -m statistic --mode random --probability 0.125 -j NAT1
-A PREROUTING -i eth3 -m statistic --mode random --probability 0.143 -j NAT2
-A PREROUTING -i eth3 -m statistic --mode random --probability 0.167 -j NAT3
-A PREROUTING -i eth3 -m statistic --mode random --probability 0.200 -j NAT4
-A PREROUTING -i eth3 -m statistic --mode random --probability 0.250 -j NAT5
-A PREROUTING -i eth3 -m statistic --mode random --probability 0.333 -j NAT6
-A PREROUTING -i eth3 -m statistic --mode random --probability 0.500 -j NAT7
-A PREROUTING -i eth3 -j NAT8
А уже в этих цепочках пакет маркируется нужной меткой,..
-A NAT1 -j SET --add-set NAT1 src --exist
-A NAT1 -j MARK --set-xmark 0xa/0xffffffff
-A NAT1 -j ACCEPT
-A NAT2 -j SET --add-set NAT2 src --exist
-A NAT2 -j MARK --set-xmark 0x14/0xffffffff
-A NAT2 -j ACCEPT
-A NAT3 -j SET --add-set NAT3 src --exist
-A NAT3 -j MARK --set-xmark 0x1e/0xffffffff
-A NAT3 -j ACCEPT
-A NAT4 -j SET --add-set NAT4 src --exist
-A NAT4 -j MARK --set-xmark 0x28/0xffffffff
-A NAT4 -j ACCEPT
...
...и попадает, благодаря ip rule в нужную таблицу маршрутизации, а далее летит по нужному маршруту на нужный NAT-сервер.
Суть понятна? Если балансировщик еще не встречал этот абонентский IP, то засунет его в одну из восьми цепочек ipset благодаря mode random. Если же адрес уже нам известен (есть в каком-либо ipset), то такой трафик всегда будет лететь в нужный NAT.

Еще был написан небольшой демон, который простым пингом проверял наличие NAT-серверов за портом и выводил его из работы, динамически удаляя/добавляя правила в iptables с корректным пересчетом вероятности.

понедельник, 28 июня 2021 г.

Grafana с аутентификацией через Radius

Встала задача поднять Grafana с аутентификацией через Radius.

Сразу же была найдена статья.

Принцип работы ясен. В grafana есть специальный механизм auth.proxy, который может получать данные в виде переменных окружения. В том примере используется веб-сервер Apache, мы же воспользуемся более привычным Nginx.

Ставим grafana, nginx и libpam-radius-auth штатным для дистрибутива способом.

Настраиваем pam:

/etc/pam_radius_auth.conf
radius-server-ip:port   secret
Делаем этот файл доступным для чтения пользователю, под которым работает nginx (у меня это www-data):
chown www-data:www-data /etc/pam_radius_auth.conf
/etc/pam.d/nginx
auth    required        pam_radius_auth.so debug client_id=grafana conf=/etc/pam_radius_auth.conf
account required        pam_permit.so
session required        pam_permit.so

Настраиваем nginx:
server {
       listen 443 ssl;

       ssl_certificate /etc/ssl/cert.pem;
       ssl_certificate_key /etc/ssl/cert.key;
       ssl_protocols TLSv1.2;
       ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256;
       ssl_prefer_server_ciphers on;
       add_header Strict-Transport-Security 'max-age=604800';
       server_name grafana.example.com;

       access_log /var/log/nginx/gr.access.log;
       error_log /var/log/nginx/gr.error.log;
       location / {
           auth_pam "Auth";
           auth_pam_service_name "nginx";
           proxy_pass http://localhost:3000/;
           proxy_set_header X-WEBAUTH-USER $remote_user;
           proxy_set_header Authorization "";
       }
}

Настраиваем grafana
/etc/grafana/grafana.ini:
....
[auth.proxy]
enabled = true
....

Перезапускаем grafana-server и nginx и проверяем.

среда, 27 мая 2020 г.

Пароли в SHA256 в OpenLDAP

Понадобилось нам поднять для определенных целей OpenLDAP. С условием, что пароли, вернее хэши наших пользователей, мы будем брать из уже существующей SQL-базы. Из входящих данных у нас был SQL-запрос, вызвращающий пользователей с хэшами и утверждение нашего DBA, что пароли хешированы в SHA256.

Получаю хэш пользователя из базы, проверяю через
echo -n "test" | sha256sum
9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08  -
Строка совпадает. Быстро ваяем скрипт, который заполняет нужные поля в LDAP, в том числе вставляет эту строку в качестве пароля.

Пробуем авторизоваться:
ldapwhoami -vvv -h 127.0.0.1 -p 389 -D "cn=Иванов Иван,ou=staff,dc=example,dc=com" -W
Enter LDAP Password:                                                                                             
ldap_bind: Invalid credentials (49)
Что-то не работает.

Google подтверждает, что OpenLDAP умеет в SHA256, однако разные статьи говорят, что для этого надо либо использовать сторонний модуль slapd-sha2.so, либо воспользоваться услугами самой операционной системы, ведь пароли в Linux хранятся в SHA256/SHA512.

Выбираю второй вариант. Пишут, что надо добавить префикс {CRYPT}. Чтобы указать, что это хэш sha256, делаю предположение, что надо добавить $5$:
cat ./chpass.ldif 
dn: cn=Иванов Иван,ou=staff,dc=example,dc=com
changetype: modify
replace: userPassword
userPassword: {CRYPT}$5$9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

Заливаю изменение, проверяю...

ldap_bind: Invalid credentials (49)

Нахожу в OpenLDAP FAQ:
Just to state the obvious, SHA-256 and SHA-512 based "glibc" crypt algorithms $5$ and $6$ are totally different from plain (or salted) "{SHA256}" algorithms. The libc crypt variants do a lot of nonsensical transpositions to increase the computational load.

Черт, пишут что алгоритмы различаются.

Чтобы удостоверится, вставляем свой хэш из /etc/shadow в качестве пароля с префиксом {CRYPT}$6$.

Работает, конечно.

Но нам надо использовать те хэши, которые уже в базе. База уже успешно используется в другом проекте, так что хэши рабочие, чтобы "перехешировать" под наш алгоритм, нужно будет инициировать смену паролей у нескольких сотен пользователей, да еще и переписать уже существующий проект, успешно использующий имеющиеся хэши.

Параллельно в гугле находим, что идентичный команде sha256sum хэш даёт команда:
echo -n "test" | openssl dgst -sha256
(stdin)= 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Да и онлайн сервис по распознаванию хэшей подтверждает, что это действительно sha256.
Тогда как на результат команды:
openssl passwd -5
говорит Unknown.

Ладно, читаем мануалы дальше. Различие в "соли". Passwd в Linux и в openssl делают пароль с солью, а sha256sum, понятное дело, без. И как раз-таки в OpenLDAP это должно работать, при использовании префикса {SHA256}

Видимо надо искать модуль. И находим информацию, что в актуальных версиях OpenLDAP модуль уже присутствует в системе. А файл slapd-sha2.so мы не могли найти потому, что он называется по-другому. Сотни статей по openldap+sha256 уже не актуальны, кто бы мог подумать :)

Для подтверждения этого выкачиваю исходники openldap с гита, нахожу модуль и в его Makefile'е вижу сему подтверждение. Файл на выходе действительно называется pw-sha2.la / pw-sha2.so.

Пробую просто добавить префикс {SHA256}:
cat ./chpass.ldif 
dn: cn=Иванов Иван,ou=staff,dc=example,dc=com
changetype: modify
replace: userPassword
userPassword: {SHA256}9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

Заливаю, пробую, не работает.

Может модуль не подгружен?
Конфиг OpenLDAP сейчас хранится в самом OpenLDAP, так что смотрим:

# slapcat -b cn=config -a "(objectClass=olcModuleList)"
...
olcModuleLoad: {1}pw-sha2.la
...
И видим, что модуль загружен.

Кстати, у нас уже есть выкачанные исходники OpenLDAP. Без особой надежды лезу в них поковырятся, мож пойму, что ему надо. В readme модуля sha2 до сих пор сказано, что надо собрать slapd-sha2.so, скопировать его в нужное место и подключить через конфиг slapd.conf (т.е. инструкция в исходниках сама устарела, ведь и библиотека называется по-другому, и конфиг OpanLDAP уж много лет как в самом OpenLDAP).

Но в том же readme ниже нахожу команду для создания хэша для тестирования:
$ echo -n "secret" | openssl dgst -sha256 -binary | openssl enc -base64
K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=
Что ж, мы были близки. Пароль надо завернуть в base64. И это при том, что в LDAP оно еще раз заворачивается в base64. Но не строчное его представление, а "бинарное".

Делаю base64 над своим паролем, вставляю с префиксом {SHA256}:

# echo -n "test" | openssl dgst -sha256 -binary | base64
n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=

# cat ./chpass.ldif 
dn: cn=Иванов Иван,ou=staff,dc=example,dc=com
changetype: modify
replace: userPassword
userPassword: {SHA256}n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=

Заливаю, проверяю, работает!

Result: Success (0)

В скрипте это можно сделать так:
echo -n "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" | xxd -r -p | base64

суббота, 2 мая 2020 г.

Обнулить все файлы в каталоге.

Для довольно специфической задачи понадобилось обнулить все файлы в каталоге:
while read line; do bash -c "> \"$line\""; done < <(ls -1)

вторник, 19 ноября 2019 г.

Apt не хочет качать по https

А вот тут был интересный кейс. Сервер со свежеобновленным Debian 10.2 не хочет качать индексные файлы из репозитория по протоколу https:
Err https://download.docker.com/linux/debian buster Release
  Certificate verification failed: The certificate is NOT trusted. The certificate issuer is unknown.  Could not handshake: Error in the certificate verification. [IP: 205.251.219.17 443]
W: https://download.docker.com/linux/debian/dists/buster/InRelease: No system certificates available. Try installing ca-certificates.
W: https://download.docker.com/linux/debian/dists/buster/Release: No system certificates available. Try installing ca-certificates.
E: The repository 'https://download.docker.com/linux/debian buster Release' does not have a Release file.
E: Failed to download some files
W: Failed to fetch https://download.docker.com/linux/debian/dists/buster/Release: Certificate verification failed: The certificate is NOT trusted. The certificate issuer is unknown.  Could not handshake: Error in the certificate verification. [IP: 205.251.219.17 443]
E: Some index files failed to download. They have been ignored, or old ones used instead.
Проверил ссылку из браузера -- всё ок, на той стороне сертификат валидный.
Значит дело в нашем сервере. Оно и понятно, на других серверах этот же репозиторий работает нормально.

Но ни curl, ни wget не ругается на сертификат, как и openssl s_client.

Более того, подключил еще один проверенный репозиторий по https, на него валятся такие же ошибки.

За пару часов в гугле ничего не нашел. У всех с такой ошибкой какие-то проблемы с корневыми сертификатами и им предлагают произвести стандартные танцы с бубнами вокруг пакета ca-certificates. У меня же с сертификатами всё хорошо, состав и количество совпадают с "рабочими" серверами. Но именно apt не хочет работать по https.
Дело оказалось в правах на каталог /etc/ssl/certs. Кто-то ошибочно выставил на него режим 700, поэтому curl и wget, запущенные от root добрались до корневых сертификатов, а apt из-под непривилегированного пользователя не смог.

А значит решением будет:

# chmod 755 /etc/ssl/certs/

среда, 30 октября 2019 г.

Консольная утилита для управления WordPress

У нас есть сервер, на котором крутятся сайты на WordPress для других отделов. Они их как-то сами наполняют и админят, но, конечно же, обновлять и не думают.
Nginx, PHP, Mysql я им обновлю, а как быть с самими сайтами?

Можно было бы добавить себя в админы каждого сайта, но не всё так просто. На каких-то сайтах локальная аутентификация, на других LDAP, на третьих что-то ещё.

Объединяет их то, что они лежат на одном сервере. Обновить их оказалось довольно просто (имея рутовые права на сервере).

Нужна утилита wp-cli. К сожалению, её нет в официальных репах Debian'а, но эту утилиту можно скачать с её официального сайта (это один файл на php) и положить в любой каталог из переменной path. Если мы хотим, чтобы утилита могла обновлять сама себя, её надо положить в каталог, правами на запись в который должен обладать пользователь www-data (или под кем вы там запускаете web-сервер). Ну тут смотрите сами, снижаем безопасность в угоду удобству, как обычно.

Утилите надо создать каталог с правами на запись, куда она сможет скачивать обновления Вордпресса и плагинов. Он должен лежать в домашнем каталоге пользователя web-сервера, у меня это /var/www и назвать .wp-cli.

Далее, утилиту надо запускать от имени web-сервера, это www-data в Debian по умолчанию.

Например так:

От рута:
# sudo -u www-data bash
Переходим в каталог с первым сайтом:
$ cd /var/www/site1
$ wp core check-update
$ wp core update
$ wp core updatedb
$ wp plugin update --all
Переходим в следующий и повторяем, пока не кончатся сайты. Ну а дальше скрипты для автоматизации, ansible и т.д., но вы это и без меня знаете.

среда, 27 февраля 2019 г.

Создать пользователя в DJANGO через SQL

Понадобилось попасть в админку сайта на django, имя рутовый доступ к серверу. База сайта лежала в локальном sqlite. Генерируем себе пароль с помощью утилиты djpass:
$ ./djpass ololo
Hash: pbkdf2_sha256$120000$nwEK2MXzMCj7$u3ZGMBCbdGC72pNJkh8dZ79pcYbmlGNQCrDXvvG8WuA=
Эту утилиту я поставил в мусорную тестовую виртуальную машину на Debian, дабы не засорять свой комп, а уж тем более сервер, всяким непотребством. Ставится через программу cargo, которая есть в стандартных репах Debian:
# apt install cargo
$ cargo install djpass
Если что, утилита оказывается в каталоге ~/.carbo/bin С хешем наперевес идём в базу. В моём случае, как я уже сказал, это sqlite:
# sqlite3 /path/to/db/file

sqlite> INSERT INTO "auth_user" VALUES(3,"pbkdf2_sha256$120000$nwEK2MXzMCj7$u3ZGMBCbdGC72pNJkh8dZ79pcYbmlGNQCrDXvvG8WuA=
","",1,"megaadmin","","",1,1,"2019-02-27 11:32:20","");
Готово. Далее у меня была внутреняя ошибка 500, но при включенном дебаге выяснил, что просто права к базе были некорректными, nginx не мог до неё добраться.