Мониторим события PortSecurity коммутаторов Cisco в Zabbix

в 13:13, , рубрики: Cisco, python, zabbix, системное администрирование

Здравствуй уважаемое хабросообщество!

Решился выплеснуть в онлайн пару in-house решений, которые облегчают деятельность сетевиков и прочих ИТ братьев по разуму.

В этой статье речь пойдет о мониторинге событий стандартного (для многих вендоров) механизма защиты от несанкционированного подключения устройств к сети, — механизма PortSecurity.

Решение изначально построено для коммутаторов от компании Cisco, но при желании легко допиливается под любой коммутатор и под любые события, основанные на SNMP-трапах.

Если интересно, добро пожаловать под кут...

Краткий экскурс о чем вообще речь.

Технология PortSecurity работает на основе мак-адресов. Порт доступа коммутатора изучает заданное количество (по умолчанию один) маков для входящего трафика и при появлении нового мак-адреса активизирует защиту сети, блокируя порт. Так же коммутатор может послать SNMP-трап на хост указанный в настройках интерфейса.

Режим блокировки бывает трех типов:

  1. shutdown — выключение порта + snmp-trap
  2. restrict — ограничении входящего трафика с неизвестного мак-а + snmp-trap
  3. protected — ограничении входящего трафика с неизвестного мак-а молча, без trap-а.

В более-менее крупной сети события PortSecurity происходят постоянно и поэтому их весьма полезно мониторить. Система мониторинга, как следует из заголовка — Zabbix.

Поддержка трапов в Zabbix вроде как есть, но пользоваться этим я так и не научился. В итоге сделал свое решение, которое меня полностью устраивает. Собственно все решение — это достаточно простой скрипт-обработчик (trap handler) для пары конкретных SNMP-трапов. Обработчик написан конечно же на python и вызывается стандартным демоном snmptrapd. Код обработчика выложен на github.

Краткий теоретический экскурс закончен, переходим к конкретике.

Механизм мониторинга выстроен и работает по следующей цепочке:

[1. Коммутатор cisco] → [2. демон snmptrapd] → [3. the script] → [4. Zabbix]

В такой же последовательности и пойдет дальнейшее повествование

1. Cisco

На коммутаторах настраиваем host который будет принимать трапы и запускать скрипт

snmp-server enable traps port-security
snmp-server enable traps errdisable
snmp-server host 10.1.0.1 version 2c public

Тут же, отдельно для каждого порта настраиваем PortSecurity.

В примере ниже PortSecurity настроен в режиме для гибридного порта компьютер+телефон. Поэтому указано максимальное количество маков равное двум. Режим блокировки restricted

 switchport port-security maximum 2
 switchport port-security
 switchport port-security violation restrict
 switchport port-security mac-address sticky
 switchport port-security mac-address sticky 1111.11co.ffee vlan access
 switchport port-security mac-address sticky 0000.0000.beef vlan voice

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

errdisable recovery cause psecure-violation
errdisable recovery interval 300

На этом про коммутатор все.

Подробней как настраивать PortSecutity можно прочитать по ссылкам

2. snmptrapd

snmptrapd — это стандартный сервис для обработки snmp-трапов. В Ubuntu ставится командой

> sudo apt install snmptrapd

конфигурация настраивается в файле /etc/snmp/snmptrapd.conf

Каждый тип трапа уникален как для разных вендоров так и разных типов блокировки (restricted|shutdown).

Мы сосредоточимся на двух конкретных:

  1. CISCO-ERR-DISABLE-MIB::cErrDisableInterfaceEvent (1.3.6.1.4.1.9.9.548.0.1.1) — трап посылаемый в режиме shutdown
  2. ciscoPortSecurityMIB::cpsSecureMacAddrViolation (1.3.6.1.4.1.9.9.315.0.0.1) — трап посылаемый в режиме restrict

В конфигурационном файле демона snmptrapd /etc/snmp/snmptrapd.conf пропишем такие строки:

authCommunity   log,execute,net public
traphandle .1.3.6.1.4.1.9.9.315.0.0.1 /etc/zabbix/externalscripts/traphandlers/cisco-psec-traphandler.py
traphandle .1.3.6.1.4.1.9.9.548.0.1.1 /etc/zabbix/externalscripts/traphandlers/cisco-psec-traphandler.py

Далеее, для краткости, эти трапы буду называть по номерами 315 и 548.

3. the script

Здесь я буду последовательно описывать логику написания скрипта, руководствуясь которой можно будет по аналогии писать другие обработчики (трапхэндлеры) для других видов трапов и/или устройств. Кому не сильно интересно что творится под капотом, тот может сразу переходить в следующую главу. Правда, возможно, предварительно имеет смысл немного пробежаться по этому разделу с тем чтобы понимать зачем нужен файл конфигурации скрипта config.ini.

Кстати, наверное, c сonfig.ini и начнем. Для простоты сразу приведу его содержимое

[snmp]
community = publice

[api]
zabbix_url = https://zabbix.acme.loc
zabbix_user = api_ro
zabbix_passwd = neskazhu

[zabbix]
server = 10.1.1.1
port = 10051
zabbix_sender = /usr/bin/zabbix_sender

#the predefined keyname of an item that has to be created for a given host in Zabbix
trapkeyname_disable = ErrDisable
trapkeyname_restrict = ErrRestrict

[logging]
logfile = /var/log/cisco-errdisable-traphandler.log
loglevel = INFO

Как мне кажется конфиг достаточно прозрачен для понимания. Здесь мы задаем snmp-community, уровень логирования и параметры доступа к серверу Zabbix через Zabbix_API и zabbix_sender.

Единственный, возможно непонятный момент — это параметры trapkeyname_disable и trapkeyname_restrict.

Так вот эти параметры соответствуют трапам 548 и 315 и определяют имена ключей для Items (элементов данных) в самом Zabbix.

Идем дальше. Как было сказано выше, наш обработчик принимает на вход два вида трапов: 315 и 548.

В коде они различаются вот таким элементарным условием:

if "548.0.1.1" in trapstr:
    mode = "disable"
    trapkeyname = "trapkeyname_disable"

elif "315.0.0.1" in trapstr:
    mode = "restrict"
    trapkeyname = "trapkeyname_restrict"

else:
    logging.error("Unknown trap. Discarding ...")
    exit(1)

Далее скрипт обращается в Zabbix используя Zabbix_API для того, чтобы по имени или адресу коммутатора узнать мониторим ли мы в принципе трапы от этого коммутатора. Здесь используется модуль ZabbixAPI и пару методов host.get и item.get. Ничего сложного.

Переходим к парсингу трапов.

Интересно, что структура наших двух трапов кардинально различается. Вот смотрите
Пример трапа 548:

switch-20
UDP: [0.0.0.0]->[192.168.99.20]:-2039
DISMAN-EVENT-MIB::sysUpTimeInstance 338:5:51:38.08
SNMPv2-MIB::snmpTrapOID.0 CISCO-ERR-DISABLE-MIB::cErrDisableInterfaceEvent
cErrDisableIfStatusCause.10640.0 9

А это типовой трап 315:

switch-27
UDP: [0.0.0.0]->[192.168.99.27]:-13209
DISMAN-EVENT-MIB::sysUpTimeInstance 342:22:17:16.63
SNMPv2-MIB::snmpTrapOID.0 CISCO-PORT-SECURITY-MIB::cpsSecureMacAddrViolation
IF-MIB::ifIndex.10028 10028
IF-MIB::ifName.10028 FastEthernet0/28
CISCO-PORT-SECURITY-MIB::cpsIfSecureLastMacAddress.10028 0:22:55:88:ee:dd

Кстати, приведенные выше трапы я изобразил в человекочитаемом формате. На самом деле на вход скрипта трапы попадают в формате ASN.1, который выглядит совсем по другому. И именно с этим сырым форматом мы и будем работать.

Вот так те же самые трапы выглядят в сыром виде:

Трап 548:

iso.3.6.1.2.1.1.3.0 21:19:06.72
iso.3.6.1.6.3.1.1.4.1.0 iso.3.6.1.4.1.9.9.548.0.1.1
iso.3.6.1.4.1.9.9.548.1.3.1.1.2.10640.0 9

Трап 315:

iso.3.6.1.2.1.1.3.0 342:22:17:16.63
iso.3.6.1.6.3.1.1.4.1.0 iso.3.6.1.4.1.9.9.315.0.0.1
iso.3.6.1.2.1.2.2.1.1.10028 10028
iso.3.6.1.2.1.31.1.1.1.1.10028 "FastEthernet0/28"
iso.3.6.1.4.1.9.9.315.1.2.1.1.10.10028 "00 22 55 88 EE DD "

Формат ASN.1, хоть и относится к структурированным, но работать с ним далеко не так удобно как с json или xml. Нельзя так просто вытащить нужную информацию по ключу. Нужно изучать отдельно каждый трап и затем считать на пальцах в каком слове и букве прячется нужное значение. Не очень современно конечно, но да ладно, snmp это давно легаси. Возможно gNMI нас всех спасет. Тогда и будем делать красиво. Возращаемся к нашим баранам.

По приведенным трапам видно, что в случае трапа 315 мы легко можем вытащить номер порта и даже мак-адрес, который вызвал срабатывание port-security. И конечно мы это сделаем.

А вот с 548-м все сложнее. Здесь нам доступны только название коммутатора (switch-20) (кстати оно не сохранится если переслать трап на другой хост используя инструкцию forward в snmptrapd), а вместо названия заблокированного интерфейса в нашем распоряжении есть только его индекс — SNMP ifIndex.

Индекс содержится в последней строке нашего трапа iso.3.6.1.4.1.9.9.548.1.3.1.1.2.10640.0 9 и равен числу 10640

Для того, чтобы из ifIndex получить название порта коммутатора, необходимо обратиться к самому коммутатору по SNMP. Этим в скрипте занимается отдельная функция find_ifDesc_from_ifIndex(). И конечно коммутатор должен быть настроен на то чтобы принимать SNMP от нашего хоста причем с тем snmp-community, которое прописывается все в том же config.ini.

Итоговый код парсинга наших трапов выглядит следующим образом:

if mode is "disable":
    trapvalue = traplist[-2]
    ifIndex = trapvalue.split(".")[-2]
    ifName = find_ifDesc_from_ifIndex(ip, ifIndex, snmp_config['community'])

elif mode is "restrict":
    ifName = traplist[7].strip('"')
    mac = ':'.join(traplist[-7:-1]).strip('"')

Итак в результате у нас есть один из двух наборов данных

  1. (имя коммутатора, имя интерфейса, тип трапа)
  2. (имя коммутатора, имя интерфейса, мак-адрес, тип трапа)

И эти данные необходимо передать Zabbix-у.

По какой то причине архитекторы Zabbix не позволяют инжектить данные в систему используя API. Единственный (на момент написания скрипта, а написал я его уже давно) способ сунуть туда произвольные данные — использовать zabbix_sender.

zabbix_sender для нужного hostname передает key:value пару где value — это те самые данные которые мы подготовили, а в качестве ключа необходимо указать предопределенное имя ключа для элементов данных в Zabbix. И ровно это самое имя нужно прописать в config.ini для параметров trapkeyname_х. В качестве value передается имя интерфейса или строка, состоящая из имени интерфейса и мак-адреса.

Установка скрипта

Скрипт может находиться где угодно. Запускается он демоном snmptrapd и к Zabbix никак не привязан. Но лично я держу его в /etc/zabbix/external-scripts просто потому что "а почему бы и нет".

Для функционирования скрипта требуется ряд модулей, которые перечислены в файле requirements.txt.

Для установки необходимых модулей достаточно запустить команду:

sudo -H pip install -r requirements.txt

Так же необходимо наличие утилиты zabbix_sender.

В моей любимой Ubuntu она идет отдельным пакетом, который так и называется zabbix-sender, правда с дефисом вместо нижнего подчеркивания. Ну т.е. sudo apt install zabbix-sender

4. Zabbix

В Zabbix для каждого коммутатора, с которого мы хотим получать трапы, нужно создать элементы данных (Items) и по одному триггеру на каждый трап. Как уже говорилось выше, имена ключей для этих элементов данных должны быть прописаны в файле config.ini. Но можно не заморачиваться. Готовый шаблон с этими компонентами уже лежит в репозитарии вместе с кодом.

Так же в Zabbix необходимо создать специального пользователя от имени которого будет происходить взаимодействие скрипта с Zabbix через API. Сгодится простой пользователь с правами read-only для группы с коммутаторами и без доступа к Frontend.

Результат будет выглядеть как то так:

Мониторим события PortSecurity коммутаторов Cisco в Zabbix - 1

т.е. четко видно, что на коммутаторе с именем catalyst100 заблокировался порт Fa0/2 левым мак-адресом 00:22:55:D4:3F:51

Тут, кстати, пригодится один грязный хак. По неведомой причине, в фронтенде Zabbix, в виджете Problems для одноименного поля стоит ограничение в 20 символов на длину строки для значения тригера. Но один только мак-адрес занимает 17 символов, а с названием интерфейса как минимум 23. В общем для полной красоты это ограничение надо поменять. Находится оно в файле:

$ZABBIX_FRONTEND_HOME/include/items.inc.php

Искать вот такой фрагмент:

        switch ($item['value_type']) {
                case ITEM_VALUE_TYPE_STR:
                        $mapping = getMappedValue($value, $item['valuemapid']);
                // break; is not missing here
                case ITEM_VALUE_TYPE_TEXT:
                case ITEM_VALUE_TYPE_LOG:
                        if ($trim && mb_strlen($value) > 20) {
                                $value = mb_substr($value, 0, 20).'...';
                        }

И затем тюнить обе 20-ки. Я поставил 30. Теперь выглядит красиво. Вот наверное и все на этом. Готов ответить на вопросы в комментариях.

p.s. Хочу поделиться одной специфичной для Заббикс фишкой под названием tags. Наверное многие в курсе, а многим просто не интересно поэтому спрятал:

zabbix tags

Смотрите как я настраиваю Actions:
Мониторим события PortSecurity коммутаторов Cisco в Zabbix - 2

tag portsecurity прописан в шаблоне для каждого триггера и теперь условия в Actions
можно записать одной строчкой. Или двумя. Удобнейшая вещь, которой мне раньше сильно не хватало.

p.p.s. Почти готова вторая часть, про разблокировку заблокированных портов из фронденда Заббикс-а

Автор: iddqda

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js