пятница, 27 августа 2021 г.

Сервис дизайн с beanstalkd очередью. Реагируем на syslog события.


Странный пост которому я так и не смог придумать нормальное название. Так и хочется сказать знаменитое "Чиго?". Пробуем налабить масштабируемую систему для реакции на определенные логи.

С чего все началось?

У меня есть два инстанса VyOS (active-passive), которые смотрят в Интернет. Периодически, к ним пытаются подобрать пароли SSH, что в целом не страшно, но раздражает. 

Прям на устройствах у меня есть bash скрипт, который парсит логи и обновляет адрес группу, которая собственно блокирует запросы. Вот скрипт - https://github.com/MelHiour/vyos_onbox_scripts/blob/main/block-brute-force.sh

В целом, все прекрасно работает и ничего на данном этапе менять я не хочу. 

Но это натолкнуло меня на мысли как быть, если таких роутеров много. И что делать, есть нужно какие-то еще задачи решать похожую на эту. В общем, встали передо мной вопросы как это скейлить. Все это совпало с моим знакомством с AWS SQS и я решил пофантазировать на тему как бы выглядело мое решение, если его попытаться спланировать по уму.

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

Какова вообще цель?

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

Первое приближение

Ясно, что для того чтобы иметь возможность в дальнейшем легко дорабатывать и менять функционал нужно разделять функции. Это же позволит легче масштабировать. 

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


Первый "квадратик" это то, куда будут приходить логи с устройства. Здесь из всего потока логов вынимаются те события, на которые мы хотим реагировать и формируется некая задача.

Задачи складываем в FIFO очередь. Очереди принимает таск и отдает его по требованию. Первым пришел, первым вышел. Нужно так же обеспечить простейший жизненный цикл задач. Берем задачу в работу, помечаем задачу выполненной.

Responder. Этот "квадратик" берет задачи из очереди и выполняет их!

Пруф оф концепт

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



FIFO Queue

Признаться честно, поиграться с FIFO одна из целей поста. Я в этом ничего не понимаю, поэтому положился на Google. Последний посоветовал - beanstalkd.

Если не углубляться, концепция реально очень проста. В некую стопку кладем какие-то предметы. Снизу стопки начинаем эти предметы убирать. Когда предмет хотим убрать, резервируем его за собой. 

В моем случае, ставим эту стопку в виде docker контейнера.

yum install -y yum-utils


yum-config-manager \

   --add-repo \

   https://download.docker.com/linux/centos/docker-ce.repo


yum install docker-ce docker-ce-cli containerd.io


docker run -d --name beanstalkd -p 11300:11300 webplates/beanstalkd


systemctl start docker


systemctl enable docker

                            


В итоге получаем очередь в виде TCP порта. 

Попробуем положить пару "предметов" через Python клиент.

>>> import beanstalkc


>>> beanstalk = beanstalkc.Connection(host='localhost', port=11300)


>>> beanstalk.put('This is the first object')

1

>>> beanstalk.put('This is the second one')

2 


И забрать их

>>> import beanstalkc


>>> beanstalk = beanstalkc.Connection(host='localhost', port=11300)


>>> job = beanstalk.reserve()

>>> job.body

'This is the first object'

>>> job.delete()


>>> job = beanstalk.reserve()

>>> job.body

'This is the second one'

>>> job.delete()


Как видно. Первый положенный объект выбывает первым.

Parser

Это кусок кода, который слушает TCP порт на предмет syslog сообщений. Если сообщение нам интересно, то формируем задачу и кладем ее в очередь.

Пост не про Python, но вот краткая реализация.

Ниже подстрока которую мы ищем в логах и функция которая ее парсит. 

TRIGGERS = {'Failed password for root': password_failed,

            'Failed password for invalid user': password_failed, 

            'state DOWN': interface_down}



Для пароля, например, мы просто "вынимаем" IP адрес и формируем небольшой словарик. Этот словарик и будет нашей задачей.

def password_failed(host, data):

        result = re.search("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", data)

        job = {'host':host,'type':'password_failed','from':result.group()}

        return job


Код можно посмотреть здесь. Если прям горит от его "неидеальности", можете слать Pull Request.

В моем случае имеется VyOS подключенный к серверу. VyOS шлет все логи на наш Parser.

set system syslog host 10.0.0.1 facility all level 'all'
set system syslog host 10.0.0.1 facility all protocol 'tcp'
set system syslog host 10.0.0.1 port '10514'

Запускаем наш скрипт, подбираем пароль, роняем порт. VyOS генерирует syslog сообщения, которые шлет на Parser.

vyos@vyos:~$ grep "Failed password for root" /var/log/messages
Aug 26 13:55:13 vyos sshd[2755]: Failed password for root from 10.0.0.1 port 47336 ssh2
Aug 26 16:07:33 vyos sshd[2779]: Failed password for root from 10.0.0.1 port 47364 ssh2
vyos@vyos:~$ grep "state DOWN" /var/log/me
messages    messages.1  messages.2  
vyos@vyos:~$ grep "state DOWN" /var/log/messages
Aug 26 16:07:59 vyos netplugd[703]: eth1: state DOWN flags 0x00001002 BROADCAST,MULTICAST -> 0x00011043 UP,BROADCAST,RUNNING,MULTICAST,10000

 Cмотрим как задачи добавляются в очередь.

[root@localhost ~]# python parser.py 

{'from': '10.0.0.1', 'host': '10.0.0.2', 'type': 'password_failed'}


{'interface': 'eth1', 'host': '10.0.0.2', 'type': 'interface_down'}


Responder

Отвечать на эти события мы будет с размахом. Адрес злоумышленника добавляется в адрес группу, которая блочится на ACL. Упавший порт выключается, чтоб больше не падал!

Опять же, реализацию можно посмотреть тут. Я использовал уже написанные функции  в одном их прошлых постов.

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

Далее формируем строку, которую понимает VyOS, шлем реквест и сохраняем конфиг. Зарезервированную задачу удаляем.

    beanstalk = beanstalkc.Connection(host='localhost', port=11300)


    while True:

        job = beanstalk.reserve()

        task = json.loads(job.body)


        if task['type'] == 'interface_down':

            t = Template(SHUT_INTERFACE)

            config = t.render(INTERFACE_ID = task['interface'])

            print(config)


        elif task['type'] == 'password_failed':

            t = Template(ADD_ADDRESS)

            config = t.render(ADDRESS = task['from'])

            print(config)


        else:

            print('Type not supported')


        prepared_config = prep_config(config, API_KEY)

        r = post_config(task['host'], prepared_config)

        r = save_config(task['host'], API_KEY)


        job.delete()


Весь код можно глянуть по ссылке выше.

Запускаем скрипт. Первым таском добавляем адрес в адрес группу, вторым - выключаем порт. Responder работает.

[root@localhost ~]# python responder.py 


[{"op": "set", "path": ["firewall", "group", "address-group", "BAD_GUYS", "address", "10.0.0.1"]}]

Returned result is "{"success": true, "data": null, "error": null}"

Saving configuration...

Returned result is "{"success": true, "data": "Saving configuration to '/config/config.boot'...\nDone\n", "error": null}"


[{"op": "set", "path": ["interfaces", "ethernet", "eth1", "disable"]}]

Returned result is "{"success": true, "data": null, "error": null}"

Saving configuration...

Returned result is "{"success": true, "data": "Saving configuration to '/config/config.boot'...\nDone\n", "error": null}"


В итоге мы реализовали все три "функции" в рамках одного хоста. 
  • Устройство шлет логи
  • Parser их принимает, формирует задачи и кладет их в очередь
  • Responder подхватывает задачи и выполняет их

И че? Да пока особо ничего. Теперь у нас есть модульность, а значит можно разделить и размножить функции. Понятно, что данная схема вполне справится с логами одной коробки. Но вот, что если устройств много. Плюс, отказоустойчивость тоже не на уровне. Гибель VMки хоронит весь сервис.

Второе приближение

Абсолютно понятно, что нужно разделять и множить компоненты. В итоге у нас получатся функциональные группы/уровни, которые будут выполнять только определенный тип работы. Добавляя ноды в группу увеличиваем пропускную способность. Меняя ноды по одному обновляем код. Становится понятно, что нам нужно как-то балансировать сообщения между нодами. Придется прятать группы за балансировщиками нагрузки...


В плане сети, все это выглядит довольно тривиально.

HAPROXY

Я вообще люблю F5. Даже начинал готовиться к экзамену как-то. Но он дорогой, поэтому попробуем HAPROXY. Если честно, HAPROXY мне понравился, но взлетело не с первого раза.

Нам нужно настроить VRRP между нодами и настроить два VIPa (для парсеров и очереди). Для того чтобы определить с какого устройства пришел запрос я использую PROXY протокол (send-proxy). В примере запуска ниже будет видно.

yum install haproxy keepalived

/etc/sysctl.conf
net.ipv4.ip_nonlocal_bind=1
sudo sysctl -p

vi /etc/keepalived/keepalived.conf

vrrp_script chk_haproxy {     
  script "killall -0 haproxy" 
  interval 2 
  weight 2
}
vrrp_instance VI_1 {
  interface ens3
  state MASTER
  virtual_router_id 51
  priority 110
  virtual_ipaddress {
    10.10.10.10/24
  }
  track_script {
    chk_haproxy
  }
}

systemctl start keepalived
systemctl enable keepalived

defaults
    mode tcp

frontend PARSER
   bind 10.10.10.10:10514
   default_backend PARSER_NODES

backend PARSER_NODES
   server PARSER_NODE1 10.10.10.111:10514 send-proxy
   server PARSER_NODE2 10.10.10.112:10514 send-proxy

frontend QUEUE
   bind 10.10.10.10:11300
   default_backend QUEUE_NODES

backend QUEUE_NODES
   server QUEUE_NODE1 10.10.10.121:11300
   server QUEUE_NODE2 10.10.10.122:11300

setsebool -P haproxy_connect_any 1

systemctl start haproxy
systemctl enable haproxy

Queue

Я просто буду использовать две виртуалки с docker контейнерами.

yum install -y yum-utils

yum-config-manager \
   --add-repo \
   https://download.docker.com/linux/centos/docker-ce.repo

yum install -y docker-ce docker-ce-cli containerd.io

systemctl start docker
systemctl enable docker

docker run -d --name beanstalkd -p 11300:11300 webplates/beanstalkd

Parser/Responder

Тут просто копирую и запускаю уже написанные файлы отсюда.

Тестируем

- На устройстве (VyOS) пытаемся подобрать пароль.
- Девайс шлет syslog сообщение
- HAPROXY получает его на PARSER VIPе и балансирует в нашем случае к одной из PARSER нод 10.10.10.112:10514
- Хост получает сообщение, понимает что нужно сформировать таск и отправляет его в очередь. Обратите внимение на первую строку в payload (PROXY TCP4...). Тут видно кто на самом деле был отправитель.

[root@PRS2 ~]# python2 parser.py 

('PROXY TCP4 10.10.10.201 10.10.10.10 44170 10514',

'<13>Aug 26 21:59:03 vyos vyos: message repeated 4 times: [  Aug 26 13:55:13 vyos sshd[2755]: Failed password for root from 10.10.10.132 port 47336 ssh2]')


{'from': '10.10.10.132', 'host': '10.10.10.201', 'type': 'password_failed'}



- PRS2 шлет данные на VIP QUEUE, HAPROXY его кладет на какую-то из beanstalk нод. 
- Одна из Responder виртуалок первая забирает задачу и добавляет адрес злоумышленника в address-group.

[root@RESP2 ~]# python2 responder.py 


[{"op": "set", "path": ["firewall", "group", "address-group", "BAD_GUYS", "address", "10.10.10.132"]}]

Returned result is "{"success": true, "data": null, "error": null}"

Saving configuration...

Returned result is "{"success": true, "data": "Saving configuration to '/config/config.boot'...\nDone\n", "error": null}"


Вместо заключения

Получилось хоть и довольно сумбурно, но клево. Безусловно сам код нельзя использовать в проде, он написан только для того, чтобы сработать один раз для иллюстрации самого подхода. 

Но вот сама архитектура получилась довольно масштабируемой, отказоустойчивой и доступной. Прям как мы любим. При необходимости в любой из блоков добавляем нужное количество идентичных нод, добавляем их в VIP и все. Обновление кода также проходит гладко. Просто обновляем ноды по одной в каждой группе.


Комментариев нет:

Отправить комментарий