Skip to content
HoginHogin
Go back

Wazuh Active Response: automatically banning, isolating, and responding to incidents (part 4/6)

10 мин чтения

In part 3 we taught Wazuh to see aggressive SSH brute force: rule 100100 fires on the fourth failure within a minute from a single IP and raises a level-12 alert. The problem is what stands next in the chain: a human. At 3 a.m. there are twenty minutes to a whole morning between “detection fired” and “someone blocked the IP at the firewall” — and a brute-forcer needs five. Part 4 is about removing the human from that chain: Active Response, the mechanism built into Wazuh that bans IPs, isolates hosts, and runs arbitrary scripts on rule matches — without an external SOAR.

Table of contents

Open Table of contents

What Active Response is and where it lives

The mechanism was inherited from OSSEC and has barely changed since — the same declarative XML as the rules, and just as long-lived an investment. The idea is simple: every alert has a level, a rule id, and a group, and you keep a list of “if this fired, do that”. There is no separate service: analysisd makes the decision, and execution is delegated to wazuh-execd.

From alert to action without a human

The key nuance of this scheme: the script must live on the host where it executes. A local response executes on the agent that sent the event — so the script, and nftables, must be present on every agent. That is the price of reacting where the attack is happening, not where the server sits.

Built-in commands: firewall-drop and when it’s enough

The package ships with ready-made scripts, and most of the time they are enough:

firewall-drop is the workhorse: for SSH brute force, scanners, and web attacks it is sufficient most of the time. It is enabled by a block in the manager’s ossec.conf, and the script already ships in both the server and the agent package.

When the built-ins fall short: you need an nftables set with your own timeouts, a call to an external API — file a ticket, isolate the host in the EDR, disable the account in the IdP — or a reaction that depends on alert context. Then you write your own script, and it speaks exactly the same stdin/JSON contract as the built-ins.

The anatomy of the setup: command and active-response

The configuration lives in the manager’s ossec.conf and consists of two blocks — “what to run” and “when to run it”:

<command>
  <name>nft-ban</name>
  <executable>nft-ban.sh</executable>
  <timeout_allowed>yes</timeout_allowed>
</command>

<active-response>
  <command>nft-ban</command>
  <location>local</location>
  <rules_id>100100</rules_id>
  <timeout>600</timeout>
</active-response>

Practice: a custom nftables ban with a safety exception

Let’s build the example from the brief: repeated failed authentications → the IP goes into an nftables set, the block lifts itself, and the admin subnet is never banned. No ban registry and no cron cleanup — the timeout is held by the nftables kernel itself.

The nftables base on the agent, /etc/nftables.conf:

table inet wazuh_ar {
  set banned {
    type ipv4_addr
    flags timeout
  }
  chain input {
    type filter hook input priority -10; policy accept;
    ip saddr @banned drop
  }
}

The script goes to /var/ossec/active-response/bin/nft-ban.sh:

#!/usr/bin/env bash
# Active Response: ban srcip in nftables for 10 minutes, never touch admin subnets.
LOG="/var/ossec/logs/active-responses.log"

INPUT=$(cat)
cmd=$(echo "$INPUT"   | jq -r '.command')
srcip=$(echo "$INPUT" | jq -r '.parameters.alert.data.srcip' | grep -E '^[0-9.]+$')

log() { echo "$(date '+%F %T') nft-ban [$cmd] $*" >> "$LOG"; }

[ -n "$srcip" ] || { log "no valid srcip — skip"; exit 0; }

# Safety: never ban admin subnets, even if the rule fired
case "$srcip" in
  192.0.2.*|10.10.*) log "safety: $srcip is admin range — skip"; exit 0;;
esac

case "$cmd" in
  add)
    nft add element inet wazuh_ar banned { "$srcip" timeout 10m } \
      && log "blocked $srcip for 10m" || log "nft add failed for $srcip" ;;
  delete)
    nft delete element inet wazuh_ar banned { "$srcip" } 2>/dev/null \
      && log "unblocked $srcip" ;;
esac

What matters here:

Installation and permissions — on every agent where the script will execute:

install -m 750 -o root -g wazuh nft-ban.sh /var/ossec/active-response/bin/
systemctl restart wazuh-agent

Then the ossec.conf on the manager (the blocks from the previous section) and systemctl restart wazuh-manager.

How not to ban yourself

Auto-blocking is great while it blocks the other guys. Three lines of defense, each saving you in its own scenario:

Three lines of defense against self-banning

About timeouts — two units that are easy to mix up: timeout in ossec.conf counts seconds, while repeated_offenders counts minutes, and it lives in the agent’s ossec.conf, not the manager’s:

<active-response>
  <repeated_offenders>30,60,1440</repeated_offenders>
</active-response>

The first hit blocks for the base timeout (600 seconds), the second for 30 minutes, the third for an hour, the fourth for a day. The mechanism does not work on Windows agents — keep that in mind in a mixed fleet. Starting values: 10–15 minutes of base ban is enough to kill enumeration and not so scary if a legitimate host gets caught.

Block lifecycle with escalation

And the main rule of rollout: test from an address whose access you can afford to lose. The first week is a short timeout and tail on the logs; you grow the repeated_offenders ladder once you trust there are no false positives.

QuestionOn-call humanfirewall-dropCustom AR script
Reaction timeminutes–hourssecondsseconds
Capable ofanything a human isblock srcip via the host firewallany code: nftables, APIs, isolation
Rollbackby a humantimeouttimeout or your own mechanism
False positivefatigue decidesa legitimate IP banned for N minutesexactly what you wrote
Entry costalready paidone block in ossec.confa script + a staging test

How to verify it works

Wazuh-logtest won’t help here: it runs the rules in an isolated session and does not trigger active response — you have to verify with a live event.

From a spare host outside the admin subnet, four failed SSH attempts against the agent:

for i in 1 2 3 4; do sshpass -p wrong ssh nosuchuser@AGENT_IP true; done

On the manager — that the alert fired and reached the reaction:

tail -f /var/ossec/logs/alerts/alerts.json | grep 100100

On the agent — the script’s own life and the result:

tail -f /var/ossec/logs/active-responses.log
nft list set inet wazuh_ar banned

The set must show the address with a timeout counting down, and SSH from the test host must stop answering. After ten minutes the element disappears on its own: that way you verify both the ban and the rollback. If active-responses.log stays empty — check /var/ossec/logs/ossec.log for execd errors, and make sure the rules_id matches and the script has 750 root:wazuh permissions.

Bottom line

Detection without response is just an expensive log: pretty, but the attack is over by the time anyone reads it. Active Response will not replace a full SOAR — there is no orchestration, playbooks, or approval chains here — but it closes the first step toward automated response almost for free: one block in ossec.conf for the built-in firewall-drop, forty lines of bash for nftables with a safety exception and a timeout — and the SIEM from parts 2 and 3 sees the brute force and closes the attacker’s IP by itself for the first time. In part 5 the series moves to Kubernetes: collecting the cluster’s audit logs and learning to see who does what in it.


Share this post:

Next Post
Wazuh: writing custom rules and decoders so your SIEM doesn't drown in noise (part 3/6)