Мониторим события PortSecurity коммутаторов Cisco в 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 = public
    
    [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.


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



    т.е. четко видно, что на коммутаторе с именем 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:


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


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

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 11

      0
      Прошу прощения, а чем 802.11x не угодил? Тем более в условиях когда найти адаптер не поддерживающий смену MAC стало нетривиальной задачей.
        0
        Да как то подумали поигрались и отказались. Очень много проблем вырисовывается, причем в основном организационного характера. У нас отдельные команды для сетей для windows инфраструктуры и для ИБ. Чтоб реализовать .1х это надо всем вместе поработать, сделать, отшлифовать (выбрав кредит доверия пользователей) и потом ответственность друг на друга перекладывать если что случилось.
        А возни сколько новой на простое подключение устройств?
        А еще принтеры, телефоны, видеокамеры, сенсоры температуры, СКУД итп устройства которые если и поддерживают .1х то как-то по своему а такого добра у нас наверное две/трети от всех устройств.

        в общем, к этому аду мы, в принципе, готовы, но пока ИБ не давит стараемся туда сильно не нырять :)
          0
          У меня первый проект был во времена XP когда поддержка 802.11x появлялась только при установке цискиного wifi supplication, вот тогда был ад :-) сейчас все в разы проще, включая ответ на вопрос «что делать с теми, кто не поддерживает» и проект был на 8к юзеров :-):-) не все так страшно, как может показаться, а если есть бюджет, можно еще и в Cisco ISE поиграть, если уж вокруг одна циска :-)
            0
            Вокруг уже больше хуевеев и в перспективе булаты и маяки :)
            в хуевеях, кстати, пробовал подобный портсекюрити настроить. Но его наркоманы писали. Каждый раз при отключении порта залипшие маки слетают. т.е. выдернул комп — мак слетел, вставил другой комп — новый мак выучился и стал легитимным O_o
              0
              Это что? У них «sticky» неотключаемое?
                0

                Нет, все не так. Нашел свои старые записи на этот счет:


                1. dynamic режим сбрасывает все выученные маки при выключении порта (и в cisco так же). Таким образом этот режим бесполезен. Приходит хакер, выдергивает комп из стены, порт выключается, маки сбрасываются, хакер подключается
                2. sticky режим прибивает мак намертво к порту. Если этот прибитый мак появляется на другом порту — ничего не происходит. Коммутатору мак известен и он не генерирует никаких событий (трапов и логов). При этом в новом порту мак не работает, потому что он прибит к старому. Переезд пользователя превращается в ад, но от хакера с новым маком поможет, да.
                3. очистить sticky адрес непросто. Пока найдено два варианта и оба плохие.
                  1. undo port-security enable — убирает все настройки, которые потом придется восстанавливать
                  2. undo port-security mac-address sticky — переключает в dynamic режим. Если сразу вернуть sticky, то старые адреса опять залипнут. т.е. приходится действовать так: выключили, угнали мак, включили, проверили, повторили — много тупой ручной работы
        0
        Но можно не заморачиваться. Готовый шаблон с этими компонентами уже лежит в репозитарии вместе с кодом.

        При импорте шаблона:
        Incorrect trigger expression. Host «Template_Cisco_SW_Traps» does not exist or you have no access to this host.

        sudo -H pip install -r requirements.txt

        Но есть же соответствующие пакеты в репозитарии.

        Я так понимаю у Вас python-2.x?
        Почему выбор пал на py-zabbix, а не на более свежий: pyzabbix?
          0

          Большое спасибо за обнаруженную багу в шаблоне. Поправил.


          что касается python2 — то это все писалось довольно давно, безсистемно и на очень поверхностном уровне параллельно с изучением питона. Тем не менее оно очень стабильно работает годами поэтому нет большого смысла что-то исправлять просто ради красоты.
          Сейчас я уже 3-й питон по умолчанию использую конечно.
          А чем отличаются py-zabbix от pyzabbix и кто из них новей я не знаю.
          Какой первый под руку попался тот и пригодился :)


          p.s. С радостью приму PR

            0
            На мой взгляд стоило бы переписать под Py3, хотя бы ради статьи.
            py-zabbix не обновляется больше года и вроде как не поддерживает zbx-4
            pyzabbix последнее обновление 6 мес назад и поддерживает zbx-4
            Теперь шаблон импортировался нормально, осталось разобраться как его применять. 8))
            В статью бы пример добавить…

            PS в статье опечатка?:
            [snmp]
            community = publice

            «е» лишняя
              0

              Применять просто. Навешиваешь шаблон на коммутатор в заббиксе и готово.
              Как настраивать коммутатор описано в главе 1. cisco


              community = publice
              почему опечатка? Это конфиг файл. Каждый туда вписывает свои параметры.
              Конкретно public, который по умолчанию, я всегда меняю на всех своих устройствах.
              Но чтоб не было разночтений поправлю.


              Ок на Py3 перепишу как будет время

                0

                Переписал на python3 и pyzabbix

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

        Самое читаемое