Wazuh Active Response Silent Failure on RHEL/firewalld - Diagnostic Guide

12 views
Skip to first unread message

Chris W

unread,
May 6, 2026, 4:20:13 PM (4 days ago) May 6
to Wazuh | Mailing List
I spent a few days with Gemini and Claude working on this.  Gemini couldn't figure it out, but Claude did.  I had Claude write up a "how to" based on our many many lines of discussion on how to fix this... I didn't proofread it very carefully so use at your own risk... I hope it is helpful to someone!!


Wazuh Active Response Silent Failure on RHEL/firewalld — Complete Diagnostic Guide

Environment: RHEL 8/9 · Wazuh Manager + Agent architecture · firewalld (iptables not in use)


The Problem

After configuring firewall-drop Active Response for SSH brute force mitigation, the AR appeared to do nothing. Configuration sync was verified — the XML block existed on the agent. No errors appeared in ossec.log. active-responses.log was completely silent. The agent was running. Everything looked correct, and nothing worked.

It turned out there were four stacked failure layers, each one hiding the next. This post documents every layer, how to confirm it, and how to fix it — so you can stop at whichever layer is your actual problem.


Layer 1: Wrong Executable Name in ossec.conf The Symptom

Active response silently does nothing. No log entries anywhere.

The Cause

Wazuh ships two different scripts for firewall blocking:


Script
Method
firewall-drop
Legacy iptables
firewalld-drop.sh
Modern firewalld

On RHEL 8/9 with firewalld, the default ossec.conf command block may reference the wrong one:

<!-- Wrong — iptables based --> <command> <name>firewall-drop</name> <executable>firewall-drop</executable> <timeout_allowed>yes</timeout_allowed> </command> The Fix <!-- Correct — firewalld based --> <command> <name>firewall-drop</name> <executable>firewalld-drop.sh</executable> <timeout_allowed>yes</timeout_allowed> </command> How to Confirm grep -A 3 "firewall-drop" /var/ossec/etc/ossec.conf
Layer 2: AR Block Only in agent.conf, Not ossec.conf The Symptom

Config syncs correctly to the agent. The AR block is visible on the agent at /var/ossec/etc/shared/agent.conf. Still nothing fires.

The Cause

This is the most confusing part of the Wazuh AR architecture:

  • The <command> block in agent.conf tells the agent how to execute the script
  • The <active-response> block in agent.conf is not read by the Manager for dispatch decisions
  • The Manager only dispatches AR based on what is defined in its own ossec.conf

If your ossec.conf on the Manager has no <active-response> block for your rules, or has the wrong rule IDs, the Manager will never send the dispatch — regardless of what agent.conf says.

The Fix

The <active-response> block must exist in /var/ossec/etc/ossec.conf on the Manager:

<active-response> <disabled>no</disabled> <command>firewall-drop</command> <location>local</location> <rules_id>5710,5712,5760,2502</rules_id> <timeout>600</timeout> </active-response> How to Confirm grep -A 8 "<active-response>" /var/ossec/etc/ossec.conf
Layer 3: Wrong Rule IDs The Symptom

The AR block exists in ossec.conf with the correct command, but still no dispatch.

The Cause

The Owl's instinct: "Are you sure those Rule IDs are the ones being hit?"

On RHEL 8/9, SSH brute force against existing users (like root) fires different rules than brute force against non-existent users:


Rule
Description
Typical Scenario
5710
Attempt to login using a non-existent user
Random username scanning
5712
Brute force: multiple auth failures, non-existent user
Sustained scan, invalid users
5760
sshd: authentication failed
Brute force against root or valid users
2502
User missed the password more than one time
PAM repeated failures

If your attackers are targeting root, you'll see 5760 in your alerts — not 5710 or 5712. A config block listing only 5710,5712 will sit there and do nothing.

How to Confirm

Check what rules are actually firing during an attack:

tail -f /var/ossec/logs/alerts/alerts.log | grep "Rule:" The Fix

Add the rules you're actually seeing:

<rules_id>5710,5712,5760,2502</rules_id>
Layer 4: Script Input Format Mismatch (The Silent Killer) The Symptom

With analysisd.debug=2 enabled on the Manager you can see:

DEBUG: Send AR to execd: firewall-drop600

With execd.debug=2 on the agent you can see the command being executed. But nothing happens. active-responses.log stays empty or shows:

/var/ossec/active-response/bin/firewall-drop: Invalid input format The Cause

This is the most dangerous failure mode — it is completely silent without debug logging.

Modern Wazuh (4.x+) passes alert data to AR scripts as a JSON blob via stdin:

firewalld-drop.sh {"version":1,"origin":{"name":"node01","module":"wazuh-execd"},"command":"add","parameters":{..."data":{"srcip":"1.2.3.4"...}}}

Older or custom scripts expect positional arguments:

ACTION=$1 # expects "add" IP=$3 # expects the IP — but $3 is empty

The script runs, gets an empty IP, passes nothing to firewall-cmd, and exits silently.

How to Confirm

Enable debug on the agent and watch what execd actually sends:

echo "execd.debug=2" >> /var/ossec/etc/local_internal_options.conf systemctl restart wazuh-agent tail -f /var/ossec/logs/ossec.log | grep -i "executing command"

You'll see the full JSON payload being passed as $1.

The Fix

Rewrite the script to read JSON from stdin:

#!/bin/bash # Wazuh active response script for firewalld # Compatible with Wazuh 4.x JSON input format # Includes: timeout support, startup flush, logging read -r INPUT_JSON COMMAND=$(echo "$INPUT_JSON" | grep -o '"command":"[^"]*"' | sed 's/"command":"//;s/"//') SRC_IP=$(echo "$INPUT_JSON" | grep -o '"srcip":"[^"]*"' | head -1 | sed 's/"srcip":"//;s/"//') LOG=/var/ossec/logs/active-responses.log LOCKFILE=/var/ossec/tmp/firewalld-drop.lock # Flush all old reject rules on first execution after agent restart # /var/ossec/tmp/ is cleared on agent restart, so lockfile auto-rearms if [ ! -f "$LOCKFILE" ]; then echo "$(date '+%Y/%m/%d %H:%M:%S') firewalld-drop: First run after startup - flushing old reject rules" >> "$LOG" firewall-cmd --list-rich-rules | grep "reject" | while read -r RULE; do firewall-cmd --permanent --remove-rich-rule="$RULE" echo "$(date '+%Y/%m/%d %H:%M:%S') firewalld-drop: Flushed: $RULE" >> "$LOG" done firewall-cmd --reload echo "$(date '+%Y/%m/%d %H:%M:%S') firewalld-drop: Flush complete" >> "$LOG" touch "$LOCKFILE" fi if [ -z "$SRC_IP" ]; then echo "$(date '+%Y/%m/%d %H:%M:%S') firewalld-drop: ERROR - No source IP in message" >> "$LOG" exit 1 fi if [ "$COMMAND" = "add" ]; then firewall-cmd --permanent --add-rich-rule="rule family='ipv4' source address='$SRC_IP' reject" firewall-cmd --reload echo "$(date '+%Y/%m/%d %H:%M:%S') firewalld-drop: Blocked $SRC_IP" >> "$LOG" # Return check_keys response so execd registers the timeout # Without this, execd never adds the block to its timeout list # and the <timeout> value in ossec.conf has no effect echo '{"version":1,"origin":{"name":"firewalld-drop","module":"active-response"},"command":"check_keys","parameters":{"keys":["'"$SRC_IP"'"]}}' elif [ "$COMMAND" = "delete" ]; then firewall-cmd --permanent --remove-rich-rule="rule family='ipv4' source address='$SRC_IP' reject" RESULT=$? firewall-cmd --reload echo "$(date '+%Y/%m/%d %H:%M:%S') firewalld-drop: Unblocked $SRC_IP (exit code: $RESULT)" >> "$LOG" fi

Deploy to all agents:

chmod 750 /var/ossec/active-response/bin/firewalld-drop.sh chown root:wazuh /var/ossec/active-response/bin/firewalld-drop.sh

Note on the check_keys response: This is critical for timeout to work. Without it, execd logs:

Active response won't be added to timeout list. Message not received with alert keys from script

...and the <timeout> value in your config is completely ignored. Blocks become permanent.


Complete Working Configuration /var/ossec/etc/ossec.conf on Manager (relevant sections) <command> <name>firewall-drop</name> <executable>firewalld-drop.sh</executable> <timeout_allowed>yes</timeout_allowed> </command> <active-response> <disabled>no</disabled> <command>firewall-drop</command> <location>local</location> <rules_id>5710,5712,5760,2502</rules_id> <timeout>600</timeout> </active-response> /var/ossec/etc/shared/default/agent.conf on Manager <agent_config> <command> <name>firewall-drop</name> <executable>firewalld-drop.sh</executable> <timeout_allowed>yes</timeout_allowed> </command> <active-response> <command>firewall-drop</command> <location>local</location> <rules_id>5710,5712,5760,2502</rules_id> <timeout>600</timeout> </active-response> </agent_config>
Diagnostic Decision Tree Attack Simulated │ ▼ Alert in alerts.log? ├── NO → Wazuh isn't seeing the event. Check decoder/log source. └── YES → Rule ID matches your rules_id list? ├── NO → Add the correct Rule IDs to ossec.conf AR block. └── YES → "Send AR to execd" in Manager ossec.log? ├── NO → AR block missing/wrong in ossec.conf. └── YES → "Executing command" in agent ossec.log? ├── NO → Dispatch not reaching agent. └── YES → Entry in active-responses.log? ├── NO → Script format mismatch. └── YES → Firewall rule added? ├── NO → Script logic error. └── YES → Working!
How to Enable Debug Logging

Manager — analysisd:

echo "analysisd.debug=2" >> /var/ossec/etc/local_internal_options.conf systemctl restart wazuh-manager tail -f /var/ossec/logs/ossec.log | grep -i "active\|sending\|dispatch"

Agent — execd:

echo "execd.debug=2" >> /var/ossec/etc/local_internal_options.conf systemctl restart wazuh-agent tail -f /var/ossec/logs/ossec.log | grep -i "active\|execd\|firewall"

Clean up when done:

sed -i '/analysisd.debug=2/d' /var/ossec/etc/local_internal_options.conf sed -i '/execd.debug=2/d' /var/ossec/etc/local_internal_options.conf systemctl restart wazuh-manager # or wazuh-agent on the agent
Key Takeaways
  1. agent.conf configures how the agent executes AR scripts. It does not control what the Manager dispatches.
  2. The Manager dispatches AR exclusively based on ossec.conf on the Manager node.
  3. On RHEL 8/9, attacks against root fire rule 5760, not 5710/5712. Check your actual alert Rule IDs before assuming the config is wrong.
  4. Modern Wazuh (4.x+) passes alert data to AR scripts as JSON via stdin, not positional arguments. Old scripts will fail silently.
  5. The check_keys JSON response from the script is required for the <timeout> value to take effect. Without it, all blocks are permanent.

Tested on: RHEL 9 · Wazuh 4.x · firewalld · Agent-Manager architecture

Olamilekan Abdullateef Ajani

unread,
May 6, 2026, 4:26:01 PM (4 days ago) May 6
to Wazuh | Mailing List
Hello Chris,

Great investigation and thank you for sharing your findings and recommendations.

Regards,

Reply all
Reply to author
Forward
0 new messages