Sysadmin

Self-Hosting Security: Wazuh + CrowdSec on a VPS

Running your own servers means taking responsibility for their security. That responsibility is not optional and it doesn’t get easier with time — attackers automate their reconnaissance, which means the window between exposing a service and receiving the first probe is measured in minutes, not days. My setup uses Wazuh as the SIEM backbone and CrowdSec as the active defence layer. They complement each other in ways that took me a while to understand, and together they catch things that either tool alone would miss.

What Wazuh is actually doing

Wazuh is a SIEM built on top of OSSEC with a significantly better dashboard and a more manageable rule engine. It runs as a manager (the central component) with lightweight agents on each monitored host. The agent collects log data, file integrity events, and vulnerability information, then ships it to the manager for correlation and alerting.

Out of the box, Wazuh ships with several thousand rules covering common attack patterns. The important ones for a self-hosted setup:

File integrity monitoring. You define a list of paths — /etc/, /usr/bin/, /var/www/ — and Wazuh calculates checksums. Any change to a monitored file generates an alert. This catches both attackers who’ve gained access and botched package updates that overwrite something they shouldn’t.

Log analysis. The agent parses system logs (auth.log, syslog, application-specific logs) against the rule set and generates alerts for events like repeated sudo failures, SSH brute force attempts, and privilege escalation. Rules are assigned severity levels; you configure which severity triggers an active response.

Active response. When a rule fires above a configured threshold, Wazuh can execute a script. The default active response for SSH brute force is adding the source IP to /etc/hosts.deny or triggering a firewall rule. The response is time-limited — the IP is blocked for a configurable window, not permanently, because permanent blocks from automated systems accumulate garbage.

The manager runs on a dedicated small VPS. The agents are typically around 50 MB on disk and under 50 MB RAM each during normal operation. The manager, with the dashboard and indexer running, is heavier — expect 1–2 GB RAM. It’s not a tool you run on the same machine as your application if that machine is small.

CrowdSec: the layer Wazuh doesn’t cover

Wazuh is reactive — it sees what’s happening on your machine and responds. CrowdSec adds something different: crowd-sourced threat intelligence.

When an attacker scans my SSH port from IP 198.51.100.x, CrowdSec’s local agent detects it, generates a decision (block the IP), and reports the attacking IP to the CrowdSec community. Any other CrowdSec instance that pulls the community blocklist will receive that IP and block it preemptively, before that attacker has even reached their server.

The practical result is that a meaningful fraction of incoming attacks are blocked before the first packet hits your application, because someone else already saw that IP attacking their infrastructure. The community blocklist updates continuously and the local agent pulls fresh data on a configurable interval.

CrowdSec’s local architecture has two components: the agent (reads logs, detects attacks, generates decisions) and the bouncer (enforces those decisions at the appropriate layer). For a server with Caddy as the reverse proxy, the relevant bouncer is the Caddy plugin, which reads the local API decision list and returns 403 for blocked IPs directly at the proxy layer — before the request reaches the backend. There’s also a firewall bouncer that drops packets at the kernel level using nftables or iptables, which is faster and suitable for anything that isn’t HTTP.

You can run both simultaneously. I use the firewall bouncer for everything and the Caddy bouncer for HTTP endpoints specifically, because the Caddy bouncer can also serve a custom 403 page rather than dropping the connection silently.

Where they overlap and where they don’t

Both tools detect brute force attacks. Both can block source IPs. The overlap is intentional — defence in depth means multiple independent systems seeing the same attack.

Where they diverge:

Wazuh sees your internal logs in detail. CrowdSec works from structured log signatures. Wazuh can alert on a sudo command that ran at 3 AM by a user who’s never used sudo before. CrowdSec doesn’t have that context.

CrowdSec has the community feed. Wazuh has no equivalent. An IP that’s been scanning the internet for exposed Redis instances for the past week will arrive in your blocklist before it reaches your server, because someone else’s CrowdSec instance already caught it.

Wazuh’s file integrity monitoring has no equivalent in CrowdSec. If someone gains access and modifies a binary, Wazuh tells you. CrowdSec has no insight into the filesystem.

The systems complement each other. Running just one leaves a gap.

Adding Suricata for network-level visibility

Suricata is a network IDS — it analyses traffic rather than logs. On a VPS with a public interface, you can run Suricata in IDS mode (detection only, not inline prevention) by pointing it at the main interface. It runs Emerging Threats signatures against every packet and logs detections to EVE JSON format.

Wazuh has native support for Suricata EVE logs. The Wazuh manager knows how to parse them and will raise alerts when Suricata fires on something interesting. This gets you network-layer detections (port scans, known exploit payloads, C2 callback patterns) correlated with host-level log analysis in the same dashboard.

Suricata in pure IDS mode adds about 50–100 MB RAM on a lightly-loaded VPS. The CPU overhead depends on traffic volume — for a server handling web traffic and SSH only, it’s negligible.

Alert routing and noise reduction

The most important thing nobody tells you about running a SIEM: if you route every alert to a notification channel, you’ll turn off the notifications within a week because the volume is unbearable.

Rule severity is your first filter. Wazuh rules are scored 1–15. Level 7+ is a significant event (authentication failure cluster, privilege escalation attempt). Level 12+ is critical. Route 12+ to immediate notification — PagerDuty, Slack, whatever you’re already watching. Route 7–11 to a daily digest. Route below 7 to storage only; you’ll look at it when something higher fires and you want context.

CrowdSec’s metrics are better reviewed through its local web dashboard (cscli dashboard) or the Grafana integration than as individual alerts. What you want to see is: how many decisions are being made per day, what scenarios are firing most often, and whether the community blocklist coverage is growing or shrinking. Those are trend indicators, not real-time alerts.

The operational reality

The full stack — Wazuh agent, CrowdSec agent + bouncers, Suricata — runs in about 300 MB of RAM on a monitored host. The Wazuh manager needs more on its own server. For a solo developer running a handful of services, the cost is reasonable.

What it buys you is not invulnerability. It buys you visibility. You’ll see the brute force attempts that happen every few hours on any internet-facing SSH port. You’ll see the automated scanners probing for exposed admin panels. You’ll know immediately when a file you didn’t touch gets modified. And the CrowdSec feed means a proportion of those attackers never get through the door at all.

The setup is not plug-and-play — Wazuh’s configuration is YAML and XML rule files, CrowdSec needs its bouncers configured for your specific services, and Suricata’s interface configuration is easy to get wrong. Allow a day to get it working correctly and another few days to tune the alert thresholds so the noise level is manageable. It’s worth it.

Relevant documentation: Wazuh, CrowdSec, Suricata.

← All articles