mirror of
https://framagit.org/ppom/reaction
synced 2026-03-14 12:45:47 +01:00
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:
parent
ebad317a97
commit
daf1bf3818
7 changed files with 38 additions and 327 deletions
44
README.md
44
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
8
config/README.md
Normal 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.
|
||||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -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'),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue