Update configuration

- README.md: update minimal example (was outdated since a long time 🤐)
- README.md: link to Security part of the wiki
- README.md: fix OpenBSD broken link
- README.md: add `state_directory` option
- activitywatch & server configs: remove from config directory. only
  available in the wiki.
- heavy-load: move to new bench directory
- test: move to tests directory

Fix #121
This commit is contained in:
ppom 2025-05-31 12:00:00 +02:00
commit daf1bf3818
No known key found for this signature in database
7 changed files with 38 additions and 327 deletions

View file

@ -36,7 +36,7 @@ both are extensions of JSON, so JSON is transitively supported.
- See [reaction.yml](./app/example.yml) or [reaction.jsonnet](./config/example.jsonnet) for a fully explained reference
- See [server.jsonnet](./config/server.jsonnet) for a real-world configuration
- See [reaction.example.service](./config/reaction.example.service) for a systemd service file
- This minimal example shows what's needed to prevent brute force attacks on an ssh server (please take a look at more complete references before starting 🆙):
- This minimal example shows what's needed to prevent brute force attacks on an ssh server (please read at least the [Security](https://reaction.ppom.me/security.html) part of the wiki before starting 🆙):
<details open>
@ -46,15 +46,18 @@ both are extensions of JSON, so JSON is transitively supported.
patterns:
ip:
regex: '(([0-9]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})'
ignore:
- '127.0.0.1'
- '::1'
start:
- [ 'ip46tables', '-w', '-N', 'reaction' ]
- [ 'ip46tables', '-w', '-A', 'reaction', '-j', 'ACCEPT' ]
- [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT' ]
- [ 'ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]
- [ 'ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]
- [ 'ip46tables', '-w', '-I', 'FORWARD', '-p', 'all', '-j', 'reaction' ]
stop:
- [ 'ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ]
- [ 'ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ]
- [ 'ip46tables', '-w', '-D', 'FORWARD', '-p', 'all', '-j', 'reaction' ]
- [ 'ip46tables', '-w', '-F', 'reaction' ]
- [ 'ip46tables', '-w', '-X', 'reaction' ]
@ -65,13 +68,16 @@ streams:
failedlogin:
regex:
- 'authentication failure;.*rhost=<ip>'
- 'Failed password for .* from <ip>'
- 'Invalid user .* from <ip>',
- 'banner exchange: Connection from <ip> port [0-9]*: invalid format',
retry: 3
retryperiod: '6h'
actions:
ban:
cmd: [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '<ip>', '-j', 'block' ]
cmd: [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '<ip>', '-j', 'DROP' ]
unban:
cmd: [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-j', 'block' ]
cmd: [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-j', 'DROP' ]
after: '48h'
```
@ -85,13 +91,14 @@ streams:
local iptables(args) = [ 'ip46tables', '-w' ] + args;
local banFor(time) = {
ban: {
cmd: iptables(['-A', 'reaction', '-s', '<ip>', '-j', 'reaction-log-refuse']),
cmd: iptables(['-A', 'reaction', '-s', '<ip>', '-j', 'DROP']),
},
unban: {
after: time,
cmd: iptables(['-D', 'reaction', '-s', '<ip>', '-j', 'reaction-log-refuse']),
cmd: iptables(['-D', 'reaction', '-s', '<ip>', '-j', 'DROP']),
},
};
{
patterns: {
ip: {
@ -100,12 +107,12 @@ local banFor(time) = {
},
start: [
iptables([ '-N', 'reaction' ]),
iptables([ '-A', 'reaction', '-j', 'ACCEPT' ]),
iptables([ '-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT' ]),
iptables([ '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]),
iptables([ '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]),
iptables([ '-I', 'FORWARD', '-p', 'all', '-j', 'reaction' ]),
],
stop: [
iptables([ '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ]),
iptables([ '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ]),
iptables([ '-D', 'FORWARD', '-p', 'all', '-j', 'reaction' ]),
iptables([ '-F', 'reaction' ]),
iptables([ '-X', 'reaction' ]),
],
@ -114,7 +121,12 @@ local banFor(time) = {
cmd: [ 'journalctl', '-fu', 'sshd.service' ],
filters: {
failedlogin: {
regex: [ @'authentication failure;.*rhost=<ip>' ],
regex: [
@'authentication failure;.*rhost=<ip>'
@'Failed password for .* from <ip>',
@'banner exchange: Connection from <ip> port [0-9]*: invalid format',
@'Invalid user .* from <ip>',
],
retry: 3,
retryperiod: '6h',
actions: banFor('48h'),
@ -130,7 +142,7 @@ local banFor(time) = {
### Database
The embedded database is stored in the working directory.
The embedded database is stored in the working directory (but can be overriden by the `state_directory` config option).
If you don't know where to start reaction, `/var/lib/reaction` should be a sane choice.
### CLI
@ -172,7 +184,7 @@ reaction is packaged, but the [**module**](https://framagit.org/ppom/nixos/-/blo
#### OpenBSD
[wiki](https://reaction.ppom.me/configs/openbsd.html)
See the [wiki](https://reaction.ppom.me/configurations/OpenBSD.html).
### Compilation

View file

@ -1,4 +1,6 @@
---
# This configuration permits to test reaction's performance
# under a very high load
concurrency: 32
patterns:

8
config/README.md Normal file
View file

@ -0,0 +1,8 @@
# Configuration
Here reside two equal configurations, one in YAML and one in JSONnet.
Those serve as a configuration reference for now, waiting for a more complete reference in the wiki.
Please take a look at the [wiki](https://reaction.ppom.me) for security implications of using reaction,
FAQ, JSONnet tips, and multiple examples of filters and actions.

View file

@ -1,101 +0,0 @@
// Those strings will be substitued in each shell() call
local substitutions = [
['OUTFILE', '"$HOME/.local/share/watch/logs-$(date +%F)"'],
['DATE', '"$(date "+%F %T")"'],
];
// Substitue each substitutions' item in string
local sub(str) = std.foldl(
(function(changedstr, kv) std.strReplace(changedstr, kv[0], kv[1])),
substitutions,
str
);
local shell(prg) = [
'sh',
'-c',
sub(prg),
];
local log(line) = shell('echo DATE ' + std.strReplace(line, '\n', ' ') + '>> OUTFILE');
{
start: [
shell('mkdir -p "$(dirname OUTFILE)"'),
log('start'),
],
stop: [
log('stop'),
],
patterns: {
all: { regex: '.*' },
},
streams: {
// Be notified about each window focus change
// FIXME DOESN'T WORK
sway: {
cmd: shell(|||
swaymsg -rm -t subscribe "['window']" | jq -r 'select(.change == "focus") | .container | if has("app_id") and .app_id != null then .app_id else .window_properties.class end'
|||),
filters: {
send: {
regex: ['^<all>$'],
actions: {
send: { cmd: log('focus <all>') },
},
},
},
},
// Be notified when user is away
swayidle: {
// FIXME echo stop and start instead?
cmd: ['swayidle', 'timeout', '30', 'echo sleep', 'resume', 'echo resume'],
filters: {
send: {
regex: ['^<all>$'],
actions: {
send: { cmd: log('<all>') },
},
},
},
},
// Be notified about tmux activity
// Limitation: can't handle multiple concurrently attached sessions
// tmux: {
// cmd: shell(|||
// LAST_TIME="0"
// LAST_ACTIVITY=""
// while true;
// do
// NEW_TIME=$(tmux display -p '#{session_activity}')
// if [ -n "$NEW_TIME" ] && [ "$NEW_TIME" -gt "$LAST_TIME" ]
// then
// LAST_TIME="$NEW_TIME"
// NEW_ACTIVITY="$(tmux display -p '#{pane_current_command} #{pane_current_path}')"
// if [ -n "$NEW_ACTIVITY" ] && [ "$NEW_ACTIVITY" != "$LAST_ACTIVITY" ]
// then
// LAST_ACTIVITY="$NEW_ACTIVITY"
// echo "tmux $NEW_ACTIVITY"
// fi
// fi
// sleep 10
// done
// |||),
// filters: {
// send: {
// regex: ['^tmux <all>$'],
// actions: {
// send: { cmd: log('tmux <all>') },
// },
// },
// },
// },
// Be notified about firefox activity
// TODO
},
}

View file

@ -1,50 +0,0 @@
{
patterns: {
num: {
regex: '[0-9]+',
},
},
streams: {
tailDown1: {
cmd: ['sh', '-c', "echo 01 02 03 04 05 | tr ' ' '\n' | while read i; do sleep 0.5; echo found $i; done"],
filters: {
findIP1: {
regex: ['^found <num>$'],
retry: 1,
retryperiod: '2m',
actions: {
damn: {
cmd: ['echo', '<num>'],
},
undamn: {
cmd: ['echo', 'undamn', '<num>'],
after: '1m',
onexit: true,
},
},
},
},
},
tailDown2: {
cmd: ['sh', '-c', "echo 11 12 13 14 15 11 13 15 | tr ' ' '\n' | while read i; do sleep 0.3; echo found $i; done"],
filters: {
findIP2: {
regex: ['^found <num>$'],
retry: 2,
retryperiod: '2m',
actions: {
damn: {
cmd: ['echo', '<num>'],
},
undamn: {
cmd: ['echo', 'undamn', '<num>'],
after: '1m',
onexit: true,
},
},
},
},
},
},
}

View file

@ -1,160 +0,0 @@
// This is the extensive configuration used on a **real** server!
local banFor(time) = {
ban: {
cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP'],
},
unban: {
after: time,
cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP'],
},
};
{
patterns: {
// IPs can be IPv4 or IPv6
// ip46tables (C program also in this repo) handles running the good commands
ip: {
regex: @'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))',
// Ignore all from 192.168.1.1 to 192.168.1.255
ignore: std.makeArray(255, function(i) '192.168.1.' + (i + 1)),
},
},
start: [
['ip46tables', '-w', '-N', 'reaction'],
['ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction'],
],
stop: [
['ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction'],
['ip46tables', '-w', '-F', 'reaction'],
['ip46tables', '-w', '-X', 'reaction'],
],
streams: {
// Ban hosts failing to connect via ssh
ssh: {
cmd: ['journalctl', '-fn0', '-u', 'sshd.service'],
filters: {
failedlogin: {
regex: [
@'authentication failure;.*rhost=<ip>',
@'Connection (reset|closed) by (authenticating|invalid) user .* <ip>',
@'Failed password for .* from <ip>',
],
retry: 3,
retryperiod: '6h',
actions: banFor('48h'),
},
},
},
// Ban hosts which knock on closed ports.
// It needs this iptables chain to be used to drop packets:
// ip46tables -N log-refuse
// ip46tables -A log-refuse -p tcp --syn -j LOG --log-level info --log-prefix 'refused connection: '
// ip46tables -A log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
// ip46tables -A log-refuse -j DROP
kernel: {
cmd: ['journalctl', '-fn0', '-k'],
filters: {
portscan: {
regex: ['refused connection: .*SRC=<ip>'],
retry: 4,
retryperiod: '1h',
actions: banFor('720h'),
},
},
},
// Note: nextcloud and vaultwarden could also be filters on the nginx stream
// I did use their own logs instead because it's less logs to parse than the front webserver
// Ban hosts failing to connect to Nextcloud
nextcloud: {
cmd: ['journalctl', '-fn0', '-u', 'phpfpm-nextcloud.service'],
filters: {
failedLogin: {
regex: [
@'"remoteAddr":"<ip>".*"message":"Login failed:',
@'"remoteAddr":"<ip>".*"message":"Trusted domain error.',
],
retry: 3,
retryperiod: '1h',
actions: banFor('1h'),
},
},
},
// Ban hosts failing to connect to vaultwarden
vaultwarden: {
cmd: ['journalctl', '-fn0', '-u', 'vaultwarden.service'],
filters: {
failedlogin: {
actions: banFor('2h'),
regex: [@'Username or password is incorrect\. Try again\. IP: <ip>\. Username:'],
retry: 3,
retryperiod: '1h',
},
},
},
// Used with this nginx log configuration:
// log_format withhost '$remote_addr - $remote_user [$time_local] $host "$request" $status $bytes_sent "$http_referer" "$http_user_agent"';
// access_log /var/log/nginx/access.log withhost;
nginx: {
cmd: ['tail', '-n0', '-f', '/var/log/nginx/access.log'],
filters: {
// Ban hosts failing to connect to Directus
directus: {
regex: [
@'^<ip> .* "POST /auth/login HTTP/..." 401 [0-9]+ .https://directusdomain',
],
retry: 6,
retryperiod: '4h',
actions: banFor('4h'),
},
// Ban hosts presenting themselves as bots of ChatGPT
gptbot: {
regex: [@'^<ip>.*GPTBot/1.0'],
actions: banFor('720h'),
},
// Ban hosts failing to connect to slskd
slskd: {
regex: [
@'^<ip> .* "POST /api/v0/session HTTP/..." 401 [0-9]+ .https://slskddomain',
],
retry: 3,
retryperiod: '1h',
actions: banFor('6h'),
},
// Ban suspect HTTP requests
// Those are frequent malicious requests I got from bots
// Make sure you don't have honnest use cases for those requests, or your clients may be banned for 2 weeks!
suspectRequests: {
regex: [
// (?:[^/" ]*/)* is a "non-capturing group" regex that allow for subpath(s)
// example: /code/.env should be matched as well as /.env
// ^^^^^
@'^<ip>.*"GET /(?:[^/" ]*/)*\.env ',
@'^<ip>.*"GET /(?:[^/" ]*/)*info\.php ',
@'^<ip>.*"GET /(?:[^/" ]*/)*owa/auth/logon.aspx ',
@'^<ip>.*"GET /(?:[^/" ]*/)*auth.html ',
@'^<ip>.*"GET /(?:[^/" ]*/)*auth1.html ',
@'^<ip>.*"GET /(?:[^/" ]*/)*password.txt ',
@'^<ip>.*"GET /(?:[^/" ]*/)*passwords.txt ',
@'^<ip>.*"GET /(?:[^/" ]*/)*dns-query ',
// Do not include this if you have a Wordpress website ;)
@'^<ip>.*"GET /(?:[^/" ]*/)*wp-login\.php',
@'^<ip>.*"GET /(?:[^/" ]*/)*wp-includes',
// Do not include this if a client must retrieve a config.json file ;)
@'^<ip>.*"GET /(?:[^/" ]*/)*config\.json ',
],
actions: banFor('720h'),
},
},
},
},
}