A Wazuh 5.0 cluster is two clusters that happen to share a name. The manager cluster is one master and any number of workers talking on port 1516, synchronising agent registrations, shared group configuration and group assignments. The indexer cluster is an OpenSearch cluster of its own, with its own node roles, discovery settings and certificates. They fail for different reasons and you size them separately. The change from 4.x that catches people out is that there is no longer anything to enable: every install, including a single box, is already a one-node cluster.
What you need
- A working single-node deployment to grow from. If you do not have one, start with installing Wazuh on Ubuntu, and know how you will back it up from upgrade, backup and index retention before you add nodes.
- The hardware budget from Wazuh system requirements and sizing. Per node the documented figures are 8 GB and 4 cores minimum for the manager, 16 GB and 8 cores recommended; 8 GB and 4 cores minimum for the indexer, 32 GB and 8 cores recommended; 4 GB and 2 cores minimum for the dashboard.
- At least 20 GB of disk on each manager. The manager no longer stores alerts, but the Vulnerability Scanner feed alone needs at least 15 GB.
- Network paths planned before you generate certificates, because the certificate tool bakes node names and addresses in.
Plan the node layout before you generate certificates
The certificate tool is driven by a single config.yml that lists every node by name and address. Getting that file right first saves a full redeployment, because each indexer node later expects a certificate whose common name matches its node.name.
nodes:
# Wazuh indexer nodes
indexer:
- name: indexer
ip: "<indexer-node-ip>"
#- name: indexer-2
# ip: "<indexer-node-ip>"
#- name: indexer-3
# ip: "<indexer-node-ip>"
# Wazuh manager nodes
# If there is more than one Wazuh manager
# node, each one must have a node_type
manager:
- name: manager
ip: "<wazuh-manager-ip>"
# node_type: master
#- name: manager-2
# ip: "<wazuh-manager-ip>"
# node_type: worker
# Wazuh dashboard nodes
dashboard:
- name: dashboard
ip: "<dashboard-node-ip>"
Generate the set, pack it, and copy the archive to every node:
bash ./wazuh-certs-tool-5.0.0-beta5.sh -A
tar -cvf ./wazuh-certificates.tar -C ./wazuh-certificates/ . && rm -rf ./wazuh-certificates
The manager side needs remoted.pem, remoted-key.pem and root-ca.pem under etc/certs. In 5.0 the enrolment daemon and the agent listener share that one pair, so a 4.x configuration that pointed <auth><ssl_manager_cert> at authd.pem has to be repointed or the overrides dropped. The HTTPS listener is fail-closed: without a readable certificate and key the manager refuses to start rather than coming up without an agent listener.
Build the manager cluster
Every manager node carries the same <cluster> block apart from node_name and node_type. On the master:
<cluster>
<name>wazuh</name>
<node_name>master-node</node_name>
<node_type>master</node_type>
<key>c98b62a9b6169ac5f67dfe55b73a8d2a</key>
<port>1516</port>
<bind_addr>0.0.0.0</bind_addr>
<nodes>
<node><MASTER_NODE_IP_ADDRESS></node>
</nodes>
<hidden>no</hidden>
</cluster>
On each worker the same block with a unique node_name and <node_type>worker</node_type>. The <nodes> list points at the master on every node, master included. A cluster supports exactly one master; extra entries in the list are ignored.
The key is a 32-character shared secret and it is a credential. Generate one with openssl rand -hex 16, read it off the master with a targeted sed, and keep it out of source control and shell history. Two nodes with the same node_name will not both join: one is rejected.
Defaults worth changing deliberately: bind_addr is 127.0.0.1, which is correct for a single node and wrong for every real cluster. port is 1516.
Verify from any node:
/var/wazuh-manager/bin/cluster_control -l
The documented output is a table of NAME TYPE VERSION ADDRESS, one row per node. cluster_control -a lists registered agents with the node each is recorded against, and cluster_control -i health prints cluster health.
The single-node lab confirms the “always a cluster” claim from the other direction. The default manager configuration shipped by the 5.0.0-beta5 image already carries a full <cluster> block, with a generated 32-character key and the container hostname as the only node:
<cluster>
<name>wazuh</name>
<node_name>manager</node_name>
<node_type>master</node_type>
<key>eee8bbd98e558a21955541f174a1b63a</key>
<port>1516</port>
<bind_addr>wazuh.manager</bind_addr>
<nodes>
<node>wazuh.manager</node>
</nodes>
<hidden>no</hidden>
</cluster>
The key in that block is the one the image generated for a throwaway lab; treat any key you find in a default configuration as something to replace before the second node joins. cluster_control and rbac_control are both present in /var/wazuh-manager/bin on beta5, and wazuh-manager-clusterd is running on a stock single-node install.
What the manager cluster actually synchronises
Agent registration data, shared configuration and agent group assignments. That is the list, and knowing it tells you what still has a single point of coordination. Enrolment tokens are minted on the master node and their store is written on the master. Manager API requests are routed to the correct node automatically, so you can point the dashboard or an automation script at any node.
What it does not synchronise is detection content. In 5.0 decoders, rules, KVDBs and integrations live in the indexer and are pulled by each manager through the indexer connector, so a worker with a broken indexer path is a worker with stale content, not a worker with its own copy.
Build the indexer cluster
Each indexer node edits /etc/wazuh-indexer/opensearch.yml:
network.host: "<indexer-node-ip>"
node.name: "indexer"
cluster.initial_manager_nodes:
- "indexer"
- "indexer-2"
- "indexer-3"
discovery.seed_hosts:
- "<indexer-1-ip>"
- "<indexer-2-ip>"
- "<indexer-3-ip>"
plugins.security.nodes_dn:
- "CN=indexer,OU=Wazuh,O=Wazuh,L=California,C=US"
- "CN=indexer-2,OU=Wazuh,O=Wazuh,L=California,C=US"
- "CN=indexer-3,OU=Wazuh,O=Wazuh,L=California,C=US"
node.name must match the name used in config.yml, and every node’s distinguished name must appear in nodes_dn on every node. After all nodes are installed and started, initialise the security index once, from any single node:
/usr/share/wazuh-indexer/bin/indexer-security-init.sh
Then confirm membership:
curl -k -u admin https://<WAZUH_INDEXER_IP_ADDRESS>:9200/_cat/nodes?v
Three nodes is the practical floor for production. The OpenSearch guidance is three dedicated cluster-manager-eligible nodes across three failure domains so the cluster never loses quorum, with data nodes added in multiples of the number of zones. On a small SOC you will run combined cluster-manager and data nodes rather than dedicated ones, which is fine at three nodes and stops being fine once search load starts interrupting cluster state work.
Point every manager at every indexer
The indexer connector takes a list, and the manager moves to another listed host when one becomes unavailable:
<indexer>
<hosts>
<host>https://10.0.0.1:9200</host>
<host>https://10.0.0.2:9200</host>
<host>https://10.0.0.3:9200</host>
</hosts>
<ssl>
<certificate_authorities>
<ca>etc/certs/root-ca.pem</ca>
</certificate_authorities>
<certificate>etc/certs/indexer-connector.pem</certificate>
<key>etc/certs/indexer-connector-key.pem</key>
</ssl>
</indexer>
The connector buffers in memory and discards buffered documents when the manager stops or restarts. Read that twice before you plan maintenance. The official guidance is to size the indexer for the expected rate rather than relying on the manager to absorb sustained indexing delays, and in practice that means you drain and stop ingestion deliberately rather than restarting a busy manager to pick up a config change.
Put agent traffic behind a load balancer
This is where 5.0 is genuinely easier than 4.x. A persistent AES connection pinned each agent to one node; HTTPS request-per-operation does not. Any node answers any request, the manager no longer tracks which node an agent is connected to, and ordinary HTTP infrastructure works without a custom protocol in the middle.
What that means for the load balancer configuration:
| Concern | Setting for Wazuh 5.0 |
|---|---|
| Listener | 1517/TCP, TLS 1.3 to the backend |
| Session affinity | Not required, and not wanted |
| PROXY protocol | Not used |
| Request integrity | A POST /stateful session is one request and must not be split or partially retried |
| Body size and timeouts | Aligned with the manager’s max_body_size, which defaults to 20MB |
| Path rewriting | Avoid it. The bearer token does not sign the path, so a rewritten path yields a 404, not a 401 |
Two of those settings are documented inconsistently in the beta, and both decide whether the load balancer works. The lab settles them. On a 5.0.0-beta5 manager the shipped <remote><https> block is:
<remote>
<https>
<port>1517</port>
<bind_addr>0.0.0.0</bind_addr>
<global_prefix>/wazuh-manager/</global_prefix>
<certificate>etc/certs/remoted.pem</certificate>
<key>etc/certs/remoted-key.pem</key>
</https>
So bind_addr defaults to 0.0.0.0, not 127.0.0.1, and the path prefix is real rather than an alternative to serving at the root. Point the health check at the prefix. Probing a beta5 manager gives a different answer at each path:
curl -sk https://<MANAGER>:1517/
{"error":"not_found"}
curl -sk https://<MANAGER>:1517/wazuh-manager/
{"status":"ok","module":"remoted"}
Decide on the legacy listener deliberately, because beta5 ships it enabled. The stock configuration contains <remote><legacy><enabled>yes</enabled> on port 1514/TCP, and <auth><port>1515</port> alongside it, so a default manager listens on 1514, 1515, 1516, 1517 and 55000. Remove or disable the legacy block if you have no 4.x agents: with it absent, no legacy socket is bound and only 5.x agents can connect. A 4.x agent on the legacy channel keeps events, keep-alives, centralised configuration and remote upgrade, but loses active response, inventory synchronisation and dbsync deltas, so treat it as a migration bridge and not as a steady state. The path off it is in the 4.x to 5 migration runbook.
Reference shapes by fleet size
These are shapes, not a sizing table. The disk arithmetic that drives them is in the sizing lesson; the documented starting point is 0.25 EPS and 3.7 GB per server, 0.1 EPS and 1.5 GB per workstation and 0.5 EPS and 7.4 GB per network device, each over 90 days.
| Fleet | Managers | Indexer nodes | Dashboard | Notes |
|---|---|---|---|---|
| Up to ~100 endpoints | 1 (still a cluster) | 1 | On the same host | The quickstart shape. Snapshot it before you touch anything |
| A few hundred | 1 master + 1 worker | 3 | Dedicated node | First point at which the load balancer and replicas matter |
| Low thousands | 1 master + 2 or more workers | 3 or more data nodes | Dedicated node, behind an identity-aware proxy | Add workers for agent connection load, indexer nodes for query and ingest load |
Add manager workers when agent-facing work is the constraint: enrolment bursts, event intake, shared configuration distribution. Add indexer nodes when search, aggregation or ingest is the constraint. Those are different symptoms and the wrong addition buys nothing, which is the most common piece of wasted money in a self-hosted deployment.
What to monitor
Monitor both clusters, not just the one whose dashboard you look at.
/var/wazuh-manager/bin/cluster_control -i health
curl -k -u admin https://<WAZUH_INDEXER_IP_ADDRESS>:9200/_cluster/health?pretty
On the manager API, GET /cluster/healthcheck, GET /cluster/nodes and GET /cluster/{node_id}/daemons/stats give you the same picture in a form an automation can consume, and GET /cluster/{node_id}/logs/summary saves an SSH session. On the indexer, confirm memory locking took effect, because a swapping indexer looks like a slow one:
curl -k -u <INDEXER_USERNAME>:<INDEXER_PASSWORD> "https://<INDEXER_IP_ADDRESS>:9200/_nodes?filter_path=**.mlockall&pretty"
The documented healthy value is true. If it is false, the indexer log at /var/log/wazuh-indexer/wazuh-indexer.log carries Unable to lock JVM Memory.
Add clock synchronisation to the monitored set. Agent bearer tokens have a 60-second lifetime with about 30 seconds of tolerated skew, so NTP drift on a manager node is a security-visible fault rather than a cosmetic one.
Common errors
Nodes never appear in cluster_control -l
Symptom: each manager reports only itself. Cause: the shared <key> differs between nodes, bind_addr is still 127.0.0.1, or 1516/TCP is filtered between nodes. Fix: compare the key on master and worker, set bind_addr to an address the other nodes can reach, and open 1516 between manager nodes only.
A worker joins and then disappears
Cause: two nodes are configured with the same node_name. The documented behaviour is that one of them is rejected. Fix: give every node a unique node_name and restart the manager on the renamed node.
(1244): Invalid configuration at '/remote/https/certificate': file not found:
Cause: the HTTPS agent listener has no certificate or key at the configured path. Fix: deploy remoted.pem and remoted-key.pem under etc/certs from the certificate archive before starting. Starting is fail-closed by design, so there is no partially working state to diagnose.
Cannot start the HTTPS agent listener:
Cause: the certificate pair exists but the service user cannot read it. Fix: correct ownership and mode on the files under etc/certs.
Agents get 401 from one node and not another
Cause: clock skew on that node, or a certificate rolled on some nodes and not others. Every credential failure collapses to a generic 401 on purpose, so the specific cause is in the manager log rather than in the response. Fix: check NTP on each manager, then check that all nodes present the same leaf and CA.
The indexer cluster forms but the dashboard shows no data
Cause: the managers are still pointing at one indexer node that is now a non-writable member, or the connector certificate was not redeployed after the node list changed. Fix: list every indexer node in <indexer><hosts> on every manager and verify with the documented connectivity check against /_cluster/health.
Where to go next
The exact option list for the manager cluster is in the official cluster configuration reference, and the agent-side protocol change that makes load balancing simple is described in the agent-manager protocol migration guide.
Once the cluster stands up, the next question is always throughput, which is performance tuning. If you are building this to serve more than one customer, the group and label design in multi-tenant Wazuh should be decided before the first agent enrols, and the exposure a distributed deployment creates is covered in hardening Wazuh itself.
Next: Wazuh Performance Tuning: EPS, Heap, Shards and Queues.
Files for this lesson
Every file this lesson refers to, exactly as used. Open a file to read it in the browser, or download the set as a zip. Download wazuh-cluster.zip. The full set for all lessons is at wazuh-tutorial-files.zip.
README.md3.2 KBcerts-config.yml1.2 KBcluster-health.sh1.9 KBcluster-master.xml1.1 KBcluster-worker.xml764 Bindexer-connector.xml1.0 KB
Frequently asked questions
Does Wazuh 5.0 still need the cluster to be enabled?
No. Every Wazuh 5.0 manager runs as a cluster node, including a single-node install. The 4.x disabled option inside the cluster block is no longer processed and is silently ignored if present. You turn the cluster off at service level instead, by stopping or disabling wazuh-manager-clusterd.
How many master nodes can a Wazuh cluster have?
Exactly one. The nodes list in the cluster block must contain a single entry. If more entries are present, the first is used as master and the rest are ignored. Every other manager node is configured as a worker.
Do Wazuh agents need session affinity on the load balancer?
No. Wazuh 5.0 agents make an independent HTTPS request per operation on port 1517, so any manager node can answer any request and spreading an agent across nodes is the intended behaviour. The one rule is that a POST /stateful request is a single session and must not be split or partially retried.
How many Wazuh indexer nodes should a production cluster have?
Three is the usual minimum for a production cluster, because three cluster-manager-eligible nodes in three failure domains keep quorum when one is lost. Below three nodes you cannot survive a node failure without losing quorum or data, and the shipped index templates default to zero replicas.
What port does Wazuh cluster communication use?
1516/TCP between manager nodes, set by the port option in the cluster block. It is separate from 1517 for agents, 55000 for the manager API, 9200 for the indexer and 443 for the dashboard.