Skip to content

Wazuh Ansible Deployment - Complete Automation Guide

Published: at 11:00 AM

Wazuh Ansible Deployment

Deploying Wazuh at scale requires robust automation to ensure consistency, repeatability, and efficient management. This guide provides a complete Ansible-based deployment solution for Wazuh, covering everything from initial cluster setup to ongoing maintenance and monitoring.

Wazuh Architecture for Ansible Deployment

Understanding the Wazuh architecture is crucial for planning your Ansible deployment strategy:

graph TB
    subgraph "Wazuh Cluster"
        subgraph "Master Node"
            WM[Wazuh Manager Master]
            WA[Wazuh API]
            FR[Filebeat/Forwarder]
        end

        subgraph "Worker Nodes"
            WW1[Wazuh Worker 1]
            WW2[Wazuh Worker 2]
            WW3[Wazuh Worker N]
        end

        subgraph "Indexer Cluster"
            OS1[OpenSearch Node 1]
            OS2[OpenSearch Node 2]
            OS3[OpenSearch Node 3]
        end

        subgraph "Dashboard"
            WD[Wazuh Dashboard]
            OSD[OpenSearch Dashboards]
        end
    end

    subgraph "Agents"
        subgraph "Linux Agents"
            LA1[CentOS Agent]
            LA2[Ubuntu Agent]
            LA3[RHEL Agent]
        end

        subgraph "Windows Agents"
            WA1[Windows Server]
            WA2[Windows Desktop]
        end

        subgraph "Other Agents"
            MA[macOS Agent]
            DA[Docker Agent]
        end
    end

    subgraph "External Integrations"
        LDAP[LDAP/AD]
        SMTP[Email Gateway]
        SLACK[Slack]
        SPLUNK[Splunk]
    end

    WM --> WW1
    WM --> WW2
    WM --> WW3

    FR --> OS1
    WW1 --> OS1
    WW2 --> OS2
    WW3 --> OS3

    OS1 <--> OS2
    OS2 <--> OS3
    OS1 <--> OS3

    WD --> WM
    OSD --> OS1

    LA1 --> WM
    LA2 --> WW1
    LA3 --> WW2
    WA1 --> WW3
    WA2 --> WM
    MA --> WW1
    DA --> WW2

    WM --> LDAP
    WM --> SMTP
    WM --> SLACK
    WM --> SPLUNK

    style WM fill:#f96,stroke:#333,stroke-width:2px
    style OS1 fill:#9f9,stroke:#333,stroke-width:2px

Ansible Project Structure

Directory Layout

wazuh-ansible/
├── ansible.cfg
├── inventory/
   ├── production/
   ├── hosts.yml
   ├── group_vars/
   ├── all.yml
   ├── wazuh_managers.yml
   ├── wazuh_indexers.yml
   └── wazuh_agents.yml
   └── host_vars/
       ├── wazuh-master.yml
       └── wazuh-worker-01.yml
   └── staging/
       └── hosts.yml
├── playbooks/
   ├── site.yml
   ├── deploy-cluster.yml
   ├── deploy-agents.yml
   ├── upgrade-wazuh.yml
   └── backup-restore.yml
├── roles/
   ├── common/
   ├── wazuh-manager/
   ├── wazuh-indexer/
   ├── wazuh-dashboard/
   ├── wazuh-agent/
   └── wazuh-integrations/
├── templates/
   ├── ossec.conf.j2
   ├── cluster.conf.j2
   └── rules/
├── files/
   ├── certificates/
   └── custom-rules/
└── requirements.yml

Core Ansible Configuration

ansible.cfg

[defaults]
inventory = inventory/production/hosts.yml
remote_user = ansible
host_key_checking = False
retry_files_enabled = True
retry_files_save_path = .ansible-retry
stdout_callback = yaml
callback_whitelist = timer, profile_tasks
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_fact_cache
fact_caching_timeout = 86400
roles_path = roles
force_color = True
nocows = 1

[inventory]
enable_plugins = yaml, ini, script

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no
pipelining = True
control_path = /tmp/ansible-%%h-%%p-%%r

Inventory Configuration

# inventory/production/hosts.yml
all:
  children:
    wazuh_cluster:
      children:
        wazuh_managers:
          hosts:
            wazuh-master:
              ansible_host: 10.0.1.10
              wazuh_node_type: master
              wazuh_cluster_key: "my_secure_cluster_key"
            wazuh-worker-01:
              ansible_host: 10.0.1.11
              wazuh_node_type: worker
            wazuh-worker-02:
              ansible_host: 10.0.1.12
              wazuh_node_type: worker

        wazuh_indexers:
          hosts:
            indexer-01:
              ansible_host: 10.0.2.10
              indexer_node_name: node-1
              indexer_cluster_initial_master_nodes:
                - node-1
                - node-2
                - node-3
            indexer-02:
              ansible_host: 10.0.2.11
              indexer_node_name: node-2
            indexer-03:
              ansible_host: 10.0.2.12
              indexer_node_name: node-3

        wazuh_dashboard:
          hosts:
            dashboard-01:
              ansible_host: 10.0.3.10

    wazuh_agents:
      children:
        linux_agents:
          hosts:
            web-server-01:
              ansible_host: 10.0.4.10
              wazuh_agent_name: web-server-01
              wazuh_agent_groups:
                - webservers
                - linux
            db-server-01:
              ansible_host: 10.0.4.20
              wazuh_agent_name: db-server-01
              wazuh_agent_groups:
                - databases
                - linux

        windows_agents:
          hosts:
            win-server-01:
              ansible_host: 10.0.5.10
              ansible_connection: winrm
              ansible_winrm_transport: ntlm
              ansible_winrm_server_cert_validation: ignore
              wazuh_agent_name: win-server-01
              wazuh_agent_groups:
                - windows
                - domain_controllers

Wazuh Manager Role

Role Structure

roles/wazuh-manager/
├── defaults/
   └── main.yml
├── tasks/
   ├── main.yml
   ├── install.yml
   ├── configure.yml
   ├── cluster.yml
   └── ssl.yml
├── templates/
   ├── ossec.conf.j2
   ├── cluster.conf.j2
   └── api.yaml.j2
├── handlers/
   └── main.yml
└── vars/
    └── main.yml

Manager Installation Tasks

# roles/wazuh-manager/tasks/install.yml
---
- name: Add Wazuh repository GPG key
  rpm_key:
    key: https://packages.wazuh.com/key/GPG-KEY-WAZUH
    state: present
  when: ansible_os_family == "RedHat"

- name: Add Wazuh repository
  yum_repository:
    name: wazuh
    description: Wazuh repository
    baseurl: https://packages.wazuh.com/4.x/yum/
    gpgcheck: yes
    gpgkey: https://packages.wazuh.com/key/GPG-KEY-WAZUH
    enabled: yes
  when: ansible_os_family == "RedHat"

- name: Install Wazuh manager package
  package:
    name: wazuh-manager
    state: present
  notify: restart wazuh-manager

- name: Enable and start Wazuh manager service
  systemd:
    name: wazuh-manager
    enabled: yes
    state: started
    daemon_reload: yes

Manager Configuration Template

<!-- templates/ossec.conf.j2 -->
<ossec_config>
  <global>
    <jsonout_output>yes</jsonout_output>
    <alerts_log>yes</alerts_log>
    <logall>no</logall>
    <logall_json>no</logall_json>
    <email_notification>{{ wazuh_email_notification | default('no') }}</email_notification>
    <smtp_server>{{ wazuh_smtp_server | default('localhost') }}</smtp_server>
    <email_from>{{ wazuh_email_from | default('wazuh@company.com') }}</email_from>
    <email_to>{{ wazuh_email_to | default('security@company.com') }}</email_to>
    <email_maxperhour>{{ wazuh_email_maxperhour | default(12) }}</email_maxperhour>
    <email_log_source>alerts.log</email_log_source>
    <agents_disconnection_time>{{ wazuh_agents_disconnection_time | default('10m') }}</agents_disconnection_time>
    <agents_disconnection_alert_time>{{ wazuh_agents_disconnection_alert_time | default('0') }}</agents_disconnection_alert_time>
  </global>

  <alerts>
    <log_alert_level>{{ wazuh_log_alert_level | default(3) }}</log_alert_level>
    <email_alert_level>{{ wazuh_email_alert_level | default(12) }}</email_alert_level>
  </alerts>

  <!-- Cluster configuration -->
  {% if wazuh_cluster_enabled | default(false) %}
  <cluster>
    <name>{{ wazuh_cluster_name }}</name>
    <node_name>{{ inventory_hostname }}</node_name>
    <node_type>{{ wazuh_node_type }}</node_type>
    <key>{{ wazuh_cluster_key }}</key>
    <port>{{ wazuh_cluster_port | default(1516) }}</port>
    <bind_addr>{{ wazuh_cluster_bind_addr | default('0.0.0.0') }}</bind_addr>
    <nodes>
      <node>{{ wazuh_master_node }}</node>
    </nodes>
    <hidden>{{ wazuh_cluster_hidden | default('no') }}</hidden>
    <disabled>no</disabled>
  </cluster>
  {% endif %}

  <!-- Remote connections -->
  <remote>
    <connection>secure</connection>
    <port>{{ wazuh_authd_port | default(1514) }}</port>
    <protocol>tcp</protocol>
    <queue_size>{{ wazuh_queue_size | default(131072) }}</queue_size>
  </remote>

  <!-- Policy monitoring -->
  <rootcheck>
    <disabled>no</disabled>
    <check_files>yes</check_files>
    <check_trojans>yes</check_trojans>
    <check_dev>yes</check_dev>
    <check_sys>yes</check_sys>
    <check_pids>yes</check_pids>
    <check_ports>yes</check_ports>
    <check_if>yes</check_if>
    <frequency>{{ wazuh_rootcheck_frequency | default(43200) }}</frequency>
    <rootkit_files>etc/rootcheck/rootkit_files.txt</rootkit_files>
    <rootkit_trojans>etc/rootcheck/rootkit_trojans.txt</rootkit_trojans>
  </rootcheck>

  <!-- File integrity monitoring -->
  <syscheck>
    <disabled>no</disabled>
    <frequency>{{ wazuh_syscheck_frequency | default(43200) }}</frequency>
    <scan_on_start>yes</scan_on_start>
    <alert_new_files>yes</alert_new_files>
    <auto_ignore>no</auto_ignore>

    <!-- Directories to check -->
    {% for directory in wazuh_syscheck_directories %}
    <directories>{{ directory }}</directories>
    {% endfor %}

    <!-- Files/directories to ignore -->
    {% for ignore in wazuh_syscheck_ignore %}
    <ignore>{{ ignore }}</ignore>
    {% endfor %}

    <!-- File types to ignore -->
    <ignore type="sregex">.log$|.swp$</ignore>

    <!-- Check files configuration -->
    <whodata>
      <restart_audit>yes</restart_audit>
      <audit_key>{{ wazuh_audit_key | default('wazuh_fim') }}</audit_key>
      <startup_healthcheck>yes</startup_healthcheck>
    </whodata>
  </syscheck>

  <!-- Log analysis -->
  <localfile>
    <log_format>syslog</log_format>
    <location>/var/log/messages</location>
  </localfile>

  <localfile>
    <log_format>syslog</log_format>
    <location>/var/log/secure</location>
  </localfile>

  {% for custom_log in wazuh_custom_logs | default([]) %}
  <localfile>
    <log_format>{{ custom_log.format }}</log_format>
    <location>{{ custom_log.location }}</location>
    {% if custom_log.label is defined %}
    <label key="{{ custom_log.label }}">{{ custom_log.value }}</label>
    {% endif %}
  </localfile>
  {% endfor %}

  <!-- Active response -->
  <active-response>
    <disabled>no</disabled>
    <ca_store>etc/wpk_root.pem</ca_store>
    <ca_verification>yes</ca_verification>
  </active-response>

  <!-- Vulnerability detector -->
  <vulnerability-detector>
    <enabled>yes</enabled>
    <interval>12h</interval>
    <ignore_time>6h</ignore_time>
    <run_on_start>yes</run_on_start>

    <!-- Operating systems to scan -->
    {% for os in wazuh_vuln_detector_os | default(['ubuntu', 'redhat', 'centos']) %}
    <provider name="{{ os }}">
      <enabled>yes</enabled>
      <update_interval>1h</update_interval>
    </provider>
    {% endfor %}
  </vulnerability-detector>

  <!-- Integration with external systems -->
  {% if wazuh_integrations is defined %}
  {% for integration in wazuh_integrations %}
  <integration>
    <name>{{ integration.name }}</name>
    <hook_url>{{ integration.hook_url }}</hook_url>
    <level>{{ integration.level | default(10) }}</level>
    <alert_format>{{ integration.format | default('json') }}</alert_format>
    {% if integration.options is defined %}
    <options>{{ integration.options | to_json }}</options>
    {% endif %}
  </integration>
  {% endfor %}
  {% endif %}
</ossec_config>

Cluster Configuration

# roles/wazuh-manager/tasks/cluster.yml
---
- name: Configure Wazuh cluster
  template:
    src: cluster.conf.j2
    dest: /var/ossec/etc/cluster.conf
    owner: root
    group: wazuh
    mode: "0640"
  notify: restart wazuh-manager
  when: wazuh_cluster_enabled | default(false)

- name: Generate cluster key if master node
  shell: |
    openssl rand -hex 16 > /var/ossec/etc/cluster.key
  args:
    creates: /var/ossec/etc/cluster.key
  when:
    - wazuh_node_type == "master"
    - wazuh_cluster_key is not defined

- name: Read cluster key from master
  slurp:
    src: /var/ossec/etc/cluster.key
  register: cluster_key_content
  when: wazuh_node_type == "master"

- name: Set cluster key fact
  set_fact:
    wazuh_cluster_key: "{{ cluster_key_content.content | b64decode | trim }}"
  when: wazuh_node_type == "master"

- name: Distribute cluster key to workers
  copy:
    content: "{{ hostvars[wazuh_master_node]['wazuh_cluster_key'] }}"
    dest: /var/ossec/etc/cluster.key
    owner: root
    group: wazuh
    mode: "0640"
  when: wazuh_node_type == "worker"
  notify: restart wazuh-manager

Wazuh Indexer Role

Indexer Configuration

# roles/wazuh-indexer/templates/opensearch.yml.j2
cluster.name: {{ wazuh_cluster_name | default('wazuh-cluster') }}
node.name: {{ indexer_node_name }}
network.host: {{ ansible_default_ipv4.address }}
http.port: {{ indexer_http_port | default(9200) }}
transport.port: {{ indexer_transport_port | default(9300) }}

# Discovery
discovery.seed_hosts:
{% for host in groups['wazuh_indexers'] %}
  - {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ indexer_transport_port | default(9300) }}
{% endfor %}

cluster.initial_master_nodes:
{% for node in indexer_cluster_initial_master_nodes %}
  - {{ node }}
{% endfor %}

# Security settings
plugins.security.ssl.transport.pemcert_filepath: {{ indexer_cert_path }}/{{ indexer_node_name }}.pem
plugins.security.ssl.transport.pemkey_filepath: {{ indexer_cert_path }}/{{ indexer_node_name }}-key.pem
plugins.security.ssl.transport.pemtrustedcas_filepath: {{ indexer_cert_path }}/root-ca.pem
plugins.security.ssl.transport.enforce_hostname_verification: true

plugins.security.ssl.http.enabled: true
plugins.security.ssl.http.pemcert_filepath: {{ indexer_cert_path }}/{{ indexer_node_name }}.pem
plugins.security.ssl.http.pemkey_filepath: {{ indexer_cert_path }}/{{ indexer_node_name }}-key.pem
plugins.security.ssl.http.pemtrustedcas_filepath: {{ indexer_cert_path }}/root-ca.pem

plugins.security.allow_unsafe_democertificates: false
plugins.security.allow_default_init_securityindex: true
plugins.security.authcz.admin_dn:
  - CN=admin,O=Wazuh,L=California,C=US

plugins.security.audit.type: internal_opensearch
plugins.security.enable_snapshot_restore_privilege: true
plugins.security.check_snapshot_restore_write_privileges: true
plugins.security.restapi.roles_enabled: ["all_access", "security_rest_api_access"]

# Performance settings
indices.memory.index_buffer_size: {{ indexer_memory_buffer_size | default('10%') }}
thread_pool.search.size: {{ indexer_search_threads | default(ansible_processor_vcpus * 2) }}
thread_pool.search.queue_size: {{ indexer_search_queue_size | default(1000) }}
thread_pool.write.size: {{ indexer_write_threads | default(ansible_processor_vcpus) }}
thread_pool.write.queue_size: {{ indexer_write_queue_size | default(200) }}

# Path settings
path.data: {{ indexer_data_path | default('/var/lib/wazuh-indexer') }}
path.logs: {{ indexer_log_path | default('/var/log/wazuh-indexer') }}

Wazuh Agent Role

Multi-Platform Agent Deployment

# roles/wazuh-agent/tasks/main.yml
---
- name: Include OS-specific variables
  include_vars: "{{ ansible_os_family }}.yml"

- name: Deploy Wazuh agent on Linux
  include_tasks: install-linux.yml
  when: ansible_system == "Linux"

- name: Deploy Wazuh agent on Windows
  include_tasks: install-windows.yml
  when: ansible_system == "Win32NT"

- name: Deploy Wazuh agent on macOS
  include_tasks: install-macos.yml
  when: ansible_system == "Darwin"

- name: Configure Wazuh agent
  include_tasks: configure.yml

- name: Register agent with manager
  include_tasks: register.yml

Linux Agent Installation

# roles/wazuh-agent/tasks/install-linux.yml
---
- name: Add Wazuh repository (Debian/Ubuntu)
  apt:
    deb: https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_{{ wazuh_agent_version }}_amd64.deb
  when: ansible_os_family == "Debian"

- name: Add Wazuh repository (RedHat/CentOS)
  yum:
    name: https://packages.wazuh.com/4.x/yum/wazuh-agent-{{ wazuh_agent_version }}.x86_64.rpm
    state: present
  when: ansible_os_family == "RedHat"

- name: Create Wazuh agent configuration
  template:
    src: ossec-agent.conf.j2
    dest: /var/ossec/etc/ossec.conf
    owner: root
    group: ossec
    mode: "0640"
  notify: restart wazuh-agent

- name: Import Wazuh manager SSL certificate
  copy:
    src: "{{ wazuh_ssl_cert_file }}"
    dest: /var/ossec/etc/sslmanager.cert
    owner: root
    group: ossec
    mode: "0640"

Windows Agent Installation

# roles/wazuh-agent/tasks/install-windows.yml
---
- name: Download Wazuh agent installer
  win_get_url:
    url: "https://packages.wazuh.com/4.x/windows/wazuh-agent-{{ wazuh_agent_version }}-1.msi"
    dest: "C:\\temp\\wazuh-agent.msi"

- name: Install Wazuh agent
  win_package:
    path: "C:\\temp\\wazuh-agent.msi"
    arguments: /q WAZUH_MANAGER="{{ wazuh_manager_ip }}" WAZUH_REGISTRATION_SERVER="{{ wazuh_manager_ip }}" WAZUH_AGENT_NAME="{{ wazuh_agent_name }}" WAZUH_AGENT_GROUP="{{ wazuh_agent_groups | join(',') }}"
    state: present

- name: Configure Windows agent
  win_template:
    src: ossec-agent-windows.conf.j2
    dest: "C:\\Program Files (x86)\\ossec-agent\\ossec.conf"
  notify: restart wazuh-agent-windows

- name: Start Wazuh agent service
  win_service:
    name: WazuhSvc
    state: started
    start_mode: auto

Agent Registration

# roles/wazuh-agent/tasks/register.yml
---
- name: Check if agent is already registered
  stat:
    path: /var/ossec/etc/client.keys
  register: client_keys_file

- name: Register agent with manager
  shell: |
    /var/ossec/bin/agent-auth -m {{ wazuh_manager_ip }} \
      -A {{ wazuh_agent_name }} \
      -G {{ wazuh_agent_groups | join(',') }} \
      {% if wazuh_agent_password is defined %}-P {{ wazuh_agent_password }}{% endif %}
  when: not client_keys_file.stat.exists
  notify: restart wazuh-agent

- name: Wait for agent to connect
  wait_for:
    path: /var/ossec/logs/ossec.log
    search_regex: "Connected to the server"
    timeout: 60
  when: not client_keys_file.stat.exists

Custom Rules and Decoders

Deploy Custom Rules

<!-- files/custom-rules/web-attacks.xml -->
<group name="web,attack">
  <!-- SQL Injection attempts -->
  <rule id="100001" level="10">
    <if_sid>31100</if_sid>
    <match>(?i)(union.*select|select.*from|insert.*into|delete.*from|drop.*table|update.*set)</match>
    <description>SQL injection attempt detected</description>
    <group>web,sql_injection,attack</group>
  </rule>

  <!-- XSS attempts -->
  <rule id="100002" level="8">
    <if_sid>31100</if_sid>
    <regex><script|javascript:|onerror=|onload=</regex>
    <description>Possible XSS attack detected</description>
    <group>web,xss,attack</group>
  </rule>

  <!-- Directory traversal -->
  <rule id="100003" level="7">
    <if_sid>31100</if_sid>
    <match>../</match>
    <description>Directory traversal attempt</description>
    <group>web,directory_traversal,attack</group>
  </rule>

  <!-- Rate limiting rule -->
  <rule id="100004" level="10" frequency="50" timeframe="60">
    <if_matched_sid>31100</if_matched_sid>
    <same_source_ip />
    <description>Multiple web requests from same IP (possible DoS)</description>
    <group>web,dos,attack</group>
  </rule>
</group>

Custom Decoder

<!-- files/custom-decoders/application.xml -->
<decoder name="custom-app">
  <program_name>^custom-app</program_name>
</decoder>

<decoder name="custom-app-json">
  <parent>custom-app</parent>
  <type>json</type>
  <regex>^(\S+) (\S+) ({.*})$</regex>
  <order>timestamp,level,json_message</order>
</decoder>

<decoder name="custom-app-fields">
  <parent>custom-app-json</parent>
  <regex>"user":"(\S+)".*"action":"(\S+)".*"status":"(\S+)"</regex>
  <order>user,action,status</order>
</decoder>

Complete Deployment Playbook

# playbooks/deploy-cluster.yml
---
- name: Deploy Wazuh cluster
  hosts: all
  gather_facts: yes

  pre_tasks:
    - name: Verify connectivity to all hosts
      ping:

    - name: Gather facts from all hosts
      setup:
      tags: always

- name: Install and configure indexer cluster
  hosts: wazuh_indexers
  serial: 1
  roles:
    - role: common
      tags: common
    - role: wazuh-indexer
      tags: indexer

  post_tasks:
    - name: Wait for indexer to be ready
      uri:
        url: "https://{{ ansible_default_ipv4.address }}:9200/_cluster/health"
        user: admin
        password: "{{ indexer_admin_password }}"
        validate_certs: no
      register: cluster_health
      until: cluster_health.json.status == "green" or cluster_health.json.status == "yellow"
      retries: 30
      delay: 10

- name: Install and configure Wazuh managers
  hosts: wazuh_managers
  serial: "{{ wazuh_rolling_update | default(1) }}"
  roles:
    - role: common
      tags: common
    - role: wazuh-manager
      tags: manager

  post_tasks:
    - name: Verify cluster status
      command: /var/ossec/bin/cluster_control -l
      register: cluster_status
      changed_when: false

    - name: Display cluster status
      debug:
        var: cluster_status.stdout_lines

- name: Install and configure dashboard
  hosts: wazuh_dashboard
  roles:
    - role: common
      tags: common
    - role: wazuh-dashboard
      tags: dashboard

- name: Deploy Wazuh agents
  hosts: wazuh_agents
  strategy: free
  roles:
    - role: wazuh-agent
      tags: agent

  post_tasks:
    - name: Verify agent connection
      delegate_to: "{{ wazuh_manager_ip }}"
      shell: |
        /var/ossec/bin/agent_control -l | grep "{{ wazuh_agent_name }}"
      register: agent_status
      changed_when: false
      failed_when: agent_status.rc != 0

Monitoring and Maintenance Playbooks

Health Check Playbook

# playbooks/health-check.yml
---
- name: Wazuh cluster health check
  hosts: wazuh_managers
  gather_facts: no

  tasks:
    - name: Check Wazuh manager service status
      systemd:
        name: wazuh-manager
      register: manager_service

    - name: Check cluster synchronization
      command: /var/ossec/bin/cluster_control -i
      register: cluster_info
      changed_when: false

    - name: Count connected agents
      shell: |
        /var/ossec/bin/agent_control -l | grep -c "Active"
      register: active_agents
      changed_when: false

    - name: Check disk usage
      shell: |
        df -h /var/ossec | tail -1 | awk '{print $5}' | sed 's/%//'
      register: disk_usage
      changed_when: false

    - name: Generate health report
      set_fact:
        health_report:
          hostname: "{{ inventory_hostname }}"
          service_status: "{{ manager_service.status.ActiveState }}"
          cluster_status: "{{ cluster_info.stdout | from_json }}"
          active_agents: "{{ active_agents.stdout }}"
          disk_usage: "{{ disk_usage.stdout }}%"

    - name: Alert if issues found
      mail:
        to: "{{ wazuh_admin_email }}"
        subject: "Wazuh Health Alert - {{ inventory_hostname }}"
        body: |
          Health check found issues:

          Service Status: {{ health_report.service_status }}
          Active Agents: {{ health_report.active_agents }}
          Disk Usage: {{ health_report.disk_usage }}
      when:
        - health_report.service_status != "active" or
          health_report.disk_usage | int > 80

- name: Check indexer cluster health
  hosts: wazuh_indexers
  gather_facts: no

  tasks:
    - name: Check cluster health
      uri:
        url: "https://{{ ansible_default_ipv4.address }}:9200/_cluster/health"
        user: admin
        password: "{{ indexer_admin_password }}"
        validate_certs: no
      register: cluster_health

    - name: Check indices health
      uri:
        url: "https://{{ ansible_default_ipv4.address }}:9200/_cat/indices/wazuh-*?format=json"
        user: admin
        password: "{{ indexer_admin_password }}"
        validate_certs: no
      register: indices_health

    - name: Alert on cluster issues
      mail:
        to: "{{ wazuh_admin_email }}"
        subject: "Wazuh Indexer Alert - Cluster Status {{ cluster_health.json.status }}"
        body: |
          Indexer cluster status: {{ cluster_health.json.status }}
          Number of nodes: {{ cluster_health.json.number_of_nodes }}
          Active shards: {{ cluster_health.json.active_shards }}
          Unassigned shards: {{ cluster_health.json.unassigned_shards }}
      when: cluster_health.json.status == "red"

Backup Playbook

# playbooks/backup-wazuh.yml
---
- name: Backup Wazuh configuration and data
  hosts: wazuh_managers

  vars:
    backup_dir: "/backup/wazuh/{{ ansible_date_time.date }}"

  tasks:
    - name: Create backup directory
      file:
        path: "{{ backup_dir }}"
        state: directory
        mode: "0700"

    - name: Stop Wazuh manager for consistent backup
      systemd:
        name: wazuh-manager
        state: stopped
      when: wazuh_online_backup is not defined or not wazuh_online_backup

    - name: Backup Wazuh configuration
      archive:
        path:
          - /var/ossec/etc
          - /var/ossec/rules
          - /var/ossec/decoders
        dest: "{{ backup_dir }}/wazuh-config-{{ inventory_hostname }}.tar.gz"
        format: gz

    - name: Backup Wazuh database
      shell: |
        tar czf {{ backup_dir }}/wazuh-db-{{ inventory_hostname }}.tar.gz /var/ossec/queue/db/
      args:
        creates: "{{ backup_dir }}/wazuh-db-{{ inventory_hostname }}.tar.gz"

    - name: Backup agent keys
      copy:
        src: /var/ossec/etc/client.keys
        dest: "{{ backup_dir }}/client.keys-{{ inventory_hostname }}"
        remote_src: yes

    - name: Start Wazuh manager
      systemd:
        name: wazuh-manager
        state: started
      when: wazuh_online_backup is not defined or not wazuh_online_backup

    - name: Create backup manifest
      copy:
        content: |
          Backup Date: {{ ansible_date_time.date }}
          Backup Time: {{ ansible_date_time.time }}
          Hostname: {{ inventory_hostname }}
          Wazuh Version: {{ wazuh_version }}
          Total Agents: {{ groups['wazuh_agents'] | length }}

          Files:
          - wazuh-config-{{ inventory_hostname }}.tar.gz
          - wazuh-db-{{ inventory_hostname }}.tar.gz
          - client.keys-{{ inventory_hostname }}
        dest: "{{ backup_dir }}/manifest.txt"

    - name: Sync backup to remote storage
      synchronize:
        src: "{{ backup_dir }}"
        dest: "{{ backup_remote_path }}"
        mode: push
      when: backup_remote_path is defined

- name: Backup indexer data
  hosts: wazuh_indexers

  tasks:
    - name: Create indexer snapshot repository
      uri:
        url: "https://{{ ansible_default_ipv4.address }}:9200/_snapshot/backup"
        method: PUT
        user: admin
        password: "{{ indexer_admin_password }}"
        body_format: json
        body:
          type: fs
          settings:
            location: "/backup/indexer/{{ ansible_date_time.date }}"
        validate_certs: no

    - name: Create snapshot
      uri:
        url: "https://{{ ansible_default_ipv4.address }}:9200/_snapshot/backup/{{ ansible_date_time.epoch }}"
        method: PUT
        user: admin
        password: "{{ indexer_admin_password }}"
        body_format: json
        body:
          indices: "wazuh-*"
          include_global_state: false
        validate_certs: no

Integration Playbooks

Slack Integration

# playbooks/configure-slack-integration.yml
---
- name: Configure Slack integration for Wazuh alerts
  hosts: wazuh_managers

  vars:
    slack_webhook_url: "{{ vault_slack_webhook_url }}"

  tasks:
    - name: Install jq for JSON processing
      package:
        name: jq
        state: present

    - name: Create Slack integration script
      template:
        src: wazuh-slack-integration.sh.j2
        dest: /var/ossec/integrations/slack
        mode: "0750"
        owner: root
        group: wazuh

    - name: Configure Slack integration in ossec.conf
      blockinfile:
        path: /var/ossec/etc/ossec.conf
        marker: "<!-- {mark} ANSIBLE MANAGED SLACK INTEGRATION -->"
        insertbefore: "</ossec_config>"
        block: |
          <integration>
            <name>slack</name>
            <hook_url>{{ slack_webhook_url }}</hook_url>
            <level>10</level>
            <alert_format>json</alert_format>
            <options>{"channel": "#security-alerts"}</options>
          </integration>
      notify: restart wazuh-manager

Splunk Forwarder Integration

# playbooks/configure-splunk-forwarder.yml
---
- name: Configure Splunk forwarder for Wazuh
  hosts: wazuh_managers

  tasks:
    - name: Install Splunk Universal Forwarder
      unarchive:
        src: "{{ splunk_forwarder_url }}"
        dest: /opt
        remote_src: yes
        owner: splunk
        group: splunk

    - name: Configure Splunk forwarder inputs
      template:
        src: splunk-inputs.conf.j2
        dest: /opt/splunkforwarder/etc/system/local/inputs.conf

    - name: Configure Splunk forwarder outputs
      template:
        src: splunk-outputs.conf.j2
        dest: /opt/splunkforwarder/etc/system/local/outputs.conf

    - name: Start Splunk forwarder
      command: |
        /opt/splunkforwarder/bin/splunk start --accept-license
        /opt/splunkforwarder/bin/splunk enable boot-start

Ansible Vault for Sensitive Data

# group_vars/all/vault.yml (encrypted with ansible-vault)
$ANSIBLE_VAULT;1.1;AES256
66383439383437363764343838376...

# Decrypted content:
vault_wazuh_authd_password: "SecureAuthPassword123!"
vault_indexer_admin_password: "AdminPassword456!"
vault_wazuh_api_password: "ApiPassword789!"
vault_slack_webhook_url: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
vault_smtp_password: "SmtpPassword321!"
vault_ldap_bind_password: "LdapPassword654!"

Testing and Validation

Molecule Testing Configuration

# molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: wazuh-manager
    image: centos:7
    pre_build_image: true
    privileged: true
    command: /sbin/init
    groups:
      - wazuh_managers

  - name: wazuh-indexer
    image: centos:7
    pre_build_image: true
    privileged: true
    command: /sbin/init
    groups:
      - wazuh_indexers

  - name: wazuh-agent-centos
    image: centos:7
    pre_build_image: true
    privileged: true
    command: /sbin/init
    groups:
      - wazuh_agents
      - linux_agents

  - name: wazuh-agent-ubuntu
    image: ubuntu:20.04
    pre_build_image: true
    privileged: true
    command: /sbin/init
    groups:
      - wazuh_agents
      - linux_agents

provisioner:
  name: ansible
  inventory:
    group_vars:
      all:
        wazuh_manager_ip: wazuh-manager
        wazuh_cluster_name: test-cluster

verifier:
  name: ansible

Test Playbook

# molecule/default/verify.yml
---
- name: Verify Wazuh deployment
  hosts: all
  gather_facts: no

  tasks:
    - name: Check if Wazuh service is running
      systemd:
        name: "{{ 'wazuh-manager' if inventory_hostname in groups['wazuh_managers'] else 'wazuh-agent' }}"
      register: wazuh_service
      failed_when: wazuh_service.status.ActiveState != "active"
      when: inventory_hostname != 'wazuh-indexer'

    - name: Verify manager API is accessible
      uri:
        url: "https://{{ ansible_default_ipv4.address }}:55000/security/user/authenticate"
        method: GET
        user: wazuh
        password: "{{ wazuh_api_password }}"
        validate_certs: no
      when: inventory_hostname in groups['wazuh_managers']

    - name: Verify agent connection
      delegate_to: wazuh-manager
      shell: |
        /var/ossec/bin/agent_control -l | grep "{{ inventory_hostname }}"
      when: inventory_hostname in groups['wazuh_agents']

Production Best Practices

Performance Tuning Variables

# group_vars/production/performance.yml
# Manager performance settings
wazuh_max_agents: 5000
wazuh_queue_size: 131072
wazuh_rlimit_nofile: 65536
wazuh_worker_pool: 8
wazuh_logcollector_threads: 4

# Indexer performance settings
indexer_heap_size: "{{ (ansible_memtotal_mb * 0.5) | int }}m"
indexer_memory_lock: true
indexer_bootstrap_memory_lock: true

# Agent performance settings
wazuh_agent_buffer_disabled: no
wazuh_agent_buffer_size: 5120
wazuh_agent_buffer_events_per_second: 500
wazuh_agent_time_reconnect: 60

# File integrity monitoring optimization
wazuh_syscheck_max_eps: 100
wazuh_syscheck_process_priority: 10
wazuh_syscheck_synchronization_max_eps: 10

Security Hardening

# playbooks/harden-wazuh.yml
---
- name: Harden Wazuh deployment
  hosts: all

  tasks:
    - name: Configure firewall rules
      firewalld:
        service: "{{ item }}"
        permanent: yes
        state: enabled
      loop:
        - wazuh-authd
        - wazuh-cluster
        - wazuh-api
      when: inventory_hostname in groups['wazuh_managers']

    - name: Set secure file permissions
      file:
        path: "{{ item.path }}"
        owner: "{{ item.owner }}"
        group: "{{ item.group }}"
        mode: "{{ item.mode }}"
      loop:
        - {
            path: "/var/ossec/etc",
            owner: "root",
            group: "wazuh",
            mode: "0750",
          }
        - {
            path: "/var/ossec/etc/client.keys",
            owner: "root",
            group: "wazuh",
            mode: "0640",
          }
        - {
            path: "/var/ossec/logs",
            owner: "wazuh",
            group: "wazuh",
            mode: "0750",
          }

    - name: Configure SELinux contexts
      sefcontext:
        target: "{{ item }}"
        setype: wazuh_var_t
        state: present
      loop:
        - "/var/ossec(/.*)?"
        - "/var/ossec/logs(/.*)?"
      when: ansible_selinux.status == "enabled"

Monitoring Dashboard

graph TB
    subgraph "Wazuh Monitoring Stack"
        subgraph "Metrics Collection"
            PM[Prometheus]
            NE[Node Exporter]
            WE[Wazuh Exporter]
        end

        subgraph "Visualization"
            GF[Grafana]
            WD[Wazuh Dashboard]
        end

        subgraph "Alerting"
            AM[AlertManager]
            PD[PagerDuty]
            SL[Slack]
        end
    end

    WE --> PM
    NE --> PM
    PM --> GF
    PM --> AM
    AM --> PD
    AM --> SL

    style PM fill:#f96,stroke:#333,stroke-width:2px
    style GF fill:#9f9,stroke:#333,stroke-width:2px

Conclusion

This comprehensive Ansible deployment guide provides a complete automation solution for Wazuh, from initial setup to ongoing maintenance. By following these playbooks and best practices, you can deploy and manage Wazuh at scale with confidence.

Key benefits of this approach:

Remember to:

Resources