Привет, я 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-списки успешно доставлены:

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

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

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

Теперь у нас есть полное понимание о запуске и работе всех процессов на виртуальной машине, по аналогии делаем с ip адресами, портами и протоколами и у нас есть полное понимание что происходит на машине. Я бы рекомендовал ip адреса и порты интегрировать с вашими ролями для создания ip-tables и security group в облаке, чтобы иметь единый список разрешенных адресов и держать всегда актуальное состояние для избежания ложных тревог.
Итог
В результате мы получили не просто набор Ansible-ролей, а устойчивую модель управления правилами Wazuh:
каждый агент имеет свои собственные CDB-списки и правила;
правила генерируются автоматически и не конфликтуют по ID;
менеджер остаётся единственной точкой истины;
добавление нового сервера - это изменение IaC, а не ручная правка XML;
вся система детерминирована и воспроизводима.
По сути, мы превратили Wazuh из "SIEM с XML-конфигами" в policy-driven систему, где:
инфраструктура описывает допустимое поведение;
Wazuh фиксирует любое отклонение от этой модели.
Почему я считаю этот подход правильным
Потому что безопасность - это не набор ручных исключений, а строго описанная модель допустимого поведения системы. Если у сервера есть роль - у него должно быть и ожидаемое поведение. Всё, что выходит за рамки - это повод для анализа. И именно так Wazuh начинает реально помогать, а не просто собирать логи.