Привет, я Devops-инженер в сфере ЖКХ, нами пользуется сейчас больше 8 000 юрлиц. У нас большой парк машин (более двух сотен), и вручную создавать правила и CDB-списки для каждого агента Wazuh и поддерживать их — просто очень сложно. Поэтому мы автоматизировали генерацию пер-агентных списков и правил и их доставку в Wazuh Manager.

Под "пер-агентными" далее я имею в виду:

  • CDB-списки и правила, уникальные для конкретного Wazuh-агента;

  • управляемые централизованно с менеджера;

  • но логически привязанные к одному хосту.

Зачем это надо

Типовая боль при работе с Wazuh - это ручная рутина. Каждый раз, когда в инфраструктуре появляется новый сервер с агентом, и мы хотим чтобы у него были свои уникальные CDB-списки и правила, нужно идти вручную прописывать:

  • whitelist известных процессов, ip адресов и портов;

  • добавлять ссылки на списки в ossec.conf;

  • создавать свои правила.

В итоге можно ошибиться с id правила и допустить коллизию, из-за чего оно не будет применяться или указать неверный путь до CDB-списка и тогда тоже правило работать не будет.

Что мы хотим получить

  • пер-агентные known-apps / known-ips / known-ports / known-protocols (CDB-списки);

  • "умные" правила per agent: логировать неизвестные процессы/протоколы/направления, но не шуметь на разрешённое;

  • хранить всё централизованно на менеджере;

  • ID правил не конфликтуют между агентами (каждому - свой "чанк" из 1000 ID в окне 200000..999999).

По сути, Wazuh в таком виде перестаёт быть "набором XML-файлов" и становится конструктором, где:

  • данные (whitelist’ы) описываются декларативно;

  • правила собираются автоматически;

  • менеджер выступает как точка сборки и доставки политики.

С чего начать

У нас сервера разворачиваются с помощью IaC, описаны они вот так (структура упрощена для понимания общей концепции):

servers:
  - name: server1
    ip: 1.1.1.1
    setup:
      - module: ssh
      - module: security

  - name: server2
    ip: 1.1.1.2
    setup:
      - module: ssh
      - module: security

Блок module - это ansible роль, которая должна быть выполнена на сервере.

Теперь составим план, того как мы будем генерировать CDB-списки и правила:

  • Роль wazuh-manager на хосте с Wazuh Manager прокинет файл конфигурации ossec.conf и файл rules.xml, где будут храниться правила;

  • Роль wazuh-agent выполняет "агентские" таски на сервере с агентом, а "менеджерские" - на сервере с Wazuh Manager через delegate_to.

Начнем с роли wazuh-manager, ее структура:

roles/wazuh-manager/
├── defaults
│   └── main.yml
├── tasks
│   ├── main.yml
│   ├── setup_config.yml      ← тут прокидывается конфигурация
│   └── setup_rules.yml       ← тут прокидывются правила
└── templates
    ├── rules.xml.j2          ← общие для всех агентов правила
    └── wazuh_manager.conf.j2 ← конфигурации wazuh-manager

В этой роли мы храним кофиг для wazuh-manager. Сам конфиг ossec.conf может быть любым, в зависимости от ваших требований к конфигурации, главное что в блоке ruleset должно быть прописано следующее:

  <ruleset>
    <list>etc/lists/agents/common-known-apps</list>
    <list>etc/lists/agents/common-known-ips</list>
    <list>etc/lists/agents/common-known-ports</list>
    <list>etc/lists/agents/common-known-protocols</list>
    <list>etc/lists/agents/common-known-ssh-ips</list>

    <!-- Start agent lists -->
{{ agent_lists_block | default('') }}
    <!-- End agent lists -->
  </ruleset>

Мы прописываем пути на общие для всех агентов CDB-списки и задаем блок <!-- Start agent lists -->, в котором будут храниться все пути на уникальные списки для агентов. Иначе manager их не увидит и не сможет загрузить, а значит что и правила для агентов также работать не будут.

Рассмотрим таску setup_config:

Проверяем, существует ли файл конфигурации Wazuh Manager по пути wazuh_host_conf_path и в переменной wazuhconf_stat получаем значение true/false:

- name: Check if wazuh manager config exists
  become: true
  ansible.builtin.stat:
    path: "{{ wazuh_host_conf_path }}"
  register: _wazuh_conf_stat

Читаем содержимое текущего конфига в base64:

- name: Read existing wazuh manager config
  when: _wazuh_conf_stat.stat.exists
  become: true
  ansible.builtin.slurp:
    path: "{{ wazuh_host_conf_path }}"
  register: _wazuh_conf_raw

Теперь нам нужно декодировать содержимое и вырезать между маркерами блок agent lists и сохранить его в факт agent_lists_block:

- name: Preserve agent lists block from existing config
  when: _wazuh_conf_stat.stat.exists
  ansible.builtin.set_fact:
    agent_lists_block: >-
      {{ ((_wazuh_conf_raw.content
           | b64decode
           | regex_findall('<!-- Start agent lists -->\\s*([\\s\\S]*?)\\s*<!-- End agent lists -->')
           | default([])) | first) | default('') }}

Нам нужно отрендерить новый конфиг из шаблона и вставить в него сохраненный agent_lists_block, в котором хранятся пути до CDB-списков агентов. Эти пути лежат как раз в блоке <!-- Start agent lists -->:

- name: Deploy wazuh manager config (with preserved agent lists)
  become: true
  ansible.builtin.template:
    src: wazuh_manager.conf.j2
    dest: "{{ wazuh_host_conf_path }}"
    mode: "0644"
Полная версия таски setup_config:
---
- name: Ensure host wazuh config dir exists
  become: true
  ansible.builtin.file:
    path: "{{ wazuh_host_conf_path | dirname }}"
    state: directory
    mode: "0755"

- name: Check if wazuh manager config exists
  become: true
  ansible.builtin.stat:
    path: "{{ wazuh_host_conf_path }}"
  register: _wazuh_conf_stat

- name: Read existing wazuh manager config
  when: _wazuh_conf_stat.stat.exists
  become: true
  ansible.builtin.slurp:
    path: "{{ wazuh_host_conf_path }}"
  register: _wazuh_conf_raw

- name: Preserve agent lists block from existing config
  when: _wazuh_conf_stat.stat.exists
  ansible.builtin.set_fact:
    agent_lists_block: >-
      {{
        ((_wazuh_conf_raw.content | b64decode
          | regex_findall('<!-- Start agent lists -->\s*([\s\S]*?)\s*<!-- End agent lists -->')
          | default([]))
         | first)
         | default('')
      }}

- name: Deploy wazuh manager config (with preserved agent lists)
  become: true
  ansible.builtin.template:
    src: wazuh_manager.conf.j2
    dest: "{{ wazuh_host_conf_path }}"
    mode: "0644"

После того как мы сгенерировали конфигурация для менеджера, необходимо создать правила. Наши правила будут лежать в файле rules.xml.j2, рассмотрим их:

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

<group name="syscollector,">
  <rule id="221" level="0" overwrite="yes">
    <category>ossec</category>
    <decoded_as>syscollector</decoded_as>
    <description>Syscollector event.</description>
  </rule>

В этой статье я рассмотрю правила связанные с процессами на машинах. Чтобы система работала одинаково для всех агентов, мы вводим два универсальных правила для процессов:

  <rule id="101000" level="3">
    <if_sid>221</if_sid>
    <field name="type">dbsync_processes</field>
    <field name="operation_type">^INSERTED$|^MODIFIED$|^DELETED</field>
    <list field="process.name" lookup="match_key">etc/lists/agents/common-known-apps</list>
    <description>Процесс из базового белого списка $(process.name) на $(hostname)</description>
  </rule>

  <rule id="101001" level="3">
    <if_sid>221</if_sid>
    <field name="type">dbsync_processes</field>
    <field name="operation_type">^INSERTED$|^MODIFIED$|^DELETED</field>
    <field name="process.name" negate="yes">^kworker</field>
    <list field="process.name" lookup="not_match_key">etc/lists/agents/common-known-apps</list>
    <description>Процесс не из базового белого списка $(process.name) на $(hostname)</description>
  </rule>

В первом правиле мы проверяем все процессы, и если имя процесса находится в общем whitelist (etc/lists/agents/common-known-apps), то событие логируется как обычное, без повышения уровня.

Во втором правиле wazuh ловит процессы, которых нет в whitelist. Здесь есть дополнительная проверка negate="yes", чтобы не реагировать на системные процессы ядра (kworker), которые всегда присутствуют и не представляют интереса. Именно второе правило становится базой для генерации кастомных правил под каждого агента. Логика простая: если процесс отсутствует в общем whitelist для всех серверов, это ещё не значит, что он "подозрительный". Возможно, он входит в уникальный список, характерный только для конкретного агента. Как раз эти индивидуальные исключения мы будем разбирать чуть позже.

Далее мы запишем блок <!-- Start custom processes rules -->, в котором будут лежать агентские правила:

  <!-- Start custom processes rules  -->
{{ custom_processes_rules_block | default('') }}
  <!-- End custom processes rules  -->
Полный пример rules.xml.j2
<group name="syscollector,">
  <rule id="221" level="0" overwrite="yes">
    <category>ossec</category>
    <decoded_as>syscollector</decoded_as>
    <description>Syscollector event.</description>
  </rule>

  <rule id="101000" level="3">
    <if_sid>221</if_sid>
    <field name="type">dbsync_processes</field>
    <field name="operation_type">$OPERATION_TYPE</field>
    <list field="process.name" lookup="match_key">etc/lists/agents/common-known-apps</list>
    <description>Процесс из базового белого списка $(process.name) на $(hostname)</description>
  </rule>

  <rule id="101001" level="3">
    <if_sid>221</if_sid>
    <field name="type">dbsync_processes</field>
    <field name="operation_type">$OPERATION_TYPE</field>
    <field name="process.name" negate="yes">^kworker</field>
    <list field="process.name" lookup="not_match_key">etc/lists/agents/common-known-apps</list>
    <description>Процесс не из базового белого списка $(process.name) на $(hostname)</description>
  </rule>

  <!-- Start custom processes rules  -->
{{ custom_processes_rules_block | default('') }}
  <!-- End custom processes rules  -->
</group>

Перейдем к ansible таске для генерации правил. Она в точности повторяет таску setup_config, поэтому не будем на ней останавливаться.

Полная версия таски setup_rules:
---
- name: Ensure host rules dir exists
  become: true
  ansible.builtin.file:
    path: "{{ wazuh_host_rules_path | dirname}}"
    state: directory
    mode: "0755"

- name: Check if local_rules.xml exists
  become: true
  ansible.builtin.stat:
    path: "{{ wazuh_host_rules_path }}"
  register: _rules_stat

- name: Read existing local_rules.xml
  when: _rules_stat.stat.exists
  become: true
  ansible.builtin.slurp:
    path: "{{ wazuh_host_rules_path }}"
  register: _local_rules_raw

- name: Preserve custom processes rules block from existing rules file
  when: _rules_stat.stat.exists
  ansible.builtin.set_fact:
    custom_processes_rules_block: >-
      {{
        ((_local_rules_raw.content | b64decode
          | regex_findall('<!--\s*Start custom processes rules\s*-->\s*([\s\S]*?)\s*<!--\s*End custom processes rules\s*-->')
          | default([]))
         | first)
         | default('')
      }}

- name: Deploy local rules template (with preserved custom processes)
  become: true
  ansible.builtin.template:
    src: rules.xml.j2
    dest: "{{ wazuh_host_rules_path }}"
    mode: "0644"

Как выглядит роль wazuh-agent:

roles/wazuh-agent/
├── defaults/main.yml
├── tasks/
│   ├── repo.yml
│   ├── install.yml
│   ├── rootkits_behavior.yml
│   ├── docker_setup.yml
│   ├── syscollector_setup.yml
│   ├── custom_rules_cdb_lists.yml ← выделение чанка на 1000 ID под агента
│   ├── custom_rules.yml           ← генерация CDB + правил + правка ossec.conf (на менеджере)
│   └── main.yml
├── templates/
│   └── agent_rules.xml.j2         ← шаблон правил per agent c rule_id_base
└── vars/main.yml

Таски allocate_rule_ids и custom_rules_cdb_lists выполняются через delegate_to на хосте с manager.

Начнем с таски allocate_rule_ids :

Создаём директорию для хранения реестра ID (registry). В этом файле мы будем хранить JSON-словарь с привязкой "имя_агента → базовый ID":

- name: Read ID registry if exists
  become: true
  throttle: 1
  ansible.builtin.slurp:
    path: "{{ wazuh_id_registry_path }}"
  register: _idreg_raw
  failed_when: false 

Парсим JSON из реестра, если он существует. В результате получаем словарь вида {"agent1": 200000, "agent2": 201000, ...}:

- name: Parse ID registry
  throttle: 1
  ansible.builtin.set_fact:
    _idreg: >-
      {{ (_idreg_raw.content | default('') | b64decode | trim | from_json)
         if (_idreg_raw is defined and _idreg_raw.content is defined and (_idreg_raw.content | length) > 0)
         else {} }}

Нормализуем числовые параметры (chunk, min, max):

  • chunk - размер чанка для правил (например, 1000 ID подряд);

  • minb - минимальная база (с какой цифры можно начинать);

  • maxid - максимальный допустимый ID в Wazuh.

- name: Normalize numeric params
  ansible.builtin.set_fact:
    _chunk: "{{ (wazuh_rule_chunk     | default(1000)  | int) }}"
    _minb: "{{ (wazuh_rule_min_base | default(200000) | int) }}"
    _maxid: "{{ (wazuh_rule_max      | default(999999) | int) }}"

Берём существующую базу для этого агента, если она уже есть в реестре, если агента там нет - результат будет None:

- name: Use existing base if present (or None)
  ansible.builtin.set_fact:
    rule_id_base: "{{ _idreg.get(wazuh_agent_name, None) }}"

Если базы нет или она выходит за допустимый диапазон - ищем первый свободный чанк из диапазона [minb, maxid]. Для этого

  • строим список всех возможных баз (all_bases);

  • исключаем занятые (used_bases);

  • берём первый свободный вариант.

- name: Compute first free base if missing or invalid
  when: (rule_id_base is none) or ((rule_id_base | int) < (_minb | int)) or ((rule_id_base | int) > (_maxid | int))
  vars:
    used_bases: "{{ (_idreg.values() | map('int') | list) }}"
    all_bases: "{{ range(_minb | int, (_maxid | int) - (_chunk | int) + 1, _chunk | int) | list }}"
    free_bases: "{{ (all_bases | difference(used_bases)) | sort }}"
  ansible.builtin.set_fact:
    rule_id_base: "{{ (free_bases | first | default(_minb)) | int }}"

Обновляем файл реестра, добавляя туда нового агента и его базу. Если агент уже есть - обновляем запись (combine) и сохраняем всё в JSON:

- name: Update ID registry with this agent
  throttle: 1
  ansible.builtin.copy:
    dest: "{{ wazuh_id_registry_path }}"
    mode: "0644"
    content: >-
      {{
        (_idreg | combine({ wazuh_agent_name: (rule_id_base | int) }))
        | to_nice_json
      }}
Полная версия таски allocate_rule_ids.yml:
---
- name: Ensure registry dir exists
  become: true
  throttle: 1
  ansible.builtin.file:
    path: "{{ wazuh_id_registry_path | dirname }}"
    state: directory
    mode: "0755"

- name: Read ID registry if exists
  become: true
  throttle: 1
  ansible.builtin.slurp:
    path: "{{ wazuh_id_registry_path }}"
  register: _idreg_raw
  failed_when: false

- name: Parse ID registry
  throttle: 1
  ansible.builtin.set_fact:
    _idreg: >-
      {{ (_idreg_raw.content | default('') | b64decode | trim | from_json)
         if (_idreg_raw is defined and _idreg_raw.content is defined and (_idreg_raw.content | length) > 0)
         else {} }}

- name: Ensure ID registry fact exists
  ansible.builtin.set_fact:
    _idreg: "{{ _idreg | default({}) }}"

- name: Normalize numeric params
  ansible.builtin.set_fact:
    _chunk: "{{ (wazuh_rule_chunk     | default(1000)  | int) }}"
    _minb: "{{ (wazuh_rule_min_base | default(200000) | int) }}"
    _maxid: "{{ (wazuh_rule_max      | default(999999) | int) }}"

- name: Use existing base if present (or None)
  ansible.builtin.set_fact:
    rule_id_base: "{{ _idreg.get(wazuh_agent_name, None) }}"

- name: Compute first free base if missing or invalid
  when: (rule_id_base is none) or ((rule_id_base | int) < (_minb | int)) or ((rule_id_base | int) > (_maxid | int))
  vars:
    used_bases: "{{ (_idreg.values() | map('int') | list) }}"
    all_bases: "{{ range(_minb | int, (_maxid | int) - (_chunk | int) + 1, _chunk | int) | list }}"
    free_bases: "{{ (all_bases | difference(used_bases)) | sort }}"
  ansible.builtin.set_fact:
    rule_id_base: "{{ (free_bases | first | default(_minb)) | int }}"

- name: Update ID registry with this agent
  throttle: 1
  ansible.builtin.copy:
    dest: "{{ wazuh_id_registry_path }}"
    mode: "0644"
    content: >-
      {{
        (_idreg | combine({ wazuh_agent_name: (rule_id_base | int) }))
        | to_nice_json
      }}

Теперь рассмотрим таску custom_rules_cdb_lists:

Фиксируем рабочие пути и префиксы прямо в фактах.

  • wazuh_agent_lists_prefix - базовый префикс для списков конкретного агента, чтобы в правилах ссылаться как etc/lists/agents/<agent>-known-apps;

  • wazuh_agent_cdb_lists - перечень генерируемых списков. Здесь минимум - known-apps, но можно расширять (known-ips/ports/protocols).

- name: Set wazuh host facts
  ansible.builtin.set_fact:
    wazuh_host_conf_path: "{{ wazuh_host_conf_path }}"
    wazuh_host_lists_dir: "{{ wazuh_host_lists_dir }}"
    wazuh_host_rules_path: "{{ wazuh_host_rules_path }}"
    wazuh_agent_lists_prefix: "etc/lists/agents/{{ wazuh_agent_name }}"
    wazuh_agent_cdb_lists:
      - filename: "known-apps"
  run_once: false

Создаем CDB-список для агента (например, test-agent-known-apps) на сервере менеджера.

Вход values может быть списком (['sshd','nginx']) или строкой ("sshd,nginx"); шаблон нормализует оба случая.

Каждая строка заканчивается двоеточием (value:) - это важный формат для match_key/address_match_key.

- name: Write per-agent CDB lists on host
  become: true
  ansible.builtin.copy:
    dest: "{{ wazuh_host_lists_dir }}/{{ wazuh_agent_name }}-{{ item.name }}"
    content: |
      {% set raw = item['values'] | default([]) %}
      {% set vals = (raw if (raw is iterable and raw is not string) else (raw | string).split(',')) %}
      {% for v in vals | map('trim') | reject('equalto','') | list %}
      {{ v }}:
      {% endfor %}
    mode: "0644"
  loop:
    - { name: "known-apps", values: "{{ wazuh_agent_known_apps }}" }

Рендерим небольшой XML-фрагмент <list>…</list> из шаблона agent_lists.xml.j2, который потом вставим в ossec.conf.

- name: Render agent list references snippet
  ansible.builtin.set_fact:
    wazuh_agent_lists_block: "{{ lookup('template', 'agent_lists.xml.j2') | regex_replace('\\n+$', '') }}"

Шаблон agent_lists.xml.j2 работает по такой идее: из заранее заданного каталога списков (wazuh_agent_cdb_lists) формируем ссылки для ossec.conf .

{% for item in wazuh_agent_cdb_lists %}
<list>{{ wazuh_agent_lists_prefix }}-{{ item.filename }}</list>
{% endfor %}

Вставляем (или обновляем) в ossec.conf блок <list> для конкретного агента.

- name: Declare this agent lists in manager config
  become: true
  ansible.builtin.blockinfile:
    path: "{{ wazuh_host_conf_path }}"
    marker: "<!-- {mark} WAZUH AGENT LISTS {{ wazuh_agent_name }} -->"
    insertbefore: "<!-- End agent lists -->"
    block: "{{ wazuh_agent_lists_block }}"

Рендерим XML-правила <rule>…</rule> для этого агента из agent_rules.xml.j2. Правила, как правило, обогащают общую логику (например, "если не попали в общий whitelist - смотри кастомный whitelist агента").

- name: Render agent custom process rules snippet
  ansible.builtin.set_fact:
    wazuh_agent_rules_block: "{{ lookup('template', 'agent_rules.xml.j2') | regex_replace('\\n+$', '') }}"

Шаблон agent_rules.xml.j2 представляет из себя:

  • rid(off) вычисляет ID правила как rule_id_base + offset, что позволяет гарантировать уникальные ID в чанке агента (например, по 1000 ID каждому);

  • Оба правила наследуются от общего правила с id 101001 (универсальное правило "не в базовом whitelist").

  • Правило с lookup="match_key" пропускает процесс, если он есть в персональном whitelist агента;

  • Правило с lookup="not_match_key" срабатывает, если процесса нет ни в общем, ни в персональном whitelist → поднимаем алерт.

{% macro rid(off) -%}
{{ (rule_id_base | int) + (off | int) }}
{%- endmacro %}

  <!-- Agent {{ wazuh_agent_name }} custom process rules -->
  <rule id="{{ rid(100) }}" level="3">
    <if_sid>101001</if_sid>
    <hostname>{{ wazuh_agent_name }}</hostname>
    <list field="process.name" lookup="match_key">{{ wazuh_agent_lists_prefix }}-known-apps</list>
    <description>Процесс из кастомного белого списка $(process.name) на $(hostname)</description>
  </rule>

  <rule id="{{ rid(101) }}" level="12">
    <if_sid>101001</if_sid>
    <hostname>{{ wazuh_agent_name }}</hostname>
    <list field="process.name" lookup="not_match_key">{{ wazuh_agent_lists_prefix }}-known-apps</list>
    <description>Неизвестный процесс $(process.name) на $(hostname)</description>
  </rule>

Вставляем (или обновляем) кастомные правила агента в rules.xml (или другой указанный rules-файл) на хосте.

- name: Merge agent rules into manager local_rules
  become: true
  ansible.builtin.blockinfile:
    path: "{{ wazuh_host_rules_path }}"
    marker: "<!-- {mark} WAZUH AGENT RULES {{ wazuh_agent_name }} -->"
    insertbefore: "<!-- End custom processes rules  -->"
    block: "{{ wazuh_agent_rules_block }}"
Полная версия таски custom_rules_cdb_lists:
---
- name: Set wazuh host facts
  ansible.builtin.set_fact:
    wazuh_host_conf_path: "{{ wazuh_host_conf_path }}"
    wazuh_host_lists_dir: "{{ wazuh_host_lists_dir }}"
    wazuh_host_rules_path: "{{ wazuh_host_rules_path }}"
    wazuh_agent_lists_prefix: "etc/lists/agents/{{ wazuh_agent_name }}"
    wazuh_agent_cdb_lists:
      - filename: "known-apps"
  run_once: false

- name: Write per-agent CDB lists on host
  become: true
  ansible.builtin.copy:
    dest: "{{ wazuh_host_lists_dir }}/{{ wazuh_agent_name }}-{{ item.name }}"
    content: |
      {% set raw = item['values'] | default([]) %}
      {% set vals = (raw if (raw is iterable and raw is not string) else (raw | string).split(',')) %}
      {% for v in vals | map('trim') | reject('equalto','') | list %}
      {{ v }}:
      {% endfor %}
    mode: "0644"
  loop:
    - { name: "known-apps", values: "{{ wazuh_agent_known_apps }}" }

- name: Render agent list references snippet
  ansible.builtin.set_fact:
    wazuh_agent_lists_block: "{{ lookup('template', 'agent_lists.xml.j2') | regex_replace('\\n+$', '') }}"

- name: Declare this agent lists in manager config
  become: true
  ansible.builtin.blockinfile:
    path: "{{ wazuh_host_conf_path }}"
    marker: "<!-- {mark} WAZUH AGENT LISTS {{ wazuh_agent_name }} -->"
    insertbefore: "<!-- End agent lists -->"
    block: "{{ wazuh_agent_lists_block }}"

- name: Render agent custom process rules snippet
  ansible.builtin.set_fact:
    wazuh_agent_rules_block: "{{ lookup('template', 'agent_rules.xml.j2') | regex_replace('\\n+$', '') }}"

- name: Merge agent rules into manager local_rules
  become: true
  ansible.builtin.blockinfile:
    path: "{{ wazuh_host_rules_path }}"
    marker: "<!-- {mark} WAZUH AGENT RULES {{ wazuh_agent_name }} -->"
    insertbefore: "<!-- End custom processes rules  -->"
    block: "{{ wazuh_agent_rules_block }}"

Теперь, когда все ansible роли реализованы, мы можем обновить IaC конфигурации:

wazuh-manager-ip: &wazuh-manager-ip
  wazuh_manager_ip: 1.1.1.2

servers:
  - name: server1
    ip: 1.1.1.1
    setup:
      - module: ssh
      - module: security
      - module: wazuh-agent
        vars:
          <<: *wazuh-manager-ip
          wazuh_agent_name: server1
          wazuh_agent_known_apps: ["ncdu"]

  - name: wazuh-manager
    ip: 1.1.1.2
    setup:
      - module: ssh
      - module: security
      - module: wazuh-manager

Мы прописываем, что на сервере server1 должна быть выполнена роль wazuh-agent с белым списком процессов, в котором указан ncdu, а на сервере wazuh-manager - роль wazuh-manager.

Теперь можно все это прокатить и увидеть в wazuh-manager следующее:

Правила успешно доставлены:

CDB-списки успешно доставлены:

Белый список процессов для dev netox
Белый список процессов для dev netox

Теперь заходим на сервер и запускаем ncdu и видим в лагах следующее, лог о запуске процесса ncdu с уровнем алерта 3, а это значит, что никто не будет реагировать, но мы видим историю запущенных процессов:

Так же после завершения процесса мы увидим еще один лог о, соответственно, завершении процесса:

Теперь проверим процессы не из белого списка, мы видим уровень алерта 12, а значит на него необходимо реагировать:

Теперь у нас есть полное понимание о запуске и работе всех процессов на виртуальной машине, по аналогии делаем с ip адресами, портами и протоколами и у нас есть полное понимание что происходит на машине. Я бы рекомендовал ip адреса и порты интегрировать с вашими ролями для создания ip-tables и security group в облаке, чтобы иметь единый список разрешенных адресов и держать всегда актуальное состояние для избежания ложных тревог.

Итог

В результате мы получили не просто набор Ansible-ролей, а устойчивую модель управления правилами Wazuh:

  • каждый агент имеет свои собственные CDB-списки и правила;

  • правила генерируются автоматически и не конфликтуют по ID;

  • менеджер остаётся единственной точкой истины;

  • добавление нового сервера - это изменение IaC, а не ручная правка XML;

  • вся система детерминирована и воспроизводима.

По сути, мы превратили Wazuh из "SIEM с XML-конфигами" в policy-driven систему, где:

  • инфраструктура описывает допустимое поведение;

  • Wazuh фиксирует любое отклонение от этой модели.

Почему я считаю этот подход правильным

Потому что безопасность - это не набор ручных исключений, а строго описанная модель допустимого поведения системы. Если у сервера есть роль - у него должно быть и ожидаемое поведение. Всё, что выходит за рамки - это повод для анализа. И именно так Wazuh начинает реально помогать, а не просто собирать логи.

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