free exercise · worked answer · real lab output
Write a Wazuh custom rule that alerts when a new user account can log in
A small rule that sits under Wazuh's rule 5902 and raises level 12 when a new account can log in, tested line by line with wazuh-logtest 4.14.10.
The scenario
Your Wazuh manager raises rule 5902, "New user added to the system", at level 8 every time an account is created on a Linux server. Most of these are service accounts that cannot log in, made by the administrators on purpose. The team has stopped reading them, which is exactly how an attacker's own account would slip past.
Here is a real one from our cloud lab, made by the lab's administrator script for a backup job:
Nov 13 02:09:50 linux-server-01 useradd[6249]: new user: name=backup-svc, UID=1002, GID=1002, home=/home/backup-svc, shell=/usr/sbin/nologin, from=none
Your task: write a Wazuh custom rule that raises the alert to level 12 only when the new account can log in, that is, when its shell is not /usr/sbin/nologin or /bin/false. Leave service accounts at level 8. Then show that it works with wazuh-logtest.
Words this exercise uses
- Rule
- An XML block that tells Wazuh which log lines to alert on and at which level, from 0 to 15.
- Child rule
- A rule that Wazuh checks only after another rule, its parent, has matched the same line. You write it with <if_sid>.
- Decoder
- The part of Wazuh that pulls named fields, such as the user or the shell, out of a raw log line.
- Shell
- The program a user gets after logging in, such as /bin/bash. /usr/sbin/nologin and /bin/false mean the account cannot get an interactive shell.
Show a hint
Do not copy rule 5902 and edit it. Write a child rule: <if_sid>5902</if_sid> means your rule is only checked when 5902 has already matched, so you only add the one condition you care about. A Wazuh rule has this shape:
<group name="local,">
<rule id="100100" level="12">
<if_sid>PARENT RULE ID</if_sid>
<field name="FIELD">PATTERN</field>
<description>What the alert says</description>
</rule>
</group>Before you write the condition, run the line through wazuh-logtest, a tool on the manager that shows what Wazuh does with one log line, and look at its phase 2. It lists the fields the decoder pulled out of the line, exactly as your rule will see them.
Show the worked answer
Step 1. Read the parent rule
This is rule 5902 as Wazuh 4.14.10 ships it, in 0020-syslog_rules.xml:
<rule id="5902" level="8">
<match>^new user|^new account added</match>
<description>New user added to the system.</description>
<mitre>
<id>T1136</id>
</mitre>
<group>pci_dss_10.2.7,pci_dss_10.2.5,pci_dss_8.1.2,...</group>
</rule>From the wazuh/wazuh-manager:4.14.10 image, 13 November 2026 (the long compliance group list shortened).
It matches any line that starts with "new user", whatever the shell. That is why it fires for service accounts too. Your rule will sit underneath it.
Step 2. See what the decoder gives you
We pasted the real line into wazuh-logtest. Phase 2 lists the decoded fields:
**Phase 2: Completed decoding. name: 'useradd' parent: 'useradd' dstuser: 'backup-svc' gid: '1002' home: '/home/backup-svc' shell: '/usr/sbin/nologin,' uid: '1002'
wazuh-logtest v4.14.10, 13 November 2026, the real backup-svc line.
The useradd decoder pulls out a shell field, so you can test the field itself instead of searching the whole line. Notice the trailing comma: the decoded value is /usr/sbin/nologin,. A pattern that expects the value to end right after nologin would never match. This is the most common reason a new Wazuh rule silently does nothing.
Step 3. Write the rule
Put it in /var/ossec/etc/rules/local_rules.xml on the manager, the central Wazuh server that applies the rules. Wazuh's documentation recommends IDs between 100000 and 120000 for custom rules, so they never clash with its own.
<group name="local,syslog,adduser,">
<!-- A new account that can log in: useradd gave it a shell that is not nologin or false. -->
<rule id="100100" level="12">
<if_sid>5902</if_sid>
<field name="shell" type="pcre2" negate="yes">^/(usr/)?s?bin/(nologin|false),?$</field>
<description>New user $(dstuser) was created with a shell that can log in.</description>
<mitre>
<id>T1136.001</id>
</mitre>
<group>account_created,</group>
</rule>
</group>Our rule, as tested.
| Line | What it does |
|---|---|
id="100100" level="12" | A custom ID in the recommended range, and the level you want for an account that can log in. |
<if_sid>5902</if_sid> | Only check this rule when 5902 matched, so the line is already known to be a new user. It does not copy 5902's compliance tags (pci_dss, hipaa and the rest); add the ones your reports need to the group line. |
<field name="shell" ... negate="yes"> | Look at the decoded shell field and match when the pattern does NOT fit, that is, when the shell is not one of the no-login shells. |
^/(usr/)?s?bin/(nologin|false),?$ | Read it piece by piece: start of the value (^), a slash, an optional usr/, an optional s, then bin/, then nologin or false, an optional trailing comma, and the end of the value ($). So it covers /bin, /sbin, /usr/bin and /usr/sbin. type="pcre2" tells Wazuh to use the PCRE2 regular expression engine for it. |
$(dstuser) | Puts the new account's name, a decoded field, into the alert's description. |
<id>T1136.001</id> | The specific ATT&CK sub-technique, Create Account: Local Account, more precise than the T1136 on 5902. |
Step 4. Test it with wazuh-logtest
wazuh-logtest reads the saved rule files when it starts, so you do not need to restart the manager to test. We fed it four lines. The first is the real one from our lab. The other three are test lines we wrote in the same format, because a rule must be tested on what it should catch as well as on what it should leave alone:
| Line | Decoded shell | Rule that matched | Level |
|---|---|---|---|
| backup-svc (real, from our lab) | /usr/sbin/nologin, | 5902 | 8 |
| support2 (test line) | /bin/bash, | 100100 | 12 |
| svc-mon (test line) | /bin/false, | 5902 | 8 |
| tmpuser (test line) | /bin/sh, | 100100 | 12 |
Here is the full output for the line that should fire:
**Phase 1: Completed pre-decoding. full event: 'Nov 13 02:20:11 linux-server-01 useradd[7001]: new user: name=support2, UID=1003, GID=1003, home=/home/support2, shell=/bin/bash, from=/dev/pts/0' timestamp: 'Nov 13 02:20:11' hostname: 'linux-server-01' program_name: 'useradd' **Phase 2: Completed decoding. name: 'useradd' parent: 'useradd' dstuser: 'support2' gid: '1003' home: '/home/support2' shell: '/bin/bash,' uid: '1003' **Phase 3: Completed filtering (rules). id: '100100' level: '12' description: 'New user support2 was created with a shell that can log in.' groups: '['local', 'syslog', 'adduser', 'account_created']' firedtimes: '1' mail: 'True' mitre.id: '['T1136.001']' mitre.tactic: '['Persistence']' mitre.technique: '['Local Account']' **Alert to be generated.
wazuh-logtest v4.14.10 in a wazuh/wazuh-manager:4.14.10 container, 13 November 2026, 07:04 UTC.
The service accounts stay at level 8 under 5902; the accounts that can log in now arrive at level 12 under 100100, tagged T1136.001. Restart the manager (systemctl restart wazuh-manager) so live alerts use the rule.
Step 5. Know its limits
- It only sees accounts made with
useraddthat reach the log. An attacker who edits/etc/passwddirectly needs a file-integrity check instead. - A no-login shell in an unusual place, such as
/usr/local/sbin/nologin, is not in the pattern and would raise level 12. That is the safe direction to be wrong in. - An attacker can create an account with nologin and give it a real shell a minute later with
usermod -s /bin/bashorchsh. That change does not pass through rule 5902, so check whether your ruleset already alerts on usermod before you rely on this rule alone. - Level 12 sends an email if email alerts are on (logtest shows
mail: 'True'), so agree on the level with your team first.
The answer. A child rule of 5902 in local_rules.xml, ID 100100, level 12, with <field name="shell" type="pcre2" negate="yes">^/(usr/)?s?bin/(nologin|false),?$</field>. Tested with wazuh-logtest: service accounts stay at level 8, and accounts with a login shell rise to 12.
To see this kind of alert inside a whole shift, try the alert triage exercise. Writing, testing and tuning rules on your own lab is Part 2 of the course.
Questions people ask
Where do I put a Wazuh custom rule?
In /var/ossec/etc/rules/local_rules.xml on the Wazuh manager, or in a new file in /var/ossec/etc/rules/ for larger changes. Restart the manager for live alerts. wazuh-logtest picks up a saved rule file without a restart.
What rule ID should a custom Wazuh rule use?
Wazuh recommends IDs between 100000 and 120000 for custom rules, so they never clash with the rules Wazuh ships.
How do I test a Wazuh rule?
Run /var/ossec/bin/wazuh-logtest on the manager and paste a log line. It prints the decoded fields and the rule that matched, with its level and description, and says whether an alert would be generated.
What does if_sid do in a Wazuh rule?
It makes the rule a child of another rule. The child is only checked when the parent rule matched, so it can add a condition and a new level without rewriting the parent's logic. It does not copy the parent's compliance tags, such as pci_dss, so add the ones you need to the child's own group list.
Why did my field match fail on a value that looks right?
Check the decoded value in wazuh-logtest's phase 2. In our run the useradd decoder kept the trailing comma, so the shell field was /usr/sbin/nologin, with a comma at the end. Our pattern allows for it.
Where every number comes from
- Wazuh documentation, Custom rules: local_rules.xml, IDs 100000 to 120000, testing with wazuh-logtest, read 13 Nov 2026
- Wazuh documentation, Rules syntax (if_sid, field, negate, type="pcre2")
- Wazuh 4.14.10 bundled ruleset: rule 5902 in 0020-syslog_rules.xml, rule 40112 in 0280-attack_rules.xml, read from the wazuh/wazuh-manager:4.14.10 image on 13 Nov 2026
- Our wazuh-logtest run in a wazuh/wazuh-manager:4.14.10 container, 13 Nov 2026 07:04 UTC
- Our AI SOC Analyst cloud lab on AWS, 13 Nov 2026: Wazuh 4.14.10, linux-server-01, admin-pc at 10.20.1.50, the attacker at 10.20.2.66; 41 alerts pulled with pull_alerts.py
- MITRE ATT&CK T1136.001 Create Account: Local Account (modified 4 Nov 2026)
This exercise comes from the course AI SOC Analyst, where each step is done on a real Wazuh lab and explained line by line. More exercises like it: Triaging five Wazuh alerts; Mapping log lines to MITRE ATT&CK. Or see every free exercise.