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

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 – подготовка команд для отправки и сам деплой зависят производителя и версии ОС на маршрутизаторе.

 В итоге диаграмма распространения политик доступа стала выглядеть следующим образом:

policy dataflow
policy dataflow

Структура папок репозитория стала выглядеть так:

┌─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:

  1. проверит внесенные данные (мы же помним, что их вносит человек, способный на ошибки?),

  2. на основе этих данных сформирует ожидаемую конфигурацию для каждого устройства,

  3. если новая конфи��урация не соответствует текущей, вычислит разницу,

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

И, конечно, внешние скрипты, которые удалят временные правила или поменяют IP-адреса при обновлении DNS.

Обо всей этой кодовой базе читайте в следующей части. Stay tuned…