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

Часть 1 — Концепция (вы находитесь тут)
Часть 2 — Код
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
destination: any
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…