Wazuh’s built-in rule set is broad but conservative. It covers common attack patterns, but it can’t know the specifics of your environment — which users are admins, which services are expected, or which network ranges are internal. That gap is where custom rules live.
This is a curated set of ten detections I deploy early in every Wazuh engagement. Each rule targets a specific technique, maps to MITRE ATT&CK, and includes the complete decoder and rule XML. Copy them directly into your local_decoder.xml and local_rules.xml, adjust the patterns for your environment, and you have a meaningful detection baseline in an afternoon.
To test any of these before deploying, paste the decoder, rule, and sample log lines into the Wazuh Rule Builder sandbox.
1. SSH brute force from external IPs
What it detects: Repeated SSH authentication failures from the same source IP — the most common internet-facing attack.
MITRE: T1110 — Brute Force (Credential Access)
Why built-in rules aren’t enough: Wazuh’s default SSH rules fire on individual failures. They don’t distinguish internal service accounts that occasionally mistype a password from an external IP cycling through a credential list.
<decoder name="custom-sshd-fail">
<program_name>sshd</program_name>
<prematch>Failed password</prematch>
<regex>Failed password for (\S+) from (\S+) port</regex>
<order>dstuser,srcip</order>
</decoder><rule id="100001" level="5">
<decoded_as>custom-sshd-fail</decoded_as>
<description>SSH failed password attempt</description>
</rule>
<rule id="100002" level="10">
<if_sid>100001</if_sid>
<frequency>5</frequency>
<timeframe>120</timeframe>
<same_source_ip />
<description>SSH brute force — 5+ failures from same IP in 2 minutes</description>
<mitre>
<id>T1110</id>
<tactic>Credential Access</tactic>
</mitre>
</rule>Tuning note: Adjust the frequency threshold based on your environment. A bastion host with many users may need 10+. A server that only accepts key-based auth should alert on any password attempt.
2. Web shell access in upload directories
What it detects: HTTP requests to PHP, JSP, or ASP files in directories normally used for static uploads (images, uploads, tmp).
MITRE: T1505.003 — Server Software Component: Web Shell (Persistence)
Why it matters: Attackers who achieve initial access through file upload vulnerabilities almost always drop a web shell in the upload directory. The shell blends in with legitimate uploads unless you specifically watch for executable file access in those paths.
<decoder name="custom-apache-access">
<program_name>apache</program_name>
<regex>(\S+) - - "(\S+ \S+)</regex>
<order>srcip,url</order>
</decoder><rule id="100010" level="12">
<decoded_as>custom-apache-access</decoded_as>
<regex>/(uploads|images|tmp|static)/\S+\.(php|jsp|asp|aspx)</regex>
<description>Web shell access — executable file in upload directory</description>
<mitre>
<id>T1505.003</id>
<tactic>Persistence</tactic>
</mitre>
</rule>Tuning note: Add your application’s specific upload paths. If your app legitimately serves PHP from /uploads/, you have a bigger problem than detection rules.
3. Sudo to root shell by non-admin users
What it detects: Users outside the admin group using sudo to spawn an interactive shell (/bin/bash, /bin/sh, /bin/zsh).
MITRE: T1548.003 — Abuse Elevation Control Mechanism: Sudo and Sudo Caching (Privilege Escalation)
Why it matters: Legitimate admin work uses sudo for specific commands — systemctl restart, apt update, tail -f /var/log/syslog. An interactive root shell from a service account or developer user is almost always either misconfigured sudoers or active exploitation.
<decoder name="custom-sudo">
<program_name>sudo</program_name>
<regex>(\S+) : TTY=\S+ ; PWD=(\S+) ; USER=(\S+) ; COMMAND=(.*)</regex>
<order>srcuser,pwd,dstuser,command</order>
</decoder><rule id="100020" level="10">
<decoded_as>custom-sudo</decoded_as>
<regex>COMMAND=(/bin/bash|/bin/sh|/bin/zsh|/usr/bin/bash)</regex>
<description>Sudo to interactive shell</description>
<mitre>
<id>T1548.003</id>
<tactic>Privilege Escalation</tactic>
</mitre>
</rule>Tuning note: Whitelist known admin accounts by adding a negative <match> in a child rule, or use a CDB list of authorized admin usernames.
4. New user account creation
What it detects: useradd or adduser invocations that create new local accounts.
MITRE: T1136.001 — Create Account: Local Account (Persistence)
Why it matters: Attackers create local accounts for persistent access. In well-managed environments, user creation is rare and should go through provisioning workflows — not manual useradd commands.
<decoder name="custom-useradd">
<program_name>useradd</program_name>
<regex>new user: name=(\S+),</regex>
<order>dstuser</order>
</decoder><rule id="100025" level="8">
<decoded_as>custom-useradd</decoded_as>
<description>Local user account created</description>
<mitre>
<id>T1136.001</id>
<tactic>Persistence</tactic>
</mitre>
</rule>Tuning note: If you have automated provisioning that runs useradd, add an exception for the provisioning service account.
5. Crontab modification
What it detects: Changes to any user’s crontab via the crontab command.
MITRE: T1053.003 — Scheduled Task/Job: Cron (Execution, Persistence)
Why it matters: Cron jobs are a classic persistence mechanism. Attackers use them for reverse shells, cryptocurrency miners, and data exfiltration on a schedule. Legitimate crontab changes in production are rare and should be audited.
<decoder name="custom-crontab">
<program_name>crontab</program_name>
<regex>(\S+)\): (.*)</regex>
<order>srcuser,action</order>
</decoder><rule id="100030" level="8">
<decoded_as>custom-crontab</decoded_as>
<match>REPLACE</match>
<description>Crontab modified</description>
<mitre>
<id>T1053.003</id>
<tactic>Persistence</tactic>
</mitre>
</rule>Tuning note: REPLACE is the syslog message cron produces when a crontab is updated. LIST messages can be ignored — they just mean someone ran crontab -l.
6. Log file cleared or truncated
What it detects: Commands that truncate, remove, or overwrite system log files.
MITRE: T1070.002 — Indicator Removal: Clear Linux or Mac System Logs (Defense Evasion)
Why it matters: Attackers clear logs to hide evidence of compromise. This detection catches the most common methods: truncation with >, removal with rm, and shredding with shred.
<rule id="100035" level="12">
<match>COMMAND=</match>
<regex>COMMAND=.*(truncate|> /var/log|rm .*/var/log|shred .*/var/log)</regex>
<description>System log file cleared or truncated</description>
<mitre>
<id>T1070.002</id>
<tactic>Defense Evasion</tactic>
</mitre>
</rule>Tuning note: This rule relies on sudo logging. If an attacker already has root and bypasses sudo, the clearance won’t be logged through this channel. Pair with file integrity monitoring (Wazuh’s syscheck) on /var/log/.
7. Kernel module loaded
What it detects: Kernel module insertion via insmod or modprobe.
MITRE: T1547.006 — Boot or Logon Autostart Execution: Kernel Modules (Persistence, Privilege Escalation)
Why it matters: Rootkits and advanced implants load as kernel modules to achieve the deepest possible persistence. In most production servers, kernel modules should never change outside of maintenance windows.
<rule id="100040" level="10">
<match>COMMAND=</match>
<regex>COMMAND=.*(insmod|modprobe)</regex>
<description>Kernel module loaded via sudo</description>
<mitre>
<id>T1547.006</id>
<tactic>Persistence</tactic>
</mitre>
</rule>Tuning note: Some systems legitimately load modules at boot (e.g., VPN drivers, GPU modules). Correlate with the user account — root cron loading a known module is different from www-data loading an unknown one.
8. Firewall rule modification
What it detects: Changes to iptables or firewall rules via sudo commands.
MITRE: T1562.004 — Impair Defenses: Disable or Modify System Firewall (Defense Evasion)
Why it matters: Attackers modify firewall rules to open ports for reverse shells, C2 traffic, or data exfiltration. Firewall changes in production should be rare and audited.
<rule id="100045" level="10">
<match>COMMAND=</match>
<regex>COMMAND=.*(iptables|nft|firewall-cmd|ufw)</regex>
<description>Firewall rules modified</description>
<mitre>
<id>T1562.004</id>
<tactic>Defense Evasion</tactic>
</mitre>
</rule>Tuning note: If you manage firewall rules through automation (Ansible, Puppet), whitelist the automation service account. Manual iptables commands on a server managed by config management are a red flag regardless of intent.
9. Unauthorized package installation
What it detects: Package manager invocations (apt, yum, dnf, pip) via sudo.
MITRE: T1059 — Command and Scripting Interpreter (Execution)
Why it matters: Attackers install tools post-compromise — nmap for reconnaissance, socat for tunneling, gcc for compiling exploits locally. In hardened environments, package installation should only happen through CI/CD or change management.
<rule id="100050" level="8">
<match>COMMAND=</match>
<regex>COMMAND=.*(apt install|apt-get install|yum install|dnf install|pip install)</regex>
<description>Package installation via sudo</description>
<mitre>
<id>T1059</id>
<tactic>Execution</tactic>
</mitre>
</rule>Tuning note: apt update and apt upgrade are routine maintenance. Only install of specific packages is interesting. If your environment uses a package proxy or mirror, you can also monitor the proxy logs for unusual packages.
10. Shadow file access
What it detects: Any attempt to read /etc/shadow via sudo.
MITRE: T1003.008 — OS Credential Dumping: /etc/passwd and /etc/shadow (Credential Access)
Why it matters: /etc/shadow contains password hashes. Reading it is the first step in offline password cracking. There is almost no legitimate reason for a human to cat /etc/shadow on a production system.
<rule id="100055" level="12">
<match>COMMAND=</match>
<regex>COMMAND=.*(cat|less|more|head|tail|vi|vim|nano) /etc/shadow</regex>
<description>Shadow file accessed via sudo</description>
<mitre>
<id>T1003.008</id>
<tactic>Credential Access</tactic>
</mitre>
</rule>Tuning note: Pair with file integrity monitoring on /etc/shadow to catch access that bypasses sudo (e.g., root shell, exploited service running as root).
Deploying these rules
Save the decoders and rules to your Wazuh manager:
# Add custom decoders
sudo tee -a /var/ossec/etc/decoders/local_decoder.xml < decoders.xml
# Add custom rules
sudo tee -a /var/ossec/etc/rules/local_rules.xml < rules.xml
# Test the configuration
/var/ossec/bin/wazuh-logtest
# Restart the manager
sudo systemctl restart wazuh-managerAfter restarting, generate test events to confirm each rule fires. For SSH rules, use a test SSH login with an incorrect password. For sudo rules, run a test command. Check /var/ossec/logs/alerts/alerts.json for the alerts.
The common theme
Every rule in this list follows the same pattern: detect a specific action that is normal in some contexts and suspicious in others, then tune for your environment. The decoder extracts the fields that matter. The rule applies the context. The tuning note tells you what to adjust.
None of these rules are complicated. The hard part isn’t the XML — it’s knowing which events to watch and which patterns separate routine from threat. Every frequency threshold and severity level is a precision-recall tradeoff — explore that interactively in the Classifier Threshold Lab. Once these rules are live, analysts need to triage the alerts they generate — practice that skill under time pressure in the Alert Triage Simulator.
If you want to practice the mechanics of writing decoders and rules, try the challenges in the Wazuh Rule Builder. If you want to go deeper on the concepts, start with Writing Your First Wazuh Custom Decoder and Rule.