Доброго времени суток! Все мы в айти хотим жить без забот, без атак на продукт и проблем с доступом у клиентов. Не хотим, чтобы левые люди и роботы попадали в нашу сеть и имели доступ к эндпоинтам. Для этого древними сисадминами придуман великий и ужасный файрвол. В современных продуктах вроде того же Fortigate можно удобно и крайне точечно рутировать трафик, давать лимитированный доступ по конкретным протоколам и тд.
Но сразу проблема - заполнять это добро из коробки надо ручками. Когда ты настраиваешь корп сеть это не так больно, но когда в твою сеть надо пустить клиентов, у которых много машин с разными адресами, им нужно попасть в разные компоненты твоего продукта, то становится грустно. Особенно, когда это обязанность ложится на руки инженера, который мог бы и более интересными вещами заняться.
Но инженер, тем более девопс, на то и инженер. Видишь что-то, что движется - автоматизируй. Не движется? Двигай и автоматизируй!
Двигай и автоматизируй!
Кратко о нашем кейсе: в качестве файрвола стоит Fortigate; запрос на добавление клиентов через наш портал попадает в Jira в виде таски в проекте CHG (Change). Изначально инженеры брали на себя эту задачу, ходили в fortigate, создавали клиенту группу или находили уже существующую, добавляли в нее адреса, а группу в специальную полиси, ведущую к части продукта по определенному протоколу (например, RabbitMQ, TCP, SSH и тд)
Можно сразу заметить, что задача рутинная, не особо сложная и повторяемая всегда, но при этом, при большем количестве добавляемых адресов, выполнение может занять время.
Наблюдательный инженер мог заметить также, что возможные пути решения и автоматизации этой рутины лежат на поверхности - fortigate и Jira имеют API . Бинго!
Берем в ручки мозги, навыки программирования и документацию по API этих двух сервисов.
Немного черной магии, говнокода и всё взлетит. Хотя, зачем напрягаться? Тем более наш инженер в вакууме наверняка использует что-то более высокоуровневое, например, для того же IaC. Копаем в глубины интернета и находим еще кое-что очень любопытное - и для Jira , и для fortigate у Ansible существуют коллекции (фактически, высокоуровневая обертка над API).
Ну, теперь мы обязаны это собрать!
Обсудим воркфлоу нашей будущей подделки и начнем шаг за шагом воплощать ее в жизнь:
Кто-то (в моем случае это будет координатор, первая линия поддержки) получает запрос клиента , похожий на "Хочу доступ с моего сервера (адрес прилагается) до части вашего продукта (назовем её "Закупка кексиков")"
Он же создает по запросу таск в Jira, с формочкой, в которой будут все нужные нам для операции поля (название компании клиента, адреса серверов, желаемый продукт и в нашем случае еще окружение прод/пре-прод )
После проверки на валидность задача маркируется лейблом "whitelisting" и ее статус переводится в "Ready to Dev" (лейбл и статус можно ставить любой, это будет конфигурироваться переменной в Ansible)
Scheduled task на AWX ходит раз в 30 минут в Jira, ищет задачи с нужным лейблом и статусом и делает черную магию.
После отписывается в таску и на его коммент натравлена автоматизация Jira, которая таску благополучно закрывает или тэгает автора, если что-то пошло не так
Вроде просто и прозрачно. Можно теперь пройтись по шагам самого Ansible playbook
Ищем нужные нам таски
Забираем из них нужные данные
Создаем объекты адресов в Fortigate
Создаем или находим группу для клиента. Добавляем адреса туда
Саму группу кладем в группу продукта/компонента, которую заранее положили в нужную полиси
Отписываем коммент об успешном или неуспешном запуске в Jira
Нам понадобится: Python 3, Ansible, ansible collections (community.general , fortinet.fortios), AWX инстанс или же любое другое окружение с зависимостями, которое будет кронтабом запускать плейбук по расписанию
Крутим Ansible, автоматизируем
Начнем шаг за шагом описывать нашу автоматизацию в Ansible:
Ищем таски к выполнению и забираем их номера
---
# tasks for fetch whitelisting tasks from Jira
- name: Search for an whitelisting issues
community.general.jira:
uri: '{{ jira_server }}'
username: '{{ jira_user }}'
password: '{{ jira_api_token }}'
operation: search
project: "{{ target_project }}"
maxresults: 5 #Количество задач, обрабатываемых за запуск
#меньшее количество проще логировать
jql: 'project="{{ target_project }}" AND labels="{{ target_label }}" AND status="{{ target_status }}"'
args:
fields:
lastViewed: null
register: issues
- name: Search result
debug:
msg: "Issues wasn't founded"
when: issues.meta.total == 0
- name: Start Whitelisting
include_tasks: whitelisting.yml
with_items: '{{ issues.meta.issues }}'
when: issues.meta.total > 0
Здесь можно заметить мою говнокод черную магию. Циклы, особенно вложенные, в Ansible это проблемная тема, поэтому в данном куске мы сначала собираем задачи к исполнению, а потом прокидываем их item'ом во вторую таску (whitelisting.yaml) нашей роли, в которой уже происходит вайтлистинг . В данной реализации все вполне читаемо и удобно, главное не запутаться в контексте использования item в последующих шагах роли.
Получаем информацию из задачи и достаем данные заполненной формы из custom field'ов
- name: Retrieving payload from issues
community.general.jira:
uri: '{{ jira_server }}'
username: '{{ jira_user }}'
password: '{{ jira_api_token }}'
project: '{{ target_project }}'
operation: fetch
issue: '{{ item.key }}'
register: target
- name: Initiate WhiteList
set_fact:
target_data:
client: '{{ target.meta.fields.customfield_10863.replace(" ", "") }}'
ips: '{{ target.meta.fields.customfield_11092.replace(" ", "").split(",") }}'
enviroments: '{{ target.meta.fields.customfield_10865[0].value }}'
components: '{{ target.meta.fields.components[0].name }}'
#Название полей в полученных данных зависит от вашей Jira
#Проверьте соотвествие заранее с помощью таска на получение таски
Проверяем полученные адреса фильтром ipaddr и если данные невалидны - пишем коммент в Jira о провале с просьбой проверить инстанс или задачу и фейлим плейбук
- name: Set wrong ips counter
set_fact:
wrong_ips: 0
- name: Validate IPs with ipaddr filter
set_fact:
wrong_ips={{ wrong_ips | int + 1 }}
with_items: " {{ target_data.ips }} "
when: not (item | ipaddr)
- name: Fail play if we have wrong IPs in payload
block:
- name: Comment on issue
community.general.jira:
uri: '{{ jira_server }}'
username: '{{ jira_user }}'
password: '{{ jira_api_token }}'
issue: '{{ item.key }}'
operation: comment
comment: "Task failed. Maybe IPs invalid format or other causes. Please, check logs and correct payload/fix instance"
- name: Fail the play
fail:
msg: "Stop the play because payload wrong or we have some problems on AWX instance or local laptop"
when: wrong_ips | int > 0
В Jira также прикручена автоматизация, которая после данного коммента дублирует его с упоминанием автора и ему приходит уведомление
Cоздаем объекты адресов в fortigate. АРI Fortigate требует указывать тип адреса, так что пришлось пошаманить с тернарными операциями
- name: Create IP ADDRESSES
fortios_firewall_address:
vdom: '{{ vdom }}'
access_token: '{{ fortigate_token }}'
firewall_address:
name: 'IP-{{ item }}'
type: '{{ (item is search("-"))|ternary("iprange","ipmask") }}'
subnet: '{{ (item is search("-"))|ternary(omit,item+((item is search("/"))|ternary(omit,"/32"))) }}'
start_ip: '{{ (item is search("-"))|ternary(item.split("-")[0],omit) }}'
end_ip: '{{ (item is search("-"))|ternary(item.split("-")[1],omit) }}'
associated_interface: '{{ interface }}'
comment: '{{ issue }}'
state: "present"
with_items: '{{ target_data.ips }}'
register: IPs
Проверяем, есть ли для нашего клиента группа, и создаем, если таковой нет. fortigate не дает создать пустую группу, так что кладем в нее первый адрес из списка (либо можно добавить Null адрес в Fortigatе, тут на ваш выбор)
- name: Check if client group already exists
fortinet.fortios.fortios_configuration_fact:
vdom: '{{ vdom }}'
access_token: "{{ fortigate_token }}"
selector: firewall_addrgrp
params:
name: "GROUP_{{ target_data.client | upper }}"
ignore_errors: true
register: group_not_exists
- name: Create CLIENT GROUP
fortinet.fortios.fortios_firewall_addrgrp:
vdom: '{{ vdom }}'
state: "present"
access_token: '{{ fortigate_token }}'
firewall_addrgrp:
allow_routing: "disable"
category: "default"
color: "5"
comment: '{{ issue }}'
name: 'GROUP_{{ target_data.client | upper }}'
type: "default"
uuid: "GROUP_{{ target_data.client | upper }}"
visibility: "enable"
member:
- name: 'IP-{{ IPs.results[0].item }}'
when: group_not_exists.failed == true
Добавляем адреса в нужную группу
- name: Put target IPs in client group
fortinet.fortios.fortios_firewall_addrgrp:
vdom: '{{ vdom }}'
state: "present"
access_token: '{{ fortigate_token }}'
member_path: member:name
member_state: present
firewall_addrgrp:
allow_routing: "disable"
category: "default"
color: "5"
comment: '{{ client_group_info.meta.results[0].comment + "," + issue }}'
name: 'GROUP_{{ target_data.client | upper }}'
type: "default"
uuid: "GROUP_{{ target_data.client | upper }}"
visibility: "enable"
member:
- name: 'IP-{{ item.item }}'
with_items: ' {{ IPs.results }} '
loop_control:
label: ' {{ IPs.results }} '
Добавляем группу в группу (простите за тавтологию) нужного нам компонента
- name: Put target client group in external COMPONENT GROUP in policy
fortinet.fortios.fortios_firewall_addrgrp:
vdom: '{{ vdom }}'
state: "present"
access_token: '{{ fortigate_token }}'
member_path: member:name
member_state: present
firewall_addrgrp:
allow_routing: "disable"
category: "default"
color: "5"
comment: '{{ component_group_info.meta.results[0].comment + "," + issue }}'
name: 'COMPONENT_{{ target_data.components | upper }}'
type: "default"
uuid: "COMPONENT_{{ target_data.components | upper }}"
visibility: "enable"
member:
- name: 'GROUP_{{ target_data.client | upper }}'
Устанавливаем лейблы для поиска задачи и пишем коммент об успешном запуске, в котором выводим результат (какие адреса в какую группу и для какого компонента добавили)
- name: Set the labels of created group for trace
community.general.jira:
uri: '{{ jira_server }}'
username: '{{ jira_user }}'
password: '{{ jira_api_token }}'
issue: '{{ item.key }}'
operation: edit
args:
fields:
labels:
- whitelisting
- 'GROUP_{{ target_data.client | upper }}'
- autocompleted
- name: Comment on issue
community.general.jira:
uri: '{{ jira_server }}'
username: '{{ jira_user }}'
password: '{{ jira_api_token }}'
issue: '{{ item.key }}'
operation: comment
comment: " {{ 'AutoWhitelisted for ' + target_data.components | upper + ' using, created/updated GROUP_' +
target_data.client | upper + ' group in firewall. Please check the results \n List of whitelisted IPs:\n' + target_data.ips | join(',') | replace(',', ',\n')}} "
Вернемся из кода в реальную жизнь для небольшой наглядности. Форма в задачах Jira имеет в нашем случае подобный вид
Информация о результате в виде коммента ( в тестах использовался мой аккаунт и токен):
Вот и всё!
Вуаля! Жизнь удалась, рутины стало меньше. Осталось только запушить эту роль и плейбук к ней в AWX или любую другую систему с Ansible , поставить schedule и радоваться. В нашей компании данная вундерваффля уже работает на пре-прод окружении и достаточно хорошо себя чувствует, справляясь с работой, которую человек может делать (при большем количестве адресов) минут 20, за считанную минуту. От наших координаторов требуется просто создать задачу, которую они и так заводили, но теперь просто с формой, и поменять статус после проверки.
Благодарю за внимание, надеюсь вам было полезно или как минимум интересно.В коде есть пара шагов, неописанных выше, но все они имеют больше характер дополнительного трейса. Ниже будет приведена ссылка на репозиторий с кодом и инструкцией по запуску, если хочется поглядеть как оно работает или даже использовать у себя. Удачной автоматизации!
Repo: https://github.com/devops-engineer-gaming/fortigate_whitelisting/tree/master/whitelisting
P.S. Картинки котов взяты из группы в ВК "паблик для работников 5/2"