Привет, я 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 начинает реально помогать, а не просто собирать логи.
