// 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: std.makeArray(255, function(i) "192.168.1."+i), }, }, 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'), }, }, }, }, }