Доброго времени суток! Все мы в айти хотим жить без забот, без атак на продукт и проблем с доступом у клиентов. Не хотим, чтобы левые люди и роботы попадали в нашу сеть и имели доступ к эндпоинтам. Для этого древними сисадминами придуман великий и ужасный файрвол. В современных продуктах вроде того же Fortigate можно удобно и крайне точечно рутировать трафик, давать лимитированный доступ по конкретным протоколам и тд.

Но сразу проблема - заполнять это добро из коробки надо ручками. Когда ты настраиваешь корп сеть это не так больно, но когда в твою сеть надо пустить клиентов, у которых много машин с разными адресами, им нужно попасть в разные компоненты твоего продукта, то становится грустно. Особенно, когда это обязанность ложится на руки инженера, который мог бы и более интересными вещами заняться.

Но инженер, тем более девопс, на то и инженер. Видишь что-то, что движется - автоматизируй. Не движется? Двигай и автоматизируй!

Двигай и автоматизируй!

Кратко о нашем кейсе: в качестве файрвола стоит Fortigate; запрос на добавление клиентов через наш портал попадает в Jira в виде таски в проекте CHG (Change). Изначально инженеры брали на себя эту задачу, ходили в fortigate, создавали клиенту группу или находили уже существующую, добавляли в нее адреса, а группу в специальную полиси, ведущую к части продукта по определенному протоколу (например, RabbitMQ, TCP, SSH и тд)

Можно сразу заметить, что задача рутинная, не особо сложная и повторяемая всегда, но при этом, при большем количестве добавляемых адресов, выполнение может занять время.

Наблюдательный инженер мог заметить также, что возможные пути решения и автоматизации этой рутины лежат на поверхности - fortigate и Jira имеют API . Бинго!

Берем в ручки мозги, навыки программирования и документацию по API этих двух сервисов.

Немного черной магии, говнокода и всё взлетит. Хотя, зачем напрягаться? Тем более наш инженер в вакууме наверняка использует что-то более высокоуровневое, например, для того же IaC. Копаем в глубины интернета и находим еще кое-что очень любопытное - и для Jira , и для fortigate у Ansible существуют коллекции (фактически, высокоуровневая обертка над API).

Ну, теперь мы обязаны это собрать!

Обсудим воркфлоу нашей будущей подделки и начнем шаг за шагом воплощать ее в жизнь:

  1. Кто-то (в моем случае это будет координатор, первая линия поддержки) получает запрос клиента , похожий на "Хочу доступ с моего сервера (адрес прилагается) до части вашего продукта (назовем её "Закупка кексиков")"

  2. Он же создает по запросу таск в Jira, с формочкой, в которой будут все нужные нам для операции поля (название компании клиента, адреса серверов, желаемый продукт и в нашем случае еще окружение прод/пре-прод )

  3. После проверки на валидность задача маркируется лейблом "whitelisting" и ее статус переводится в "Ready to Dev" (лейбл и статус можно ставить любой, это будет конфигурироваться переменной в Ansible)

  4. Scheduled task на AWX ходит раз в 30 минут в Jira, ищет задачи с нужным лейблом и статусом и делает черную магию.

  5. После отписывается в таску и на его коммент натравлена автоматизация Jira, которая таску благополучно закрывает или тэгает автора, если что-то пошло не так

Вроде просто и прозрачно. Можно теперь пройтись по шагам самого Ansible playbook

  1. Ищем нужные нам таски

  2. Забираем из них нужные данные

  3. Создаем объекты адресов в Fortigate

  4. Создаем или находим группу для клиента. Добавляем адреса туда

  5. Саму группу кладем в группу продукта/компонента, которую заранее положили в нужную полиси

  6. Отписываем коммент об успешном или неуспешном запуске в Jira

Нам понадобится: Python 3, Ansible, ansible collections (community.general , fortinet.fortios), AWX инстанс или же любое другое окружение с зависимостями, которое будет кронтабом запускать плейбук по расписанию

Крутим Ansible, автоматизируем

Начнем шаг за шагом описывать нашу автоматизацию в Ansible:

  1. Ищем таски к выполнению и забираем их номера

---
# 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 в последующих шагах роли.

  1. Получаем информацию из задачи и достаем данные заполненной формы из 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
          #Проверьте соотвествие заранее с помощью таска на получение таски
  1. Проверяем полученные адреса фильтром 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 также прикручена автоматизация, которая после данного коммента дублирует его с упоминанием автора и ему приходит уведомление

  1. 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
  1. Проверяем, есть ли для нашего клиента группа, и создаем, если таковой нет. 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
  1. Добавляем адреса в нужную группу

- 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 }} '
  1. Добавляем группу в группу (простите за тавтологию) нужного нам компонента

- 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 }}'
  1. Устанавливаем лейблы для поиска задачи и пишем коммент об успешном запуске, в котором выводим результат (какие адреса в какую группу и для какого компонента добавили)

- 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"

Комментарии (0)