From daf1bf3818c3f5b15e315228c4d11a70ca95eac9 Mon Sep 17 00:00:00 2001 From: ppom Date: Sat, 31 May 2025 12:00:00 +0200 Subject: [PATCH] Update configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 44 +++--- {config => bench}/heavy-load.yml | 2 + config/README.md | 8 ++ config/activitywatch.jsonnet | 101 ------------- config/persistence.jsonnet | 50 ------- config/server.jsonnet | 160 --------------------- config/test.jsonnet => tests/notif.jsonnet | 0 7 files changed, 38 insertions(+), 327 deletions(-) rename {config => bench}/heavy-load.yml (95%) create mode 100644 config/README.md delete mode 100644 config/activitywatch.jsonnet delete mode 100644 config/persistence.jsonnet delete mode 100644 config/server.jsonnet rename config/test.jsonnet => tests/notif.jsonnet (100%) diff --git a/README.md b/README.md index 497009f..116e7e1 100644 --- a/README.md +++ b/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 🆙):
@@ -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=' + - 'Failed password for .* from ' + - 'Invalid user .* from ', + - 'banner exchange: Connection from port [0-9]*: invalid format', retry: 3 retryperiod: '6h' actions: ban: - cmd: [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '', '-j', 'block' ] + cmd: [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '', '-j', 'DROP' ] unban: - cmd: [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '', '-j', 'block' ] + cmd: [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '', '-j', 'DROP' ] after: '48h' ``` @@ -85,13 +91,14 @@ streams: local iptables(args) = [ 'ip46tables', '-w' ] + args; local banFor(time) = { ban: { - cmd: iptables(['-A', 'reaction', '-s', '', '-j', 'reaction-log-refuse']), + cmd: iptables(['-A', 'reaction', '-s', '', '-j', 'DROP']), }, unban: { after: time, - cmd: iptables(['-D', 'reaction', '-s', '', '-j', 'reaction-log-refuse']), + cmd: iptables(['-D', 'reaction', '-s', '', '-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=' ], + regex: [ + @'authentication failure;.*rhost=' + @'Failed password for .* from ', + @'banner exchange: Connection from port [0-9]*: invalid format', + @'Invalid user .* from ', + ], 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 diff --git a/config/heavy-load.yml b/bench/heavy-load.yml similarity index 95% rename from config/heavy-load.yml rename to bench/heavy-load.yml index 6af6dfa..8ccfc2a 100644 --- a/config/heavy-load.yml +++ b/bench/heavy-load.yml @@ -1,4 +1,6 @@ --- +# This configuration permits to test reaction's performance +# under a very high load concurrency: 32 patterns: diff --git a/config/README.md b/config/README.md new file mode 100644 index 0000000..cbf7d67 --- /dev/null +++ b/config/README.md @@ -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. diff --git a/config/activitywatch.jsonnet b/config/activitywatch.jsonnet deleted file mode 100644 index 33e4083..0000000 --- a/config/activitywatch.jsonnet +++ /dev/null @@ -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: ['^$'], - actions: { - send: { cmd: log('focus ') }, - }, - }, - }, - }, - - // 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: ['^$'], - actions: { - send: { cmd: log('') }, - }, - }, - }, - }, - - // 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 $'], - // actions: { - // send: { cmd: log('tmux ') }, - // }, - // }, - // }, - // }, - - // Be notified about firefox activity - // TODO - }, -} diff --git a/config/persistence.jsonnet b/config/persistence.jsonnet deleted file mode 100644 index f3f58c1..0000000 --- a/config/persistence.jsonnet +++ /dev/null @@ -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 $'], - retry: 1, - retryperiod: '2m', - actions: { - damn: { - cmd: ['echo', ''], - }, - undamn: { - cmd: ['echo', 'undamn', ''], - 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 $'], - retry: 2, - retryperiod: '2m', - actions: { - damn: { - cmd: ['echo', ''], - }, - undamn: { - cmd: ['echo', 'undamn', ''], - after: '1m', - onexit: true, - }, - }, - }, - }, - }, - }, -} diff --git a/config/server.jsonnet b/config/server.jsonnet deleted file mode 100644 index 2886c28..0000000 --- a/config/server.jsonnet +++ /dev/null @@ -1,160 +0,0 @@ -// This is the extensive configuration used on a **real** server! - -local banFor(time) = { - ban: { - cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '', '-j', 'DROP'], - }, - unban: { - after: time, - cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '', '-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=', - @'Connection (reset|closed) by (authenticating|invalid) user .* ', - @'Failed password for .* from ', - ], - 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='], - 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":"".*"message":"Login failed:', - @'"remoteAddr":"".*"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: \. 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: [ - @'^ .* "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: [@'^.*GPTBot/1.0'], - actions: banFor('720h'), - }, - - // Ban hosts failing to connect to slskd - slskd: { - regex: [ - @'^ .* "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 - // ^^^^^ - @'^.*"GET /(?:[^/" ]*/)*\.env ', - @'^.*"GET /(?:[^/" ]*/)*info\.php ', - @'^.*"GET /(?:[^/" ]*/)*owa/auth/logon.aspx ', - @'^.*"GET /(?:[^/" ]*/)*auth.html ', - @'^.*"GET /(?:[^/" ]*/)*auth1.html ', - @'^.*"GET /(?:[^/" ]*/)*password.txt ', - @'^.*"GET /(?:[^/" ]*/)*passwords.txt ', - @'^.*"GET /(?:[^/" ]*/)*dns-query ', - // Do not include this if you have a Wordpress website ;) - @'^.*"GET /(?:[^/" ]*/)*wp-login\.php', - @'^.*"GET /(?:[^/" ]*/)*wp-includes', - // Do not include this if a client must retrieve a config.json file ;) - @'^.*"GET /(?:[^/" ]*/)*config\.json ', - ], - actions: banFor('720h'), - }, - }, - }, - }, -} diff --git a/config/test.jsonnet b/tests/notif.jsonnet similarity index 100% rename from config/test.jsonnet rename to tests/notif.jsonnet