6.2 KiB
Wazuh Custom Rules and Decoders
Custom Wazuh detection content for the wittenberger.us homelab. This folder contains the working local_rules.xml and local_decoder.xml that run on the production Wazuh manager, along with documentation for each.
Files in this folder
| File | Purpose |
|---|---|
local_rules.xml |
All custom correlation and suppression rules |
local_decoder.xml |
Custom decoders for services shipped decoders don't handle |
README.md |
This file. Overview and deployment procedure |
DECODERS.md |
Detailed reference for every custom decoder |
RULES.md |
Detailed reference for every custom rule, grouped by service |
Rule ID range allocation
Rules are numbered in service-specific ranges to prevent collisions when adding new content:
| Range | Service | Notes |
|---|---|---|
| 100000-100099 | SSH | Currently only one example rule |
| 100100-100199 | Attack / reputation lookups | AlienVault IP blacklist correlation |
| 100200-100299 | Dovecot | Mailcow noise suppression and recon detection |
| 100300-100399 | Postfix / mail correlation | Brute force, compromise, credential stuffing |
| 100400-100499 | Gitea | Auth, account lifecycle, SSH keys, tokens |
| 100500-100599 | pfSense | Currently unused (shipped rules cover baseline) |
Rules 87699, 87761, and 87762 are also custom despite being below 100000. These override shipped IDs to enable pfSense wrapped-log correlation.
Deployment procedure
Deploy any change from this folder to the running manager:
sudo cp local_decoder.xml /var/ossec/etc/decoders/
sudo cp local_rules.xml /var/ossec/etc/rules/
sudo /var/ossec/bin/wazuh-analysisd -t
# Verify no errors before restarting
sudo systemctl restart wazuh-manager
sudo tail -20 /var/ossec/logs/ossec.log
# Verify clean startup
If validation fails, do not restart. Fix the XML in this folder, re-copy, re-validate.
Testing procedure
Before deploying, test any new rule against real logs using wazuh-logtest:
sudo /var/ossec/bin/wazuh-logtest
Paste a real log line at the prompt. Verify:
- Phase 1 extracts
program_namecorrectly - Phase 2 shows the expected decoder and all captured fields
- Phase 3 shows the intended rule firing at the expected level
Test with both malicious and benign log samples. A rule that fires on the malicious case but ALSO fires on legitimate traffic is a false positive generator and must be tuned before deploy.
Custom decoders overview
Two decoders exist because mailcow's Docker log wrapping breaks shipped decoders:
mailcow-postfix-sasl-fail— extracts srcip, src_subnet, and dstuser from wrapped Postfix SASL failuresmailcow-dovecot-success— extracts srcip from any Dovecot event containingrip=IP,
Additional decoders handle other homelab services:
giteaandgitea-auth-fail— parse Gitea log format for auth eventspfsense-wrappedand children — parse pfSense filter logs
See DECODERS.md for exact patterns and field extraction details.
Sync workflow
Working state on the manager should always match this repo. Recommended workflow:
Edit in repo first, deploy from repo:
# Edit the rules/decoders in this folder
nano local_rules.xml
# Deploy to manager
sudo cp local_rules.xml /var/ossec/etc/rules/
sudo /var/ossec/bin/wazuh-analysisd -t
sudo systemctl restart wazuh-manager
# Commit
git add local_rules.xml
git commit -m "Add rule 100XXX: description"
git push
If you edited on the manager first, copy back:
sudo cp /var/ossec/etc/rules/local_rules.xml ./
sudo cp /var/ossec/etc/decoders/local_decoder.xml ./
git add local_rules.xml local_decoder.xml
git commit -m "Sync working manager state"
git push
Never edit both places simultaneously; you'll lose changes.
Known suppression rules
Several rules exist purely to reduce alert noise from expected internal traffic:
| Rule ID | Suppresses |
|---|---|
| 100200 | mailcow watchdog auth attempts against watchdog@invalid |
| 100201 | Dovecot disconnects from Docker-internal range 172.22.1.x |
| 100202 | Chris's own mailbox routine disconnects |
| 100203 | mailcow watchdog managesieve health checks |
| 100204 | Successful Dovecot logins (needed for correlation, level 0 hides from alerts) |
| 100205 | Single Dovecot connections with no auth attempt (port scan primitives) |
Rule 100204 is special. It intentionally suppresses ALL successful Dovecot logins from alerting because rule 100311 (compromise correlation) needs 100204 to fire in order to correlate. Without the suppression, every login would alert; with it, only the correlated compromise fires.
Adversary intel
Real credential-stuffing campaign observed against wittenberger.us mailcow:
- Pattern: Distributed brute force against SMTP submission (587)
- Timing: Bursts every ~3.5 hours, each lasting 6-10 seconds
- Source IPs: Rotate every burst, no persistent single-source pattern
- Target usernames: Common admin defaults (admin, info, contact, postmaster, demo)
- Detection rules that fire: 100315 (credential stuffing), 100316 (volume)
- Rules that don't fire: 100310, 100313 (single-source rules) - because IPs rotate
Rules 100312-100316 were built specifically to catch this pattern after 100310 alone proved insufficient.
Lessons applicable to CyberForce 2026
Building and tuning this ruleset over July 2026 produced several transferable lessons for the JJC Cyberwolves training curriculum. See Cyberforce_2026/Cyberforce/Wazuh/ for training materials that reference this configuration as a case study.
Key takeaways:
- Shipped decoders often fail on containerized services. mailcow's double-wrapped log format broke both shipped Postfix and Dovecot decoders. Custom decoders were necessary.
same_srcipalone is not enough. Modern brute force uses rotating IPs. Correlate onsame_user, subnet, and volume too.- Always test with real logs. Injected logs miss subtle formatting differences that break decoders.
- XML entities matter.
<must be<,>must be>, both with trailing semicolons. - PCRE2 vs OSSEC regex is not interchangeable. OSSEC lacks
.*,?,{n,m}, and negation classes. Use PCRE2 explicitly for complex patterns.
Last updated: 2026-07-31