Back to blog

Integrating Wazuh into My Network

Deployed Wazuh, added an agent easily, then spent a night discovering that getting my OpenBSD router's logs into it required a homemade syslog bridge.

Post

Getting It Running

Deployed via Docker Compose, I setup the manager, indexer, and dashboard as three separate containers. First real snag was the dashboard failing to reach the manager's API, ECONNREFUSED on port 55000, buried in the dashboard container's own logs rather than anywhere obviously related to what I'd actually configured. That turned out to be unrelated to anything I'd touched, just the containers coming up in an order where the dashboard tried to talk to the manager before it was fully ready.

I then changed the default passwords immediately after confirming the stack was actually healthy.

My web server was the first agent, straightforward, well-documented, worked immediately. Then I wanted my OpenBSD router's logs in there too, and that's where things got interesting.

The First Attempt

OpenBSD isn't in Wazuh's list of officially supported agent platforms, so the obvious path was syslog forwarding straight to the manager. I added a <remote> block to ossec.conf, pointed my router at it, and nothing arrived.

The first problem was caused by me: I'd edited the running container's config directly, not realizing the Wazuh Docker image regenerates ossec.conf from a host-side template file on every start. My edits kept vanishing on restart, not because they were wrong, but because I was editing the wrong file entirely. Found the actual template under config/wazuh_cluster/wazuh_manager.conf and moved my changes there instead.

I cast a wide net and forwarded everything with *.* in syslog.conf just to rule out a facility/priority scoping issue, and that's when I learned the hard way that flooding the manager with unscoped log volume can genuinely strain it. Long story short, it crashed almost immediately. I walked that back real quick.

pf Logs Went Nowhere

Here's the thing about pf: the log keyword in a pf rule doesn't write to syslog. It writes to /var/log/pflog, in binary pcap format, the same format tcpdump reads for network captures. I'd been troubleshooting a syslog forwarding problem that was never going to work, because pf was never sending anything through syslog to begin with.

Once I knew that, the fix was a bridge, not a config change. A small script reading the live pflog0 interface with tcpdump, piping each line through logger to inject it into the normal syslog stream:

tcpdump -n -e -ttt -l -i pflog0 2>/dev/null | while read -r line; do
    logger -p local0.info -t pflog "$line"
done

Genuinely hacky. Also genuinely the only reasonable way to get packet-level firewall events into a text-based logging pipeline that was never designed to carry them.

The Real Fix Was a Different Architecture Entirely

Rather than keep fighting direct syslog-to-manager forwarding, I built a dedicated syslog collector instead, a small Debian LXC running rsyslog, receiving forwarded logs from the router, with the Wazuh agent installed directly on the collector reading those files locally. Turns out this is Wazuh's own documented pattern for exactly this situation, devices that can't run an agent themselves. I hadn't been improvising a workaround. I'd just found the actual intended path the slow way.

Setting up the collector taught me rsyslog has the same kind of rule-ordering sensitivity. I separated remote logs into their own directory structure with a template and & stop, placed it too early in the config, and quietly broke the collector's own local logging in the process, every message hit my rule and stopped there before ever reaching the defaults. Moved the block to the end of the file. Both local and remote logging worked side by side after that.

Turning a Shell Pipeline into a Real Service

Getting pf-logger.sh running as a proper background service was its own small saga. I tried a PID file first, echo $$ at the top of the script, the same pattern that worked cleanly for a Python service weeks earlier. It didn't work here, and the reason was interesting: the script's pipeline spawns multiple related shell processes, one for each side of the pipe, and $$ doesn't reliably end up pointing at the one still doing real work. kill would run successfully against the PID in the file and nothing would actually stop.

I chased that for a while, su -l turned out to be adding unnecessary process indirection on top of an already-root service, contributing to duplicate instances every restart. Removed it. Still had the wrong-PID problem. Eventually gave up on tracking a specific PID at all and used pgrep -f/pkill -f directly in the rc.d script's rc_check and rc_stop functions instead, matching on the full command line rather than trying to capture and remember one specific number. Simpler, and it actually worked.

Along the way I also managed to swap the contents of rc_check and rc_stop by accident, meaning running rcctl check would silently kill the service instead of just checking on it. Worth reading your own function names back to yourself before trusting them.

Data Arriving Isn't the Same as Data Visible

Once the pipeline was fully wired up, I still couldn't find the pf logs in the dashboard. Confirmed with the manager's raw archive log that the data was genuinely arriving, it was. Just not showing up in Discover under wazuh-archives-*.

Turns out archives.log, the manager's own local file, and wazuh-archives-*, the indexed, searchable version in the dashboard, are two different things entirely. Data reaching the manager doesn't automatically get indexed, that's a separate setting, logall and logall_json, off by default. Flipped them on.

Even then, the pf logs still didn't generate anything under Security Events, while a fake SSH login attempt did, immediately. That part turned out to be correct, expected behavior rather than another bug. Wazuh's default ruleset knows what an sshd auth failure looks like. It has no idea what to do with raw tcpdump output describing a blocked pf rule, because nothing exists yet to decode it. Data arriving with no alert isn't broken, it's just data with nowhere to go yet. Writing decoders and rules for that is its own project, for another day.

What Actually Ended Up Working

I ended up deploying a centralized syslog collector in an LXC. I properly separated local and remote logging, set up log rotation, and an rc.d service that starts and stops cleanly. I then installed the Wazuh agent on that LXC and set it up to monitor those syslog files. Most of tonight wasn't wasted effort on dead ends, even the parts that didn't work taught me something I needed for the part that did.