LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Wazuh HIDS: Self-Hosted Alternative to CrowdStrike and Falcon

wazuhhidssecuritysiemedrmonitoringself-hostedossec

If you have ever priced out a commercial EDR for a fleet of a few hundred Linux boxes, you know the quiet drawer where budget proposals go to die. CrowdStrike Falcon, SentinelOne, and Microsoft Defender for Endpoint all produce genuinely good telemetry, but the per-endpoint pricing does not scale graciously for academic clusters, research labs, or mid-sized companies with a heavy infrastructure footprint. At some point you find yourself asking whether the open-source world has a serious answer.

Wazuh is the serious answer. It’s a fork of OSSEC that has grown up into a full host-based intrusion detection platform with a web UI, a rules engine, file-integrity monitoring, vulnerability scanning, container inspection, and a SIEM frontend all glued to the OpenSearch stack. It’s not pretending to be a cutting-edge EDR — it is not going to do in-kernel behavioral detection the way a commercial agent does. But for the 80% of “I need to know when /etc/shadow changes, when someone runs wget from /tmp, when a sudden SSH bruteforce hits, and when a CVE lands on one of my hosts,” Wazuh is very capable and very free.

This post walks the architecture, agent internals, rule writing, decoders, active response, and the scaling concerns you hit when you go past a few thousand agents.

What Wazuh actually is

There are four pieces, and understanding the split is important because it governs how you deploy and scale:

  • Wazuh agent — a ~15 MB C daemon (wazuh-agentd, plus siblings like wazuh-syscheckd, wazuh-logcollector, wazuh-modulesd) installed on each monitored endpoint. It collects logs, hashes files, enumerates running processes, scans for vulnerable packages, and streams all of that to the manager.
  • Wazuh manager — the server that receives agent data, runs the rules engine and decoders, stores the processed alerts, and forwards them to the indexer. This is where the ruleset lives and where detection logic evaluates.
  • Wazuh indexer — a fork of OpenSearch (which is itself a fork of Elasticsearch). Holds alerts, agent state, and inventory data. Queryable from the dashboard.
  • Wazuh dashboard — a fork of OpenSearch Dashboards (née Kibana). Visualizes alerts, MITRE ATT&CK coverage, vulnerability reports, and FIM (file integrity monitoring) findings.

The manager can be a single node for small deployments or a cluster (one master, many workers) for serious volume. The indexer is almost always a cluster in production because you want hot-warm-cold tiering. In a homelab or small production setup, all four components run on one box; at scale they become three separate tiers.

Agents talk to the manager over TCP 1514 (encrypted with a pre-shared key per agent, registered via agent-auth on port 1515). Managers talk to the indexer over HTTPS 9200. The dashboard is the only thing users hit, over 443.

The agent’s modules

A Wazuh agent is not one daemon — it is a handful of specialized collectors:

  • wazuh-logcollector — tails log files (or reads the Windows event log, or journalctl). Ships line by line to the manager for decoder/rule evaluation.
  • wazuh-syscheckd — file integrity monitoring. Walks a configurable set of directories, hashes files, stores a baseline, and alerts on changes. Can watch registry keys on Windows.
  • wazuh-modulesd — hosts periodic tasks: vulnerability detection, SCA (security configuration assessment), command monitoring (run a shell command and submit the output), OSQuery integration, CIS-CAT, rootcheck.
  • wazuh-agentd — the pipe to the manager. Multiplexes data from all the other collectors, handles reconnection, buffers when the manager is down.
  • wazuh-execd — receives active-response commands from the manager and runs them locally. This is the scary one: it can block IPs via iptables, kill processes, or disable accounts. Think carefully before giving it broad authority.

All of this is configurable through /var/ossec/etc/ossec.conf on the agent, which is an XML file. The XML is not hip but it is stable — you’ll work with it a lot.

Installing the manager

The happy path is the official Wazuh all-in-one installer, which stands up manager, indexer, and dashboard on a single VM in about 15 minutes:

curl -sO https://packages.wazuh.com/4.9/wazuh-install.sh
sudo bash wazuh-install.sh -a

You get a self-signed cert, an auto-generated admin password (printed once to the terminal — capture it or you’ll be regenerating it from passwords tool), and a reachable dashboard on https:///.

For real deployments, run the three components on separate hosts. The step-by-step docs walk each component’s systemd units, certificates (wazuh-certs-tool.sh), and config files. Plan on:

  • Manager: 4–8 vCPU, 8–16 GB RAM for up to a few thousand agents. RAM is important because the ruleset is all loaded into memory.
  • Indexer: 3-node cluster, each 8 vCPU and 16+ GB RAM, with dedicated SSD. OpenSearch’s memory profile and JVM heap sizing are the same as Elasticsearch — heap = 50% of RAM capped at 31 GB.
  • Dashboard: 2 vCPU, 4 GB RAM. Stateless; can be scaled behind a load balancer.

Enrolling agents

Wazuh has three enrollment models:

  1. Manual key — generate on the manager (manage_agents tool), paste into the agent. Works, tedious at scale.
  2. agent-auth with a shared password — agents register themselves using a shared secret (/var/ossec/etc/authd.pass). Simple, but rotating the shared secret is a pain.
  3. agent-auth with certificate verification — agents present a client certificate signed by a CA the manager trusts. This is the right approach for production: revoking a compromised agent means revoking a cert.

On the agent side after install:

# /var/ossec/etc/ossec.conf
<client>
  <server>
    <address>wazuh-manager.internal</address>
    <port>1514</port>
    <protocol>tcp</protocol>
  </server>
  <enrollment>
    <enabled>yes</enabled>
    <manager_address>wazuh-manager.internal</manager_address>
    <port>1515</port>
    <authorization_pass_path>/var/ossec/etc/authd.pass</authorization_pass_path>
  </enrollment>
</client>

Restart wazuh-agent, and within seconds the manager shows the agent as “active” in the UI.

Grouping agents. Every agent belongs to at least one group (default: default). Groups let you push different configurations — FIM rules for web servers differ from DB servers. Put agents into groups with /var/ossec/bin/agent_groups -a -i <id> -g webservers, and place the group’s custom config in /var/ossec/etc/shared/webservers/agent.conf. On next sync, the agent receives the group config merged with the local one.

Decoders: turning lines into structured events

When the agent ships a log line like:

Feb 21 14:33:02 ny-web-01 sshd[13551]: Failed password for alice from 203.0.113.44 port 50012 ssh2

the manager has to extract structured fields (srcuser=alice, srcip=203.0.113.44) before it can reason about the event. That is a decoder’s job. Decoders live in XML at /var/ossec/ruleset/decoders/ (built-in) and /var/ossec/etc/decoders/ (your customizations).

A decoder matches a program name first, then applies a regex to extract fields:

1
2
3
4
5
6
7
8
9
<decoder name="sshd">
  <program_name>sshd</program_name>
</decoder>

<decoder name="sshd-failed-password">
  <parent>sshd</parent>
  <regex>^Failed password for (\S+) from (\S+) port (\d+)</regex>
  <order>srcuser, srcip, srcport</order>
</decoder>

The parent-child relationship is crucial: sshd-failed-password only fires after sshd matches. Wazuh ships thousands of decoders out of the box (sshd, nginx, Apache, Windows Security events, Kubernetes audit, Docker, sysmon) — you’ll rarely write a decoder from scratch unless you have a homegrown app producing structured logs.

For JSON logs, the decoder is dead simple:

1
2
3
4
<decoder name="my-app">
  <prematch>^{"app":"my-app"</prematch>
  <plugin_decoder>JSON_Decoder</plugin_decoder>
</decoder>

Every top-level key becomes a field available for rules.

Rules: from events to alerts

Rules live at /var/ossec/ruleset/rules/ (built-in) and /var/ossec/etc/rules/local_rules.xml (yours). They consume decoded fields and produce alerts with a severity level from 0 (ignore, useful for correlation parents) to 15 (catastrophic).

Simplest form:

1
2
3
4
5
<rule id="100100" level="5">
  <if_sid>5716</if_sid>
  <srcip>10.0.0.0/8</srcip>
  <description>SSH failed login from internal network</description>
</rule>

if_sid inherits matching from rule 5716 (the built-in sshd failed-password rule). The srcip filter narrows to internal IPs. Level 5 is low-severity; a dashboard filter would show this as info.

Rules support frequency-based matching — the classic “brute force detection” pattern:

1
2
3
4
5
6
7
8
<rule id="100101" level="10" frequency="5" timeframe="60">
  <if_matched_sid>5716</if_matched_sid>
  <same_source_ip />
  <description>Possible SSH brute force: 5+ failures from same IP in 60s</description>
  <mitre>
    <id>T1110</id>
  </mitre>
</rule>

This fires when rule 5716 matches 5 times from the same source IP within 60 seconds. The <mitre> block maps to ATT&CK tactics, which populate the ATT&CK dashboard.

Key rule features you will reach for:

  • <if_sid> / <if_matched_sid> / <if_group> — compose against existing rules rather than matching raw log text.
  • <field name="some.json.field">pattern</field> — match inside JSON-decoded fields (including nested paths).
  • <list> with a CDB list — compile a list of known-bad IPs, user agents, or hostnames with ossec-makelists, then reference in rules for fast lookups.
  • <same_user />, <same_source_ip />, <same_field>…</same_field> — correlation across events.
  • <description> — supports variables like $(srcip) that get substituted into the alert message.

Rule IDs: built-in rules use 1–99999 (reserved) and 100000+ for your customizations. Do not edit built-in rules directly — override with a rule of the same id in local_rules.xml (or use <overwrite>yes</overwrite>).

Testing rules without waiting for real traffic

The wazuh-logtest tool is the rule-development iteration loop:

$ /var/ossec/bin/wazuh-logtest
Starting wazuh-logtest v4.9.0
Type one log per line

Feb 21 14:33:02 ny-web-01 sshd[13551]: Failed password for alice from 203.0.113.44 port 50012 ssh2

**Phase 1: Completed pre-decoding.
    full event: 'Feb 21 14:33:02 ny-web-01 sshd[13551]: Failed password for alice from 203.0.113.44 port 50012 ssh2'
    timestamp: 'Feb 21 14:33:02'
    hostname: 'ny-web-01'
    program_name: 'sshd'

**Phase 2: Completed decoding.
    name: 'sshd'
    parent: 'sshd'
    dstuser: 'alice'
    srcip: '203.0.113.44'

**Phase 3: Completed filtering (rules).
    id: '5716'
    level: '5'
    description: 'sshd: authentication failed.'
    groups: '['sshd', 'authentication_failed']'

Paste a real log line, see what decodes, see what rule hits. This is the fastest way to validate your work before restarting the manager.

File Integrity Monitoring

FIM is the feature I’ve gotten the most use out of. Configure what to watch in the agent’s <syscheck> block:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<syscheck>
  <frequency>43200</frequency>

  <directories check_all="yes" realtime="yes">/etc,/usr/bin,/usr/sbin</directories>
  <directories check_all="yes" report_changes="yes" whodata="yes">/var/www</directories>

  <nodiff>/etc/shadow</nodiff>
  <nodiff>/etc/gshadow</nodiff>

  <ignore>/etc/mtab</ignore>
  <ignore>/etc/hosts.deny</ignore>
  <ignore type="sre">.*\.swp$</ignore>
</syscheck>

Key options:

  • realtime="yes" — use inotify on Linux (or ReadDirectoryChangesW on Windows). Alerts within seconds of a change.
  • whodata="yes" — leverage the Linux audit subsystem to record who changed the file (process, UID, command line). Requires auditd to be running and consumes audit events.
  • report_changes="yes" — include a diff of the file content in the alert. Useful for config files, dangerous for files with secrets (hence <nodiff> to suppress content).
  • <ignore type="sre"> — posix-extended regex. Useful for ignoring temp files, swap files, log rotation.

Every change produces an alert with the path, old hash, new hash, and (if whodata is on) the process that did it. In practice, you quickly tune to ignore /etc/resolv.conf (DHCP rewrites it), /etc/krb5.keytab (Kerberos rotates it), and similar.

Vulnerability detection

The vulnerability detector module pulls CVE feeds (NVD, Red Hat OVAL, Ubuntu OVAL, Debian OVAL, Microsoft), cross-references against the agent’s package inventory, and produces alerts when a CVE lands on a system. Enable on the manager:

1
2
3
4
5
<vulnerability-detection>
  <enabled>yes</enabled>
  <index-status>yes</index-status>
  <feed-update-interval>60m</feed-update-interval>
</vulnerability-detection>

And on the agent, the inventory module must be running (it is by default). After a feed sync, the dashboard’s Vulnerabilities view shows a per-host list of CVEs with CVSS scores, severity, and fix availability.

Two caveats:

  • Not every CVSS 9+ is a real risk in your environment. The Vulnerabilities view is noisy by default; filter to Critical + known-exploited (via CISA KEV integration) for on-call attention.
  • The agent reports installed package versions. If you install a patched package but haven’t restarted the service still running the vulnerable library, Wazuh shows you clean while you’re actually still exposed. Wazuh can’t see which processes have unlinked files mapped — tools like checkrestart or needrestart complement FIM here.

Active response

This is the module that moves Wazuh from “notice things” to “do things.” Example: on N SSH brute-force alerts, add the source IP to iptables for an hour.

On the manager’s ossec.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<command>
  <name>firewall-drop</name>
  <executable>firewall-drop</executable>
  <timeout_allowed>yes</timeout_allowed>
</command>

<active-response>
  <command>firewall-drop</command>
  <location>local</location>
  <rules_id>5712,100101</rules_id>
  <timeout>3600</timeout>
</active-response>
  • <location>local</location> — run on the agent that produced the alert.
  • <location>all</location> — run on every agent (for, say, blocking a known-bad IP everywhere).
  • <timeout>3600</timeout> — undo the action after 1 hour. Prevents permanent blocks from a false positive.

The bundled scripts (firewall-drop, host-deny, disable-account, restart-ossec) live at /var/ossec/active-response/bin/. You can write custom ones in any language; they receive the alert JSON on stdin and return a status.

Danger: active response is how you self-DoS. I once had a rule match sshd: connection from X without scoping to “failed” — we blocked every legitimate connection within minutes. Always:

  1. Test active response commands in a dev group first.
  2. Maintain an exclude list of known-good IPs (bastion, monitoring, your own office egress).
  3. Set <timeout> so mistakes auto-heal.
  4. Use <repeated_offenders> to escalate durations rather than going straight to permanent blocks.

Rootcheck and SCA

Two periodic scans worth enabling:

Rootcheck — scans for known rootkits, suspicious hidden files, process/port discrepancies. Catches the obvious stuff (old-school rootkits that hide processes from ps). Modern attackers are cleverer, but it’s a zero-effort layer.

SCA (Security Configuration Assessment) — runs CIS Benchmark–style checks against the agent and produces a score. Out-of-the-box policies cover Ubuntu, RHEL, Debian, Windows, macOS. You can write custom SCA policies as YAML — for example, checking that sshd_config disallows root login:

1
2
3
4
5
6
7
checks:
  - id: 10001
    title: "Disable SSH root login"
    compliance:
      - cis: ["5.2.10"]
    rules:
      - 'f:/etc/ssh/sshd_config -> !r:^\s*PermitRootLogin\s+no'

The SCA dashboard shows pass/fail/unknown per check per host, and historical trending as you fix issues.

Integrations that matter

TheHive / Cortex — Wazuh can export alerts to TheHive as cases, giving you a real case-management workflow for incident response.

OSQuery — the agent can host an osquery instance and run periodic queries. Good for fleet inventory, lateral movement detection, ad-hoc hunting.

VirusTotal — enrich FIM alerts with VT scan results. When a new binary appears in a watched directory, Wazuh can hash-lookup against VT and include the result in the alert.

Slack/PagerDuty/Jira — via the <integration> block in the manager config. Alerts above a level threshold get posted to the channel. Keep the threshold high (level 12+) or Slack becomes unusable.

MISP — threat intel sharing. Pull IOCs from MISP into Wazuh CDB lists and alert on matches.

Scaling: where the manager starts to hurt

A single manager can comfortably handle 1k-2k agents on decent hardware. Beyond that, you need the cluster mode:

  • Master — one node. Owns config, rule updates, agent registration.
  • Workers — any number. Receive agent connections, run the rules engine, forward to the indexer.

An HAProxy or similar in front of the workers distributes agents. The master does not handle agent traffic in cluster mode — it coordinates.

The indexer side is where most scaling pain lives. OpenSearch cluster sizing is its own art:

  • Separate hot (current week, SSD, queried often) from warm (older, HDD, rarely queried) with ISM (index state management) policies.
  • Daily index rotation (wazuh-alerts-4.x-YYYY.MM.DD) with a rollover policy.
  • Plan for ~1-2 KB per alert after compression. At a rate of 50 alerts/sec aggregated across 1k agents, that’s ~4 GB/day — manageable. At 500 alerts/sec (chattier ruleset, more agents), you’re at 40 GB/day and retention is a budget decision.
  • Shard count matters. Default is 3 primaries + 1 replica; for small deployments, drop to 1 primary + 1 replica per daily index to avoid the “thousands of tiny shards” pathology.

Tuning noise down

Fresh deployments are noisy. The most common noise sources:

  • FIM on /var/log — log rotations produce constant change alerts. Exclude.
  • FIM whodata without audit rules tuning — audit backlog overflows and you lose kernel events. Either increase audit_backlog_limit=8192 on the kernel cmdline, or drop whodata="yes" from very busy directories.
  • SSH failures from scanning IPs — every internet-facing host is under constant attack. Aggregate, don’t alert per-event. Rule 5716 fires forever; the real signal is the frequency rule at a higher level.
  • Sudo audit events — every sudo produces an alert. For an engineer-heavy fleet, filter by outcome (“sudo failure,” not “sudo success”).

Spend the first month after deployment almost entirely on noise reduction. A high-signal Wazuh is trustworthy; a Wazuh full of false positives is ignored.

What Wazuh isn’t

Wazuh is explicitly not an EDR in the CrowdStrike sense. It does not hook syscalls in the kernel, does not trace process ancestry in real-time, does not intercept binary executions for behavioral analysis. It watches logs and filesystems. If you need eBPF-grade runtime visibility, pair Wazuh with Falco, Tetragon, or osquery’s kernel extension and feed those events into Wazuh for rule processing.

Wazuh also is not a full SIEM. It has a SIEM-flavored dashboard thanks to OpenSearch, but it lacks the sophisticated correlation-across-log-sources you get from Splunk ES or Elastic Security. What it is excellent at: being the host-level data source that feeds into a larger SIEM, or serving as the SIEM itself for a smaller shop.

Closing thoughts

For a homelab, Wazuh is absurd overkill in the best way — you get a professional-grade alerting platform for the cost of a VM. For a small-to-mid company with a competent sysadmin, Wazuh replaces a six-figure EDR license with some config-writing and a few hours of rule tuning per month. For a large enterprise, Wazuh is a respectable floor that handles file integrity, vulnerability detection, and compliance reporting while you layer behavioral EDR on top for the hard stuff.

The trick with Wazuh — and with any self-hosted security platform — is that the tool is a small fraction of the work. The rules, the decoders, the active response scripts, the noise reduction, the on-call integration, the “we actually investigate alerts” culture — that’s the work. Wazuh gives you a competent foundation; the rest is up to the team. But that is exactly the same tradeoff commercial EDR products face. What Wazuh lets you do is spend the money you would have spent on licenses on an engineer who understands the tool and the threats you’re actually facing.

Comments