Skip to main content

Comparison between Wazuh and CrowdSec

Having gained quite a lot of practical experience with both Wazuh and CrowdSec host intrustion detection systems, I wanted to share a few notes on how these compare in real life.

An important note for starters: this is not intended as a comprehensive or systematic review of either Wazuh or CrowdSec. Ther are already such comparisons but I rarely find them very useful in demonstrating the true capabilities of such tools in real-life environments. And this one will be precisely that: focused on my personal, hands-on experience with each of the tools, in no particular order.

Similarities

  • Both tools are designed to ingest vast amounts of operating system and applications logs, including local files, syslog, JSON and practically anything other that is in use today. They do this very well, they come with numerous parsers, decoders and data manipulation capabilities such as extraction, transformation and aggregation of individual data points. Tasks such as “alert me of any occurrence of Suricata signature of ET HUNT group, but not ET INFO” or “alert me of any series of bruteforce SSH attempts followed by a success” are precisely what they were designed for.
  • Both tools are highly extensible. You can write custom rules, decoders for custom log formats - this is extremely powerful part of any HIDS, and both tools are really good at it. For Wazuh, tons of community rules come packaged out of the box and for CrowdSec they can be browsed and installed from CrowdSec Hub.
  • Both systems are scalable. I have designed and am managing Wazuh deployments covering hundreds to thousands of servers, ranging from Linux, FreeBSD, Windows and even Solaris, but see note for CrowdSec below.
  • Alerting and response - both tools are perfectly suited to generate alerts of varying severity levels through popular channels ranging from email, Slack, PagerDuty, etc. Both are extensible so you can write custom alerting scripts that will send alerts to Matrix or Telegram, for example. Same goes for response - if you consider a scenario like “I want to update HAProxy blocklist config on all load-balancers, plus send a record to MISP, when HIDS detects a specific pattern”, that’s precisely what they were designed for.

Centralisation

CrowdSec comes with something called CAPI and LAPI. CAPI is the Central CrowdSec API operated by the vendor that seems to be mandatory in order to receive community blocklists and contribute your ban decisions. LAPI is the Local CrowdSec API that is used to manage the local instance of CrowdSec. In a single-machine setup it’s simply set to localhost but becomes mostly useful when you set up a distributed deployment with multiple nodes - the child notes talk to the parent node specifically through its LAPI. Both APIs are HTTP-based.

One of the most powerful but also limiting features of CrowdSec is its Dashboard website - it’s imply a vendor-provided visualisation and managementplatform that uses data submitted from your CAPI to present colorful charts and graphs such of number of attacks detected, prevented etc. The Dashboards are, as far as I’m aware, the only way to visualise SIEM feed from CrowdSec by design and to select blocklists you want to receive. It certainly is an useful tool, as are CrowdSec-provided blocklists, but with anything more complicated than a test set up you will very quickly find yourself in a situation where CrowdSec will on every occassion insist on buying a Pro subscription. CrowdSec is a great tool to deploy and learn from, but it’s clearly intended to offer a freemium model.

Wazuh on the other side is fully self-contained. The vendor does not offer any extra paid services, blocklists, central dashboards etc. The package you install comes with all rules for detection and SCA that are valid and maintained for that version. You installation will work just as well as a single Debian or FreeBSD package running as a classic daemon with no fancy dashboards, or as a full-fledged Docker Compose setup with OpenSearch dashboards customised for Wazuh. You are not pushed to buy any paid services, and you can manage your own blocklists and rules, or you can pull external ones like Firehol, MISP etc.

Architecture

Wazuh originates from an OSSEC project whose origins reach 2010’s and which was originally written for Linux and other Unix-like systems. The architecture is a classic set of Unix daemons that communicate through IPC - one monitors log files, another detects patterns etc. The original OSSEC is still alive (!) but Wazuh massively expanded it in terms of both functionality and scalability, also adding many “enterprise” features such as dashboards, support for AWS monitoring etc. Both Wazuh and OSSEC are Unix-style daemons driven by text configuration files and text rules (more about it later) that ingest one logs (input) and produce others (alerts), and they can interact with the outside world through shells scripts or binaries that trigger response or alerts - from “send a Telegram alert” to “block an IP on firewall”. Wazuh is packaged for most Unix-like systems, compiles on even more exotic ones, and Wazuh agent also runs on Windows. Wazuh/OSSEC are mostly written in C and C++, but also incorporate tons of Python and shell code for various auxiliary tasks. On the file system, Wazuh occupies its exclusive /var/ossec directory tree.

CrowdSec is a relatively modern application that is almost entirely written in Go with a couple of Unix-like daemons. Configuration and rules are written in YAML. It comes packaged for many Unix-like systems such as various Linux distributions and FreeBSD, it’s also readily available as OPNsense plugin (which is how I learned about it years ago). One component that stands out in CrowdSec is the bouncers which are an integrated and standard implementation of active response designed to smoothly work with the popular firewalls such as iptables, nftables or pf - this part simply works out of the box, but you need to be aware that it’s managed as a separate service that relies on LAPI to obtain the offending IPs. On FreeBSD at least all CrowdSec config lives in /usr/local/etc/crowdsec.

Summary: with Wazuh*, you install a Wazuh Manager on one server and then any number of Wazuh Agents on all other machines. Agents use the same code base, just run limited number of daemons whose job is limited to 1) pulling logs from the local system, 2) executing Manager-ordered tasks such as active response (“block an IP”). With CrowdSec you install the the CrowdSec package on all machines from which you want to ingest logs from. They may operate independently (reporting alerts to CAPI), but it makes more sense to make one of them the parent hosting LAPI, and configure all others to connect to that LAPI. Then, on all machines where you want to execute active response, you install the CrowdSec Bouncer packages which also connect to LAPI and receive instructions to block things. Both can also receive logs over syslog from devices where you can’t or don’t want to install things. In theory, you can run CrowdSec Bouncer without the monitoring component, it just needs access to some LAPI on the network.

Configuration

This will be probably the most interesting part of this article as it shows the real “meat” in each product described in high level above.

CrowdSec

Configuration is written in YAML, generally clean and concise. Same goes for decoders and rules. Here’s an example of very simple but fully working configuration to ingest Suricata JSON logs:

root@OPNsense:/usr/local/etc/crowdsec/acquis.d # cat suricata.yaml 
filename: /var/log/suricata/eve.json
labels:
  type: suricata-evelogs

Here’s a custom configuration file that tells CrowdSec not to block IPs from a specific range:

root@OPNsense:/usr/local/etc/crowdsec/parsers/s02-enrich # cat whitelist.yaml 
name: krvtz/whitelists
description: "Whitelist NVIDIA ranges"
whitelist:
  reason: "NVIDIA"
  cidr:
    - "80.84.160.0/20"

This is also a good opportunity to introduce the CrowdSec event processing workflow. Subdirs in parsers represent various stages that each event is traversing with what happens at each stage being self-evident from their names. Most of these are symlinks to stock CrowdSec parsers and processors, which are added when you install a specific parser package using cscli hub install command:

root@OPNsense:/usr/local/etc/crowdsec/parsers # ls -lR
total 12
drwxr-x---  2 root wheel 512 Mar 29  2023 s00-raw
drwxr-x---  2 root wheel 512 Mar 29  2023 s01-parse
drwxr-x---  2 root wheel 512 Jul  2 04:08 s02-enrich

./s00-raw:
total 0
lrwxr-x---  1 root wheel 74 Mar 29  2023 syslog-logs.yaml -> /usr/local/etc/crowdsec/hub/parsers/s00-raw/crowdsecurity/syslog-logs.yaml

./s01-parse:
total 0
lrwxr-x---  1 root wheel 82 Mar 29  2023 opnsense-gui-logs.yaml -> /usr/local/etc/crowdsec/hub/parsers/s01-parse/crowdsecurity/opnsense-gui-logs.yaml
lrwxr-x---  1 root wheel 75 Mar 29  2023 pf-logs.yaml -> /usr/local/etc/crowdsec/hub/parsers/s01-parse/firewallservices/pf-logs.yaml
lrwxr-x---  1 root wheel 74 Mar 29  2023 sshd-logs.yaml -> /usr/local/etc/crowdsec/hub/parsers/s01-parse/crowdsecurity/sshd-logs.yaml
lrwxr-xr-x  1 root wheel 78 Mar 29  2023 suricata-logs.yaml -> /usr/local/etc/crowdsec/hub/parsers/s01-parse/crowdsecurity/suricata-logs.yaml

./s02-enrich:
total 4
lrwxr-x---  1 root wheel  82 Mar 29  2023 dateparse-enrich.yaml -> /usr/local/etc/crowdsec/hub/parsers/s02-enrich/crowdsecurity/dateparse-enrich.yaml
lrwxr-x---  1 root wheel  78 Mar 29  2023 geoip-enrich.yaml -> /usr/local/etc/crowdsec/hub/parsers/s02-enrich/crowdsecurity/geoip-enrich.yaml
lrwxr-xr-x  1 root wheel  86 Jul  2 04:08 public-dns-allowlist.yaml -> /usr/local/etc/crowdsec/hub/parsers/s02-enrich/crowdsecurity/public-dns-allowlist.yaml
-rw-r--r--  1 root wheel 123 Aug  8  2023 whitelist.yaml

That’s for the parsing stage, that is where stream of events is analysed and filtered. The scenarios directory contains the actual scenarios that define the rules and actions to be taken based on the parsed events - for example, “detect Suricata alert annotated with severity level 1” (fragment only):

type: trigger
name: crowdsecurity/suricata-major-severity
description: "Detect exploit attempts via emerging threat rules"
filter: "evt.Meta.log_type == 'suricata_alert' && evt.Parsed.proto == 'TCP' && evt.Meta.suricata_rule_severity == '1'"
groupby: evt.Meta.source_ip

The scenarios available on my OPNsense install:

root@OPNsense:/usr/local/etc/crowdsec # ls -l scenarios/
total 0
lrwxr-x---  1 root wheel 72 Mar 29  2023 opnsense-gui-bf.yaml -> /usr/local/etc/crowdsec/hub/scenarios/crowdsecurity/opnsense-gui-bf.yaml
lrwxr-x---  1 root wheel 79 Mar 29  2023 pf-scan-multi_ports.yaml -> /usr/local/etc/crowdsec/hub/scenarios/firewallservices/pf-scan-multi_ports.yaml
lrwxr-x---  1 root wheel 63 Mar 29  2023 ssh-bf.yaml -> /usr/local/etc/crowdsec/hub/scenarios/crowdsecurity/ssh-bf.yaml
lrwxr-xr-x  1 root wheel 74 Jul  2  2024 ssh-cve-2024-6387.yaml -> /usr/local/etc/crowdsec/hub/scenarios/crowdsecurity/ssh-cve-2024-6387.yaml
lrwxr-xr-x  1 root wheel 73 Jul  2 04:08 ssh-generic-test.yaml -> /usr/local/etc/crowdsec/hub/scenarios/crowdsecurity/ssh-generic-test.yaml
lrwxr-xr-x  1 root wheel 73 May 15  2025 ssh-refused-conn.yaml -> /usr/local/etc/crowdsec/hub/scenarios/crowdsecurity/ssh-refused-conn.yaml
lrwxr-x---  1 root wheel 68 Mar 29  2023 ssh-slow-bf.yaml -> /usr/local/etc/crowdsec/hub/scenarios/crowdsecurity/ssh-slow-bf.yaml
lrwxr-xr-x  1 root wheel 72 Mar 29  2023 suricata-alerts.yaml -> /usr/local/etc/crowdsec/hub/scenarios/crowdsecurity/suricata-alerts.yaml

When a scenario is detected, several things can happen. You can either trigger response (e.g. ban) immediately, or you can start a “leaky bucket” counter and trigger response only if a certain threshold is reached in certain time window. Also, even triggered scenarios can be ultimately allowlisted - this may come as a surprise, because CrowdSec generously applies this by default to popular CDN and proxy services such as Cloudflare.

Wazuh

First thing you’ll notice in Wazuh is that it’s got… a long history. Main Wazuh configuration resides in /var/ossec/etc/ossec.conf and is written in XML that looks like this

<sca>
  <enabled>yes</enabled>
  <scan_on_start>yes</scan_on_start>
  <time>04:00</time>
  <skip_nfs>yes</skip_nfs>
  <policies>
    <policy>ruleset/sca/cis_freebsd14.yml</policy>
  </policies>
</sca>

Well, an important disclaimer here - it is not syntactically correct XML, more of a XML-like domain-specific language, so forget about automatic config generation or validation using XML schemas. The syntax is also inconsistent at moments - because some modules are, well, <enabled>yes</enabled>, but others are enabled when <disabled>no</disabled>.

The event flow in Wazuh/OSSEC is much simpler: the system ingests raw logs, then passes them through decoders and decoded (structured) events are passed to rules which detect patterns. Sample custom decoder to extract source and destination IP from a HAProxy log:

<decoder name="opn-haproxy">
	<parent>opn-haproxy</parent>
	<regex>from (\d+.\d+.\d+.\d+):(\d+)</regex>
	<order>srcip,srcport</order>
</decoder>

Detection rules in Wazuh are organised as a tree. In the rules below, any log entry where progam name was decoded as dnsmasq (classic syslog format example) will be first matched as rule 10200 and assigned level 0, that is muted. If nothing else matches, the event is discarded. But if the next rule happens to match a substring DNS-rebind in the original log entry, it will actually raise level 6 alert with the specified description.

<rule id="10200" level="0">
  <program_name>dnsmasq</program_name>
  <description>dnsmasq: events grouped</description>
</rule>
<rule id="100201" level="6">
  <if_sid>10200</if_sid>
  <match>DNS-rebind</match>
  <description>dnsmasq: possible DNS rebinding</description>
</rule>

You can also match events within a specific timeframe and thresholds, very much like CrowdSec’s leaky bucket logic - the following will only match for 1000 connections from the same IP within 60 seconds, and generate level 5 alert:

<rule id="100301" level="5" frequency="1000" timeframe="60">
    <if_matched_sid>10300</if_matched_sid>
    <same_srcip/>
    <description>haproxy: 1000 connections from same IP $(srcip) in 1 minute.</description>
  </rule>

The further course of events is also entirely configurable, but in most cases you will just send webhook to PagerDuty for levels like 10+, an email for 15+ etc. You can of course also take a concrete action, like invoking a script to do whatever you like with the offender, as your script will receive the whole contents of the parsed event in JSON. The simplest possible response is shown below:

<active-response>
  <disabled>no</disabled>
  <command>ipset</command>
  <location>all</location>
  <rules_id>131104</rules_id>
</active-response>

This means each time the specified rule 131104 is triggered (that’s a rather unmistakable Apache2 CVE-2021-41773 exploit signature), the whole message is passed to a script /var/ossec/active-response/bin/ipset.sh which will extract the offender’s IP and add it to an ipset blocklist on all agents currently connected.

Wazuh has a very convenient rule testing engine which shows you the whole decoding, parsing and rule matching logic, along with the final outcome:

# /var/ossec/bin/wazuh-logtest
Starting wazuh-logtest v4.3.10
Type one log per line

66.102.8.172 - - [04/Dec/2025:02:25:53 +0000] "GET /wf/open?upn=u001.mSyv5Rid1mwBOqKr82IFZNEUkKyiFFERlL4FaMTWfihtNt7k3NMBaAwRPHGwj0-2BYjJGeKSudVRhU-2FcsuY3JdN75ylY4gSOgG01h8k4z09owjG8qMYTHa5rE468rZM7Xs7Zm2um7yj5V4VT3VXb3cIYUSIw7ElHfC-2BNXGuhfrpjRSwv0VnX2rsLWTh5DNrZkzr-2FjF5jjED0DqtJKCoEvsugzgfCcZpVL5AtOM0A0o1V7Zq8Z10kCQOJ2E2K4HOP5i7CMdVYtJ7PxGknw1SAtSthXbPZoYF5uhKtNffUr3SATKWKxU1WHm8tGnE0-2Bf8hX3O3HPdOSn-2B4TkHdWBvJ24CcOV4DXooV3FTMs0KFp-2BEjGTvTv2K4kvSkMUNe-2B3JIfMllszaZcXZrEwXrK9N57BqIkR5-2F-2B62kXZ9fuWwswAXJ0AZyfF80C6ff1op8nAn22D6NxMy-2FzVCxI0n-2B0EO4zLKQ-3D-3D HTTP/1.1" 200 43 "-" "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)" "66.102.8.172, 66.102.8.172"

**Phase 1: Completed pre-decoding.
	full event: '66.102.8.172 - - [04/Dec/2025:02:25:53 +0000] "GET /wf/open?upn=u001.mSyv5Rid1mwBOqKr82IFZNEUkKyiFFERlL4FaMTWfihtNt7k3NMBaAwRPHGwj0-2BYjJGeKSudVRhU-2FcsuY3JdN75ylY4gSOgG01h8k4z09owjG8qMYTHa5rE468rZM7Xs7Zm2um7yj5V4VT3VXb3cIYUSIw7ElHfC-2BNXGuhfrpjRSwv0VnX2rsLWTh5DNrZkzr-2FjF5jjED0DqtJKCoEvsugzgfCcZpVL5AtOM0A0o1V7Zq8Z10kCQOJ2E2K4HOP5i7CMdVYtJ7PxGknw1SAtSthXbPZoYF5uhKtNffUr3SATKWKxU1WHm8tGnE0-2Bf8hX3O3HPdOSn-2B4TkHdWBvJ24CcOV4DXooV3FTMs0KFp-2BEjGTvTv2K4kvSkMUNe-2B3JIfMllszaZcXZrEwXrK9N57BqIkR5-2F-2B62kXZ9fuWwswAXJ0AZyfF80C6ff1op8nAn22D6NxMy-2FzVCxI0n-2B0EO4zLKQ-3D-3D HTTP/1.1" 200 43 "-" "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)" "66.102.8.172, 66.102.8.172"'

**Phase 2: Completed decoding.
	name: 'web-accesslog'
	id: '200'
	protocol: 'GET'
	srcip: '66.102.8.172'
	url: '/wf/open?upn=u001.mSyv5Rid1mwBOqKr82IFZNEUkKyiFFERlL4FaMTWfihtNt7k3NMBaAwRPHGwj0-2BYjJGeKSudVRhU-2FcsuY3JdN75ylY4gSOgG01h8k4z09owjG8qMYTHa5rE468rZM7Xs7Zm2um7yj5V4VT3VXb3cIYUSIw7ElHfC-2BNXGuhfrpjRSwv0VnX2rsLWTh5DNrZkzr-2FjF5jjED0DqtJKCoEvsugzgfCcZpVL5AtOM0A0o1V7Zq8Z10kCQOJ2E2K4HOP5i7CMdVYtJ7PxGknw1SAtSthXbPZoYF5uhKtNffUr3SATKWKxU1WHm8tGnE0-2Bf8hX3O3HPdOSn-2B4TkHdWBvJ24CcOV4DXooV3FTMs0KFp-2BEjGTvTv2K4kvSkMUNe-2B3JIfMllszaZcXZrEwXrK9N57BqIkR5-2F-2B62kXZ9fuWwswAXJ0AZyfF80C6ff1op8nAn22D6NxMy-2FzVCxI0n-2B0EO4zLKQ-3D-3D'

**Phase 3: Completed filtering (rules).
	id: '131105'
	level: '7'
	description: 'Possible Log4j RCE attack attempt detected.'
	groups: '['web', 'accesslog', 'log4j']'
	firedtimes: '1'
	mail: 'False'
	mitre.id: '['T1190', 'T1210', 'T1211']'
	mitre.tactic: '['Initial Access', 'Lateral Movement', 'Defense Evasion']'
	mitre.technique: '['Exploit Public-Facing Application', 'Exploitation of Remote Services', 'Exploitation for Defense Evasion']'
**Alert to be generated.

Other Wazuh features

One important advantage of Wazuh, also contributing to its complexity, is abundance of additional host-intrusion detection capabilities, such as file integrity monitoring, log analysis, and vulnerability scanning. Notably, CrowdSec does not offer any of these directly.

  • Security Configuration Assessment (SCA), which periodically scans the system against defined best security pratices baseline, such as CIS benchmarks. Configured by <sca> module.
  • File Integrity Monitoring (FIM), which monitors critical operating system file changes and alerts on suspicious activity. Configured by <syscheck> module.
  • Malware Detection, which checks the system for signatures of known botkits, rootkits and other operating system specific malware. Full-system AV scan is neither provided nor intended part of this feature, it maintains a rather limited list of IOCs known for this particular platform, such the most popular Linux rootkits, but Wazuh also handles ClamAV and other classic malware scanners logs pretty well. Configured by <rootcheck> module.
  • Vulnerability Scanning involves periodic operating system package scans checked against OVAL and NVD vulnerability databases.
  • Remote agent management which, for most of the above features, allows centralised configuration management from the Wazuh manager which is then applied to agents, greatly simplifying their maintenance, as security monitoring configuration changes don’t need to be applied by a separate deployment tools. In practice, this also keeps the security management basked in the hands of DevSecOps team without the need to interfere with typical DevOps processes.

Please note that in the above list I have merely scratched the surface and most of the above modules are quite complex, and at some places mutually overlapping. Nonetheless, the features are there, they do work and I had been working with them for years in environments involving hundeds and thousands of monitored systems.

Find me on Fediverse, feel free to comment! See how