reaction
- title
- reaction
- type
- toolbox
- summary
- Lighter fail2ban alternative that tails any command's output and acts on repeated matches
- tags
- security, log-monitoring, daemon, rust, self-hosted, watchlist
- language
- Rust
- license
- AGPL-3.0
- created
- 2026-07-18
- updated
- 2026-07-18
A daemon that tails program output, watches for patterns that repeat too often, and runs a command when they do. The canonical use is what fail2ban does: watch ssh and webserver logs, count failed logins per source IP, and ban a host that trips the threshold. But the mechanism is generic โ a stream is any command whose stdout you want to watch, an action is any command you want to run, so it isn't tied to firewalls or to banning.
The pitch against fail2ban is speed and configuration weight. The author was running fail2ban for years and got tired of its CPU use and its heavy default config. reaction has fewer features but claims more than 10x lower CPU, and a config you can read top to bottom. It's funded through NLnet's NGI0 Core fund (EU Next Generation Internet money), not a solo hobby project in the abandonment sense, though it is single-author.
Version 2.x is a complete Rust rewrite at feature parity with the old Go version (1.x, now deprecated).
How it works
The config is a single file โ YAML or JSONnet, both supersets of JSON, so plain JSON works too. JSONnet is the more capable option because you can write functions and reuse them (e.g. a banFor(time) helper that generates matching ban/unban action pairs).
Four moving parts:
- patterns โ named capture types, e.g.
ip: { type: ipv4 }. A pattern namedipis referenced in regexes as<ip>. Note this is not the usual(...)capture-group /$1substitution style; reaction uses named<placeholder>tokens, which surprised at least one reader but reads cleanly. - start / stop โ lists of commands run once when the daemon comes up and goes down. Typically these create and tear down the firewall chain the ban actions write into.
- streams โ each stream has a
cmdthat produces a line feed (journalctl -fu sshd.service, atail -f, anything on stdout) and a set of filters. - filters โ each filter has a list of
regex, aretrycount, aretryperiodwindow, and named actions. When the same captured value (say one IP) matchesretrytimes insideretryperiod, the filter fires its actions. An action is acmd; an action can carry anafterdelay so the daemon schedules it for later โ that's howunbanruns 48h afterban.
Pending delayed actions (the current bans) are kept in an embedded on-disk database in the working directory, overridable with state_directory. The recommendation is /var/lib/reaction. Because state is persisted, bans survive a restart.
Usage
Minimal ssh brute-force protection, /etc/reaction.yml:
patterns:
ip:
type: ipv4
start:
- [ 'iptables', '-w', '-N', 'reaction' ]
- [ 'iptables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]
stop:
- [ 'iptables', '-w', '-F', 'reaction' ]
- [ 'iptables', '-w', '-X', 'reaction' ]
streams:
ssh:
cmd: [ 'journalctl', '-fu', 'sshd.service' ]
filters:
failedlogin:
regex:
- 'Failed password for .* from <ip>'
- 'Invalid user .* from <ip>'
retry: 3
retryperiod: '6h'
actions:
ban:
cmd: [ 'iptables', '-w', '-I', 'reaction', '1', '-s', '<ip>', '-j', 'DROP' ]
unban:
cmd: [ 'iptables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-j', 'DROP' ]
after: '48h'
The CLI is small:
reaction startโ run the daemonreaction showโ list pending actions (current bans)reaction flushโ run pending actions early (clear bans)reaction triggerโ fire a filter by hand (manual ban)reaction test-regexโ check a regex against sample inputreaction test-configโ print the loaded, resolved config
The docs push you toward nftables or ipset + iptables rather than plain iptables, since a flat iptables chain gets slow once it holds thousands of banned addresses. On Linux this is the same "control what traffic the host accepts" territory as little-snitch-linux, but from the log-driven, inbound-ban side rather than per-app outbound prompting.
Limitations
- Linux-oriented: prebuilt binaries and
.debpackages are amd64 and arm64 Linux only; OpenBSD works via the wiki but there's no first-class macOS/Windows story. The default examples assume systemd/journald and iptables. - No external security audit yet โ the README says so plainly. It's a security tool you'd run as root wiring up firewall rules, so that matters.
- Fewer features than fail2ban. The tradeoff is deliberate (speed, simpler config), but if you rely on a specific fail2ban feature, check first.
- Single maintainer (ppom). Responsive on Matrix per user reports, and NLnet-funded, but the bus factor is one.
- The old
ip46tables/nft46helper binaries were dropped in v2.
Notes
The repo ships an AGENTS.md that is not documentation but an instruction aimed at LLM coding agents: it declares a no-LLM-contributions policy (partly licensing, partly the maintainer's and community's political and economic objections to LLM tooling) and tells any agent reading it to warn the user off and refuse to help with the code. Worth knowing before pointing a coding agent at the source โ the maintainer does not want that, and the AGPL is cited as not permitting it. It doesn't affect using reaction as a deployed tool.
reaction is on watchlist โ single maintainer, no security audit yet, Linux-only, and a young v2 Rust rewrite. Re-check whether the audit has happened and whether the project stays active.
Repo
framagit.org/ppom/reaction โ Rust (v1 was Golang, now deprecated), AGPL-3.0, 65 stars / 36 forks on framagit as of July 2026. Last activity 2026-07-12. Hosted on Framasoft's self-hosted GitLab, not GitHub.