Go to file
ppom 0b4030905b ignoreregex test 2024-02-09 12:00:00 +01:00
DEBIAN Update file control 2024-01-10 14:21:51 +00:00
app Add support for ignore regexes in filters 2024-02-08 13:28:51 +00:00
config ignoreregex test 2024-02-09 12:00:00 +01:00
helpers_c Add support for nftables 2024-02-05 12:00:00 +01:00
logger Do not panic when a regex doesn't compile 2024-01-04 12:00:00 +01:00
logo order 🧹 2023-04-26 17:11:03 +02:00
.gitignore release.sh & Makefile: add nft46 2024-02-07 12:00:00 +01:00
LICENSE Add AGPL LICENSE 2023-04-11 11:03:50 +00:00
Makefile release.sh & Makefile: add nft46 2024-02-07 12:00:00 +01:00
README.md Update wiki link to https://reaction.ppom.me 2024-01-17 12:00:00 +01:00
default.nix Standardize go project structure 2023-05-05 12:53:10 +02:00
go.mod Fix min go version 2023-11-21 12:00:00 +01:00
go.sum support json, jsonnet, yaml formats 2023-10-05 12:00:00 +02:00
reaction.go add a version subcommand 2023-12-31 12:00:00 +01:00
release.sh release.sh & Makefile: add nft46 2024-02-07 12:00:00 +01:00



A daemon that scans program outputs for repeated patterns, and takes action.

A common usage is to scan ssh and webserver logs, and to ban hosts that cause multiple authentication errors.

🚧 This program hasn't received external audit. however, it already works well on my servers 🚧


I was using the honorable fail2ban since quite a long time, but i was a bit frustrated by its cpu consumption and all its heavy default configuration.

In my view, a security-oriented program should be simple to configure and an always-running daemon should be implemented in a faster language.

reaction does not have all the features of the honorable fail2ban, but it's ~10x faster and has more manageable configuration.

📽️ quick french name explanation 😉

🇬🇧 in-depth blog article / 🇫🇷 french version


YAML and JSONnet (more powerful) are supported. both are extensions of JSON, so JSON is transitively supported.

  ip: '(([ 0-9 ]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})'

  - [ 'ip46tables', '-w', '-N', 'reaction' ]
  - [ 'ip46tables', '-w', '-A', 'reaction', '-j', 'ACCEPT' ]
  - [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '', '-j', 'ACCEPT' ]
  - [ 'ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]

  - [ 'ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ]
  - [ 'ip46tables', '-w', '-F', 'reaction' ]
  - [ 'ip46tables', '-w', '-X', 'reaction' ]

    cmd: [ 'journalctl', '-fu', 'sshd.service' ]
          - 'authentication failure;.*rhost=<ip>'
        retry: 3
        retryperiod: '6h'
            cmd: [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '<ip>', '-j', 'block' ]
            cmd: [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-j', 'block' ]
            after: '48h'
local iptables(args) = [ 'ip46tables', '-w' ] + args;
local banFor(time) = {
  ban: {
    cmd: iptables(['-A', 'reaction', '-s', '<ip>', '-j', 'reaction-log-refuse']),
  unban: {
    after: time,
    cmd: iptables(['-D', 'reaction', '-s', '<ip>', '-j', 'reaction-log-refuse']),
  patterns: {
    ip: {
      regex: @'(?:(?:[ 0-9 ]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})',
  start: [
    iptables([ '-N', 'reaction' ]),
    iptables([ '-A', 'reaction', '-j', 'ACCEPT' ]),
    iptables([ '-I', 'reaction', '1', '-s', '', '-j', 'ACCEPT' ]),
    iptables([ '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]),
  stop: [
    iptables([ '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ]),
    iptables([ '-F', 'reaction' ]),
    iptables([ '-X', 'reaction' ]),
  streams: {
    ssh: {
      cmd: [ 'journalctl', '-fu', 'sshd.service' ],
      filters: {
        failedlogin: {
          regex: [ @'authentication failure;.*rhost=<ip>' ],
          retry: 3,
          retryperiod: '6h',
          actions: banFor('48h'),


The embedded database is stored in the working directory. If you don't know where to start reaction, /var/lib/reaction should be a sane choice.


  • reaction start runs the server
  • reaction show show pending actions (ie. bans)
  • reaction flush permits to run pending actions (ie. clear bans)
  • reaction test-regex permits to test regexes
  • reaction help for full usage.


ip46tables is a minimal c program present in its own subdirectory with only standard posix dependencies.

It permits to configure iptables and ip6tables at the same time. It will execute iptables when detecting ipv4, ip6tables when detecting ipv6 and both if no ip address is present on the command line.


You'll find more ressources, service configurations, etc. on the Wiki!


Packaging status


Executables are provided here, for a standard x86-64 linux machine.

A standard place to put such executables is /usr/local/bin/.

Provided binaries in the previous section are compiled this way:

$ docker run -it --rm -e HOME=/tmp/ -v $(pwd):/tmp/code -w /tmp/code -u $(id -u) golang:1.20 make clean reaction.deb
$ make signaturese

Signature verification

Starting at v1.0.3, all binaries are signed with public key RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX. You can check their authenticity with minisign:

minisign -VP RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX -m ip46tables
minisign -VP RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX -m reaction
# or
minisign -VP RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX -m reaction.deb


The releases also contain a reaction.deb file, which packages reaction & ip46tables. You can install it using sudo apt install ./reaction.deb. You'll have to create a configuration at /etc/reaction.jsonnet.

If you want to use another configuration format (YAML or JSON), you can override systemd's ExecStart command in /etc/systemd/system/reaction.service like this:

# First an empty directive to reset the default one
# Then put what you want
ExecStart=/usr/bin/reaction start -c /etc/reaction.yml



You'll need the go (>= 1.20) toolchain for reaction and a c compiler for ip46tables.

$ make

Don't hesitate to take a look at the Makefile to understand what's happening!


To install the binaries

make install

To install the systemd file as well

make install_systemd