Reference
Configuration
reaction can be configured using the following formats:
See FAQ "What is JSONnet and why should I use it over YAML" for a rationale.
It can be configured with either:
- one configuration file,
- or a directory containing multiple configuration files:
- files are loaded and merged in alphanumeric order,
- files beginning with
.
or_
are ignored, - files not ending with a supported extension are ignored,
- no recursion in subdirectories.
Here's a graph of reaction's model. Each concept is explained below, along with all its configuration options.
Table of Contents
Stream
A Stream is an ASCII or UTF-8 text stream, typically logs.
cmd
An array of strings. The first string is the command, and the subsequent strings are the arguments.
reaction will execute the command to fetch the text stream.
command's stdout
and stderr
are read and treated equally by reaction.
See FAQ "Why start, stop, stream and action commands are arrays" for an explanation.
See Streams for details on how to write correct Stream commands for reaction.
{
streams: {
ssh: {
cmd: ['journalctl', '-f', '-n0', ...],
},
nginx: {
cmd: ['tail', '-F', '-n0', ...],
},
},
}
streams:
ssh:
cmd: ['journalctl', '-f', '-n0', ...]
web:
cmd: ['tail', '-F', '-n0', ...]
filters
We can attach one or more Filters to a Stream.
{
streams: {
ssh: {
cmd: ['journalctl', '-f', '-n0', ...],
filters: {
myfilter: {
...
},
myotherfilter: {
...
},
},
},
nginx: {
cmd: ['tail', '-F', '-n0', ...],
filters: {
...
},
},
},
}
streams:
ssh:
cmd: ['journalctl', '-f', '-n0', ...]
filters:
myfilter:
...
myotherfilter:
...
web:
cmd: ['tail', '-F', '-n0', ...]
filters:
...
Pattern
A pattern is essentially a regex.
It's included in Filters' regex to capture a specific part of the line, for example an IP or a username.
It's referenced in a Filter and Action by its name enclosed in <
and >
.
regex
The regex pattern.
{
patterns: {
name: {
regex: '[A-Z][a-z]*',
},
},
}
patterns:
name:
regex: '[A-Z][a-z]*'
ignore
A list of values to ignore.
{
patterns: {
name: {
regex: '[A-Z][a-z]*',
ignore: [
'Alice',
'Bob',
],
},
},
}
patterns:
name:
regex: '[A-Z][a-z]*'
ignore:
- 'Alice'
- 'Bob'
ignoreregex
A list of regex to ignore.
regex must match the full Match.
{
patterns: {
name: {
regex: '[A-Z][a-z]*',
ignoreregex: [
# Ignore names starting with Chr
'Chr.*',
],
},
},
}
patterns:
name:
regex: '[A-Z][a-z]*'
ignoreregex:
# Ignore names starting with Chr
- 'Chr.*'
type
Available since v2.2.0.
reaction ships with 3 special pattern types. Those types ship with a regex provided by reaction:
ip
, that matches both IPv4 and IPv6,ipv4
, that matches IPv4,ipv6
, that matches IPv6.
The default implicit type is regex
, for non-IP regexes.
{
patterns: {
ip: {
type: 'ip',
},
},
}
patterns:
ip:
type: 'ip'
{
patterns: {
ip4: {
type: 'ipv4',
},
},
}
patterns:
ip4:
type: 'ipv4'
ignorecidr
Available since v2.2.0.
Only for patterns of an IP type.
A list of IP networks to ignore, with CIDR notation.
{
patterns: {
ip: {
type: 'ip',
ignorecidr: [
'192.168.1.0/24',
'2001:db8:2345:3456::/64',
],
},
},
}
patterns:
ip:
type: 'ip'
ignorecidr:
- '192.168.1.0/24'
- '2001:db8:2345:3456::/64'
ipv4mask
and ipv6mask
Available since v2.2.0.
Only for patterns of an IP type.
Group IP Matches by network.
IPv6 are very cheap: malicious actors typically have 2^64 IPv6, with a /64 network mask. This is common even on residential IPs.
{
patterns: {
ip: {
type: 'ip',
ipv6mask: 64,
},
},
}
patterns:
ip:
type: 'ip'
ipv6mask: 64
With this configuration, those IPv6s will be grouped:
2001:db8:2345:3456::1
2001:db8:2345:3456::2
2001:db8:2345:3456::3
And the corresponding action will be run with the network mask:
2001:db8:2345:3456::/64
This is also possible for IPv4. Be careful doing this! Some actors may have only 1, 2, 4 IPs from a range, so this may be a bad idea.
{
patterns: {
ip: {
type: 'ip',
ipv4mask: 30, // 24 ...
ipv6mask: 64,
},
},
}
patterns:
ip:
type: 'ip'
ipv4mask: 30 # 24 ...
ipv6mask: 64
ipv4mask
only makes sense with patterns of typeip
andipv4
.ipv6mask
only makes sense with patterns of typeip
andipv6
.
Match
A Match is one concrete instance of a Pattern found in the logs.
For example, if we have a Pattern user
with regex [a-z]+
, and a Filter with regex ^hello <user>$
, the line hello charlie
will result in the Match charlie
.
A Match can be reused in Actions.
So an Action with the command ['echo', '<user>']
would run echo charlie
for the previous match.
Trigger
A Trigger is a Match that will make the related Filter execute its Actions.
How much Matches must happen before the Filter is triggered depends on its retry
and retryperiod
options.
Filter
Filters run Actions when they Match regexes on a Stream.
Filters are reaction's main component, enclosing most of its runtime logic.
A Filter is attached to a Stream and receive its text stream as an input.
It applies one or more regexes to each line. When there is one or more Match in a given period, the Filter is Triggered and execute one or more Actions.
regex
A list of regexes to try matching on the input Stream.
Whenever one of them matches, a Match is created.
streams:
nginx:
...
filters:
unauthorized:
regex:
- '^<ip> .* HTTP/\d\.\d" 401'
streams: {
nginx: {
...
filters: {
unauthorized: {
regex: [
'^<ip> .* HTTP/\d\.\d" 401',
],
},
},
},
}
Patterns can be referenced in the regexes by providing their name enclosed in <
and >
.
The Match will then contain the actual value matched by the Pattern and can be reused in Actions.
All regexes of a Filter must specify the same set of Patterns.
patterns:
ip:
type: 'ip'
streams:
nginx:
...
filters:
unauthorized:
regex:
- ' HTTP/\d\.\d" 401'
patterns: {
ip: {
type: 'ip',
},
},
streams: {
nginx: {
...
filters: {
unauthorized: {
regex: [
'^<ip> .* HTTP/\d\.\d" 401',
],
},
},
},
}
retry
How many Matches must happen before the Filter Triggers its Actions.
If not specified, defaults to 1. If specified, must be > 1.
Must be specified along with retryperiod
.
retryperiod
The retain period for Matches.
Must be specified along with retry
.
Format is defined as follows:
<number> <unit>
- whitespace between the integer and unit is optional
- number must be a positive integer (>= 0, no floating point)
- unit can be one of:
ms
/millis
/millisecond
/milliseconds
s
/sec
/secs
/second
/seconds
m
/min
/mins
/minute
/minutes
h
/hour
/hours
d
/day
/days
Examples:
{
streams:
stream1: {
filters: {
filter0: {
regex: [ ... ],
// no retry/retryperiod, trigger as soon as there is a match
},
filter1: {
regex: [ ... ],
// 2 matches in 10 seconds to trigger
retry: 2,
retryperiod: '10 secs',
},
filter2: {
regex: [ ... ],
// 4 matches in 30 minutes to trigger
retry: 4,
retryperiod: '30m',
},
filter3: {
regex: [ ... ],
// 3 matches in 1 day to trigger
retry: 3,
retryperiod: '1day',
},
},
},
},
}
duplicate
Available since v2.2.0.
Specify what reaction must do when the Filter matches an already Triggered Match.
3 behaviors are possible: extend
, rerun
and ignore
.
Before v2.2.0, reaction's filters used to execute the same actions multiple times.
For example, when an IP address was spamming the server, due to the latency
between the moment the request was received and the moment the ban action was
executed by reaction, and taken into account by the firewall, other logs
concerning this IP address could appear, resulting in a new trigger of the
reaction filter.
Therefore an IP could be banned multiple times, resulting in bugs on some
firewalls that deduplicate IPs, like ipset
and nftables
.
The new behavior defaults to extending the trigger period.
Let's take this filter as an example:
{
regex: [ @'Failed password for .* from <ip>' ],
actions: {
ban: {
cmd: ['iptables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP'],
},
unban: {
cmd: ['iptables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP'],
after: '2h',
},
},
}
If a filter is triggered at 0:00, the ban
action runs, and the unban
action is scheduled at 2:00.
If for some reason, some new matches appear at 0:10:
- reaction defaults to extending the
unban
. So it won't schedule a newban
command. It will instead reschedule the existingunban
action to 2:10. This default behavior can be explicitly specified by:duplicate: 'extend' // the default
- reaction can launch a new
ban
command, and schedule a newunban
command at 2:10. This (old default) behavior is possible by adding this parameter:duplicate: 'rerun'
- reaction can also simply ignore new matches for this IP, until the
unban
command runs:duplicate: 'ignore'
actions
We can attach one or more Actions to a Filter.
{
streams: {
...
filters: {
failedlogin: {
regex: [ ... ],
actions: {
...
},
},
connectionreset: {
regex: [ ... ],
actions: {
...
},
},
},
},
}
streams:
...
filters:
failedlogin:
regex:
...
actions:
...
connectionreset:
regex:
...
actions:
...
Action
Actions are commands that a Filter runs when it is Triggered.
Actions are the reaction!
cmd
An array of strings. The first string is the command, and the subsequent strings are the arguments.
See FAQ "Why start, stop, stream and action commands are arrays" for an explanation.
{
action1: {
cmd: [ '/root/myscript.sh' ],
},
}
Patterns specified in the Action's Filter can
be referenced in the command by providing their name enclosed in <
and >
.
It will be substitued by the actual Match value.
{
action2: {
cmd: [ 'iptables', '-I', ..., '<ip>' ],
},
}
after
By default, actions are executed as soon as the Filter is Triggered.
An action can be delayed with after
.
Format is defined as follows:
<number> <unit>
- whitespace between the integer and unit is optional
- number must be a positive integer (>= 0, no floating point)
- unit can be one of:
ms
/millis
/millisecond
/milliseconds
s
/sec
/secs
/second
/seconds
m
/min
/mins
/minute
/minutes
h
/hour
/hours
d
/day
/days
{
action1: {
cmd: [ ... ],
// no after, action is run immediately
},
action2: {
cmd: [ ... ],
after: '10 secs',
},
action3: {
cmd: [ ... ],
after: '30m',
},
action4: {
cmd: [ ... ],
after: '1day',
},
}
A delayed action that is scheduled and that will run later is called a pending action.
onexit
Whether to run this action when pending (scheduled by a Filter for later) on exit.
Defaults to false.
For firewalls, it's much more performant to just flush the chain or IP set than to removing each IP one by one.
Only makes sense when after
is set.
{
action1: {
cmd: [ ... ],
},
action2: {
cmd: [ ... ],
after: '2h',
onexit: true,
},
}
action1:
cmd: ...
action2:
cmd: ...
after: '2h'
onexit: true
oneshot
Available since v2.1.0.
Whether this action will rerun when reaction restarts.
Useful for alerting actions, such as sending a mail.
When reaction restarts, you surely want an IP to be banned again, but not to send the associated alert again.
Defaults to true.
{
ban: {
cmd: [ ... ],
},
unban: {
cmd: [ ... ],
after: '12h',
},
mail: {
cmd: [ ... ],
oneshot: true,
},
}
ban:
cmd: ...
unban:
cmd: ...
after: '12h'
mail:
cmd: ...
oneshot: true
ipv4only
and ipv6only
Available since v2.2.0.
Only makes sense when the underlying Filter's regex
has a Pattern of type
ip
.
ipv4only
: only execute this action when thetype: ip
Pattern matches an IPv4.ipv6only
: only execute this action when thetype: ip
Pattern matches an IPv6.
Both options are mutually exclusive.
{
ban: {
cmd: [ 'iptables', ... ],
ipv4only: true,
},
ban6: {
cmd: [ 'ip6tables', ... ],
ipv6only: true,
},
}
ban:
cmd: [ 'iptables', ... ]
ipv4only: true
ban6:
cmd: [ 'ip6tables', ... ]
ipv6only: true
Persistance
TL;DR: when an
after
Action is set in a Filter, such Filter acts as a jail, which is persisted after reboots.
When a filter is triggered, there are 2 possibilities:
-
If none of its Actions have an
after
directive set:- no action will be replayed when reaction restarts.
-
If at least one Action has an
after
directive set:If reaction stops while
after
Actions are pending,and reaction starts again when those actions would still be pending, reaction:
- executes the past actions (actions without after or with then+after <= now),
- plans the execution of future actions (actions with then+after > now).
Top-level options
start
and stop
An array of arrays of strings.
So it's an array of commands just like Stream's cmd
and Action's cmd
.
start
specifies a list of commands that will be run on start,
after initialization and before streams are started.
If any of the commands exit with a non-zero exit code,
reaction will exit with an error.
start
can be useful to prepare useful state for actions (intializing firewall, etc).
stop
specifies a list of commands that will be run on stop,
after all pending Actions (with onexit: true
) are run.
stop
can be useful to clean state set bystart
and Actions.
state_directory
Where reaction's internal state is stored.
Defaults to .
(the working directory, ie. the directory from which reaction is run).
state_directory: /var/lib/reaction
{
state_directory: "/var/lib/reaction",
}
Currently, only a
reaction.db
file is stored, but this will change in the future.Releases before v2.0.0 used other files, which can be safely removed.
concurrency
Integer that limits the maximum number of parallel actions.
- If set to a positive number, this will be the maximum number of parallel actions.
- If not specified or set to 0, it defaults to the number of CPUs on the system.
- If set to a negative number, there will be no limit on the number of parallel actions.
concurrency: 8
{
concurrency: 8
}