Manage Wazuh Manager Configuration with Ansible (Part 3)
How I managed Wazuh manager ossec.conf using inventory-owned files, local wrapper roles, and a controlled restart flow.
Published · Republished on Medium

This article continues from Part 2: Integrating Wazuh SAML Authentication with Keycloak, where I covered Keycloak realm roles, SAML backend roles, and Wazuh RBAC mapping.
In this part, I will focus on Wazuh manager configuration.
After the base Wazuh deployment is running, the next thing I want to control is the manager-side ossec.conf.
The goal is simple: instead of editing the Wazuh manager configuration manually on the server, I want the configuration to be stored in the Ansible repository and applied consistently.
Version note: configuration sections and validation behavior can change between Wazuh releases. Review the
ossec.confreference for the same pinned version used by the manager before promoting a change.
Why Manage ossec.conf with Ansible?
The Wazuh manager is the core component that receives agent events, processes rules, manages alerts, and controls many runtime behaviors.
Its main configuration file is:
bash
/var/ossec/etc/ossec.confEditing this file manually works for quick testing, but it is not ideal for a maintainable setup.
Manual changes are hard to track, allow drift between hosts, make rollback ambiguous, and turn rebuilding the server into a memory exercise.
For me, non-secret configuration should live in the repository. An ossec.conf can contain API keys, webhook URLs, enrollment settings, or other sensitive integration values; render those from Ansible Vault or an external secret manager, prevent them from appearing in diffs and logs, and restrict the resulting file on the target.
Ansible gives a better flow:
text
repository config -> copy/template to target -> validate -> restart managerThis makes manager configuration repeatable and easier to review.
Repository Layout
The manager configuration is stored under the inventory-owned files directory.

This follows the same pattern from Part 1.
Environment-specific files should live inside the inventory, not be hidden inside a generic role.
The role should handle how to deploy the file, but the inventory owns what the file contains.
The separation looks like this:

This makes ownership clear.
If I want to update the Wazuh manager configuration, I update the inventory file first, then let Ansible apply it.
Example Structure
The relevant structure looks like this:
bash
inventories/lab/files/wazuh
└── config
└── manager
└── ossec.confIf the repository later supports more environments, each environment can have its own manager config.
Example:
bash
inventories
├── lab
│ └── files/wazuh/config/manager/ossec.conf
├── staging
│ └── files/wazuh/config/manager/ossec.conf
└── production
└── files/wazuh/config/manager/ossec.confThat gives flexibility without changing the deployment role.
The role only needs to know which inventory is being used.
Ansible Role Flow
I use a local wrapper role to manage the manager config.
Example role name:
bash
roles/wazuh_manager_configThe role flow is simple:
text
1. Check source config exists on controller
2. Render a candidate without logging secret values
3. Check XML structure and required policy constraints
4. Back up the current runtime file with metadata
5. Install the candidate with root:wazuh ownership
6. Run the Wazuh daemon-specific configuration tests
7. Restart wazuh-manager only when config changes
8. Verify processes, API, agent connectivity, and alert flow
9. Restore the previous file automatically if acceptance failsThe playbook can stay thin:


The playbook only describes the intent.
The role handles the actual implementation.
Copying the Manager Config
The role copies the repository-owned file into the runtime path.
Conceptually:

This keeps the source path tied to the selected inventory.
If I run the playbook with inventories/lab/host.ini, the role uses the lab-owned config.
If I later run it with staging or production inventory, the same role can use a different source file.
Restart Handler
The manager should only restart when the config changes. A handler is useful, but delaying it until the end of a long play can leave validation and the service transition far apart. Flush the handler at an intentional maintenance boundary, then run acceptance checks immediately.
That is why I use a handler:

This avoids unnecessary restarts when there is no config change.
For Wazuh, this matters because manager restart can temporarily affect event processing and agent communication. In a manager cluster, deploy serially and verify cluster synchronization before moving to the next node; do not restart every manager at once.
Validation Before Restart
Before restarting the manager, validate as much as the installed version supports. XML parsing catches malformed structure, but valid XML can still contain invalid Wazuh options. Wazuh documents checks for individual consumers of the runtime configuration:
bash
/var/ossec/bin/wazuh-analysisd -t
/var/ossec/bin/wazuh-logcollector -t
/var/ossec/bin/wazuh-modulesd -t
/var/ossec/bin/wazuh-syscheckd -tRun the checks relevant to the sections you changed and fail on any non-zero exit. These tools inspect the installed runtime configuration, so the safe flow is: verify the candidate XML, create a protected backup, atomically install the candidate, run the daemon tests, and restore the backup before any restart if a test fails. An Ansible block with rescue and always makes that recovery path explicit.
There is no single test in the current reference that proves every ossec.conf section is operational. A successful syntax check is a gate, not the final acceptance test.
Example Deployment Command
The playbook can be executed like this:
bash
ansible-playbook -i inventories/lab/hosts.ini playbooks/wazuh-manager-config.yml --diffUse --diff only when the rendered file cannot disclose secrets. After the playbook runs, verify the systemd unit and the individual Wazuh processes:
bash
systemctl is-active wazuh-manager
/var/ossec/bin/wazuh-control statusOr check logs:
bash
journalctl -u wazuh-manager -n 100 --no-pagerThe manager merely starting is not enough. Confirm that expected agents reconnect, the Wazuh server API responds, a controlled test event reaches analysis, a resulting alert reaches the indexer, queues and error rates remain normal, and a second Ansible run reports no unintended change.
What Should Be Managed Here?
This article focuses only on manager-side configuration.
Examples include global manager settings, agent communication, log collection, integrations, vulnerability detection, and active response. Each change carries different risk: an invalid <remote> section can affect agent traffic, verbose event archiving can consume substantial disk, and active response can execute actions across endpoints. Review behavior and capacity, not just XML validity.
But I intentionally keep some things separate.
Custom suppression rules should not be mixed into the manager ossec.conf article.
They deserve their own flow and their own file ownership model.
For example:
text
Manager config:
inventories/lab/files/wazuh/config/manager/ossec.confKeeping those separate makes the repository easier to understand.
Managing Wazuh manager configuration with Ansible makes the setup easier to maintain.
Instead of editing /var/ossec/etc/ossec.conf manually, the source of truth lives in the repository:
bash
inventories/lab/files/wazuh/config/manager/ossec.confThe Ansible role is responsible for copying, validating, and restarting the manager safely.
The key idea is simple:
config
inventory owns the config
role owns the deployment logic
playbook owns the execution flowThis keeps the Wazuh manager configuration predictable and easier to review.
In the next part, I will cover Wazuh agent configuration profiles, including how to manage different ossec.conf files for different host roles using Ansible.