Привет, Хабр! Сегодня поделюсь, как мы с коллегами решали небольшую задачу по автоматизации управления списками доступов на пограничных маршрутизаторах. Исходные данные просты: 100+ маршрутизаторов, на которых необходимо поддерживать в актуальном состоянии правила NAT. Звучит несложно, но, как водится, есть свои нюансы.

Часть 1 — Концепция (вы находитесь тут)
Disclaimer
В силу определенных обстоятельств не могу привести полностью код реализованного решения. Все конфигурации, адреса, названия устройств, площадок и департаментов – вымышлены.
Про существующий ландшафт и задачу, которая перед нами стояла
Вернемся в недавнее прошлое, когда мы были молоды и полны сил, и нам хватало времени для ручного выполнения рутинных задач. Вот что у нас было:
Сетевое оборудование: маршрутизаторы Cisco с IOS-XE (на самом деле, вендор не важен, конечная реализация не сильно зависит от производителя). На каждом из них настроена политика доступа в Интернет, которой мы хотим управлять. Выглядит такая политика примерно так:
# Описания сервисов object-group service OG_DNS_PORTS udp eq domain tcp eq domain object-group service OG_WEB_PORTS tcp eq http tcp eq 443
# Описания сетей и хостов object-group network SOME_SERVER host 10.0.0.10 object-group network OG_WIFI_NETWORK 192.168.111.0 255.255.255.0
# Списки доступа ip access-list extended ACL_COMMON_NAT permit icmp any any permit object-group OG_DNS_PORTS any any ip access-list extended ACL_FF_WIFI_NAT permit object-group OG_WEB_PORTS object-group OG_WIFI_NETWORK any ip access-list extended ACL_TEMP_NAT permit ip object-group SOME_SERVER any
# Политика доступа route-map RM_NAT permit 10 match ip address ACL_COMMON_NAT route-map RM_NAT permit 20 match ip address ACL_WIFI_NAT route-map RM_NAT permit 30 match ip address ACL_TEMP_NAT
Разумеется, в реальности политики доступа намного сложнее и содержат в десятки раз больше правил.
Системы учета:
Вся информация об устройствах хранится в Nautobot
Актуальные бэкапы конфигураций хранятся в корпоративной системе версий
Описания правил доступа в интернет (откуда, куда, номер заявки и пр.) хранятся в Excel на сетевом диске
Способ конфигурирования: на один маршрутизатор изменения вносятся руками через CLI, на несколько маршрутизаторов - используя Ansible и стандартный playbook, в котором каждый раз меняются ACL и object-groups.
Примеры запросов на доступ со звездочкой:
Просят добавить специфичный доступ в интернет из сети Wi-Fi. Адреса сетей на разных площадках отличаются, значит в плейбуке надо реализовать алгоритм формирования адреса источника. Ну или все делать руками.
Просят открыть временный доступ в Интернет (на неделю) с конкретного хоста. Приходится создавать себе напоминание, чтобы не забыть удалить лишние ACL.
Чаще всего просят открыть доступ к адресам FQDN. Увы, обычные маршрутизаторы Cisco умеют работать только с IP-адресами. Приходится ставить на мониторинг A-записи DNS и при ее обновлении автоматически создавать тикет для изменения ACL. При получении тикета сетевой инженер должен добавить доступ к новому IP-адресу, а старую запись в ACL удалить.
На одном маршрутизаторе организован site-to-site VPN, поэтому надо запретить трансляцию определенных адресов
Это была вполне рабочая схема. Пользователи и системы мониторинга регулярно создавали заявки, сетевые инженеры с помощью Ansible изменяли ACL на маршрутизаторах и с помощью Excel вели документацию с комментариями. Но вероятность человеческой ошибки в этой схеме оставалась высокой. Ведь у сетевого инженера есть другие, более интересные задачи. Отвлекшись на них, можно забыть описать доступ в общем файле, не удалить устаревшие правила или проигнорировать тикет от системы мониторинга. А значит, напрашивается необходимость максимально автоматизировать процесс и избавить сетевой отдел еще от одной рутины.
Так уж вышло, что готового решения для наших условий не существует. Имей мы на площадках маршрутизаторы/МСЭ, изначально построенные для централизованного управления, этой статьи бы не было. Но имеем то, что имеем, так что автоматизацию решено было делать свою собственную.
В процессе автоматизации необходимо было определить и реализовать несколько важных аспектов:
Место хранения и формат исходных данных.
Изменение исходных данных при наступлении определенных событий.
Доставка изменений политик на маршрутизаторы.
В первой части статьи расскажу про выбранную архитектуру: в чем состоит концепция, где и в каком виде было решено хранить исходные данные для формирования политик.
Начинаем с места хранения
Excel в качестве способа хранения данных нас категорически не устраивал, а из уже имеющихся и легкодоступных было всего два – Nautobot и Gitlab. Рассмотрим особенности каждого варианта.
Разработчики Nautobot создали целую экосистему плагинов, которые значительно расширяют функционал системы. Одним из таких плагинов является Nautobot Firewall Models. Предназначено расширение для учета политик и списков сетевого доступа L4. Вполне дружелюбный пользовательский интерфейс позволяет добавить сервисы, объекты и группы объектов, IP-адреса и FQDN, политики как для zone-based firewall, так и для организации NAT. И все это уже интегрировано в нашу IPAM-систему. Данные можно обработать внутри Nautobot с помощью скриптов, а можно получить по API и ��спользовать любые внешние интеграции. Из минусов – отсутствие функционала ограничения правил доступа по времени.
В GitLab все проще и сложнее одновременно. В текстовых файлах можно хранить какую угодно информацию о правилах и политиках, «из коробки» есть история изменений и права доступа. Все данные легко получить из репозитория стандартными способами. Можно встроить скрипты в репозиторий и обрабатывать политики, используя GitLab CI/CD. Из минусов – текстовые файлы не очень удобны для презентации обычным пользователям.
Так как вариант с GitLab предоставлял больше свободы и гибкости, в итоге был выбран именно он. А это сразу же стало сигналом, что решать задачу мы будем в стиле "Network as Code".
Про "Network as Code"
Как известно, NaC является частным случаем концепции "Infrastructure as Code" и требует внедрения практик NetDevOps. В соответствии наследию Cisco мы для себя определяем следующие принципы "Network as Code":
Данные, на основе которых генерируется конфигурация устройств, хранятся в текстовом виде в системе контроля версий. В нашем случае это репозиторий GitLab.
Репозиторий с данными является единой точкой истины. Изменения вносятся только в него, ручные правки на сетевых устройствах не приветствуются.
Должен быть организован процесс доставки целевой конфигурации на сетевые устройства. В современном мире для этого используется API (RESTCONF, NETCONF), мы же пока обойдемся стандартным CLI
Что же нам надо для реализации указанных принципов? Всего-то ничего: описать правила доступа в vendor-agnostic виде, разработать кодовую базу для конвертации правил в vendor-specific конфигурацию, реализовать CI/CD для доставки конфигурации на маршрутизаторы.
Dataflow и структура репозитория
При подготовке и деплое конфигураций на сетевые устройства необходимо определить множество моментов: кто может вносить изменения, как проверять данные, каким образом формировать команды для отправки на маршрутизаторы. С учетом неизбежной мультивендорности нет других вариантов, как разделить процесс работы с политиками на две части:
Vendor-agnostic – описания политик, правила заполнения данных и пользовательский интерфейс не должны зависеть от производителя;
Vendor-specific – подготовка команд для отправки и сам деплой зависят производителя и версии ОС на маршрутизаторе.
В итоге диаграмма распространения политик доступа стала выглядеть следующим образом:

Структура папок репозитория стала выглядеть так:
┌─ansible │ └─ все, что необходимо для отправки конфигураций на устройства ├─code │ └─ все, что необходимо для подготовки конфигураций перед отправкой ├─intended_state │ ├─locations │ │ ├─ MSK-001_network_groups.yml │ │ ├─ MSK-002_network_groups.yml │ │ ├─ SPB-001_network_groups.yml │ │ └─ SPB-001_rules.yml │ ├─routers │ │ └─ MSK-001-BRD-0_rules.yml │ ├─ common_service_groups.yml │ ├─ common_network_groups.yml │ ├─ store_policies.yml │ ├─ store_rules.yml │ ├─ warehouse_policies.yml │ └─ warehouse_rules.yml └─templates └─cisco ├─ network_groups.j2 ├─ rules.j2 ├─ policies.j2 └─ service_groups.j2
Папки ansible и code нам сейчас не особо интересны, они будут использоваться на последних этапах для подготовки и отправки команд на сетевые устройства.
В папке templates будем хранить шаблоны Jinja для каждого вендора, предназначенные для генерации vendor-specific конфигураций. Для каждого типа содержимого политики предназначен отдельный шаблон.
Папка intended_state - непосредственно политики доступа в формализованном виде. Давайте разберемся, почему в ней так много YAML-файлов, и как внутри организованы данные.
Vendor-agnostic описания правил доступа
Для удобства каждый файл отвечает только за один тип данных. Напомню, их у нас всего четыре:
описание сервисов,
описание хостов и сетей,
списки доступа,
политики доступа.
Если файлов много, как определить какие именно данные в нем хранятся и к каким сетевым объектам они относятся? Ведь политики доступа могут отличаться для разных типов локаций, для разных площадок или даже для разных маршрутизаторов на одной площадке. За это отвечает блок _metadata, который содержится в каждом файле.
# intended_state/store_policies.yml _metadata: description: Политика NAT для магазинов schema: policy filter: location_type: - store weight: 1000 is_active: true ...
Поле schema определяет тип данных. В секции filter мы можем указать тип локации (например, магазин или склад), конкретные площадки или список hostname маршрутизаторов. А если какому-то маршрутизатору соответствует несколько файлов с данными? В таком случае для разрешения конфликта более приоритетным будут данные из файла, в котором вес (weight) наиболее высокий.
Далее в файле располагаются правила или описания в соответствии с указанной схемой. Ниже показан пример, как в четырех файлах описать простенькую политику доступа, в которой для Wi-Fi сети (172.18.100.0/24) открыты порты http/https, а всем остальным – только ICMP и DNS:
# intended_state/store_policies.yml ... name: RM_NAT type: route-map lines: 10: rule: ACL_COMMON_NAT action: permit 20: rule: ACL_WIFI_NAT action: permit
# intended_state/store_rules.yml ... rules: ACL_COMMON_NAT: lines: - action: permit source: any destination: any services: protocol: icmp - action: permit source: any destination: any services: group: OG_DNS_PORTS ACL_WIFI_NAT: lines: - action: permit source: OG_WIFI_NETWORK source_type: group destination: any services: group: OG_WEB_STANDARD_PORTS
# intended_state/common_network_groups.yml ... network_groups: OG_WIFI_NETWORK: networks: - 172.18.100.0 255.255.255.0
# intended_state/common_service_groups.yml ... service_groups: OG_WEB_STANDARD_PORTS: tcp: - eq 80 - eq 443 OG_DNS_PORTS: udp: - eq 53 tcp: - eq 53
Мы уже знаем, что, используя более высокий вес, мы можем переопределить правила или всю политику для конкретной площадки. А если необходимо не заменить, а дополнить правила? Для такой задачи предусмотрена директива extend. Укажем в следующем примере, что для площадки SPB-001 нам надо добавить в правило ACL_WIFI_NAT разрешение трафика для группы OG_SPECIFIC_SERVER:
# intended_state/locations/SPB-001_rules.yml _metadata: description: Правила NAT для SPB-001 schema: rules filter: locations: - SPB-001 weight: 9000 is_active: true rules: ACL_WIFI_NAT: extend: true lines: - action: permit source: any destination: OG_SPECIFIC_SERVER destination_type: group services: group: OG_SPECIFIC_PORTS
Дополнительные данные для автоматизации
Согласно нашему dataflow, источником изменений в YAML-файлах может быть не только человек, но и внешний скрипт. Добавим в схему данных директивы, позволяющие внешним скриптам реагировать на наступление событий двух типов:
истечение срока действия правила,
изменение IP-адреса в DNS.
В следующем примере создано правило ACL_TEMP_NAT с одной позицией, действующей до конца 2024 года. Теперь легко организовать автоматическую проверку данных и удаление истекшей позиции из файла:
# intended_state/store_rules.yml ... rules: ACL_TEMP_NAT: lines: - action: permit destination: OG_SPECIFIC_SERVER destination_type: group services: protocol: ip expiry: 2024-12-31
Для слежения за изменениями записей в DNS нам необходимо хранить FQDN-имя в описаниях хостов. Использовать для этого имя object-group не очень удобно, так что добавим соответствующий параметр в описание хоста. Теперь можно просто сравнить вывод nslookup и список hosts в файле и в случае несоответствия внести правки. Пример ниже показывает, как привязать адрес хоста к его имени:
# intended_state/locations/SPB-001_network_groups.yml ... OG_SPECIFIC_SERVER: fqdn: someserver.ru hosts: - 10.0.0.10
Что дальше?
К этому моменту мы:
создали репозиторий в GitLab
создали структуру файлов, содержащих всю необходимую информацию для формирования политик доступа
предусмотрели данные, нужные нам для автоматизации изменения политик
Теперь нам нужен был код, который обеспечит этап deploy:
проверит внесенные данные (мы же помним, что их вносит человек, способный на ошибки?),
на основе этих данных сформирует ожидаемую конфигурацию для каждого устройства,
если новая конфи��урация не соответствует текущей, вычислит разницу,
отправит на маршрутизатор команды, необходимые для приведения конфигурации устройства к ожидаемому виду.
И, конечно, внешние скрипты, которые удалят временные правила или поменяют IP-адреса при обновлении DNS.
Обо всей этой кодовой базе читайте в следующей части. Stay tuned…
