From 8aaa04389f153837ac28a20186c32372d8b4adf8 Mon Sep 17 00:00:00 2001 From: Y Date: Sat, 17 Mar 2018 17:46:22 +0100 Subject: [PATCH] ipset support; fixes #1 --- Changelog.md | 12 + README.md | 4 +- doc/configure.md | 77 ++- doc/dnat.md | 2 +- doc/install.md | 4 +- doc/intro_func.md | 2 +- doc/intro_tech.md | 6 +- doc/logandban.md | 112 +++- extra/examples/full_pyruse.json | 909 ++++++++++++++++------------ extra/examples/get-systemd-stats.sh | 14 +- extra/systemd/action_ipsetBan.conf | 3 + pyruse/actions/action_ipsetBan.py | 37 ++ pyruse/actions/action_nftBan.py | 92 +-- pyruse/ban.py | 85 +++ tests/action_ipsetBan.py | 147 +++++ tests/action_nftBan.py | 10 +- tests/main.py | 3 +- tests/pyruse.json | 3 + 18 files changed, 1009 insertions(+), 513 deletions(-) create mode 100644 Changelog.md create mode 100644 extra/systemd/action_ipsetBan.conf create mode 100644 pyruse/actions/action_ipsetBan.py create mode 100644 pyruse/ban.py create mode 100644 tests/action_ipsetBan.py diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..fbea396 --- /dev/null +++ b/Changelog.md @@ -0,0 +1,12 @@ +# Changelog + +This file is not intended as a dupplicate of Git logs. +Its purpose is to warn of important changes between version, that users should be aware of. + +## v2.0 + +After this version is installed, the following command should be run on the `action_nftBan.py.json` file: + +```bash +$ sudo sed -i s/nftSet/nfSet/g action_nftBan.py.json +``` diff --git a/README.md b/README.md index 1895bfb..a5ccf4b 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ For example, to my knowledge, there is no equivalent in any tool of the same sca Pyruse is [packaged for Archlinux](https://aur.archlinux.org/packages/pyruse/). For other distributions, please [read the manual installation instructions](doc/install.md). +Whenever your upgrade Pyruse, make sure to check the [Changelog](Changelog.md). + ## Configuration The `/etc/pyruse` directory is where system-specific files are looked-for: @@ -47,7 +49,7 @@ For more in-depth documentation, please refer to these pages: - [the built-in filters](doc/builtinfilters.md) - [the counter-based actions](doc/counters.md) - [the DNAT-related actions](doc/dnat.md) + - [the actions that log and ban](doc/logandban.md) - [the `action_noop` module](doc/noop.md) - [the `action_email` module](doc/action_email.md) - [the `action_dailyReport` module](doc/action_dailyReport.md) - - [the `action_nftBan` and `action_log` modules](doc/logandban.md) diff --git a/doc/configure.md b/doc/configure.md index be451c1..82091d6 100644 --- a/doc/configure.md +++ b/doc/configure.md @@ -1,33 +1,46 @@ # Configuration tips -In contrast with legacy log parsers, Pyruse works with structured [systemd-journal entries](https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html). This allows for better performance, since targeted comparisons become possible. +In contrast with legacy log parsers, Pyruse works with structured [systemd-journal entries](https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html). +This allows for better performance, since targeted comparisons become possible. -The general intent, when writing the configuration file, should be to handle the log entries that appear the most often first, in as few steps as possible. For example, I ran some stats on my server before writing my own configuration file; I got: +The general intent, when writing the configuration file, should be to handle the log entries that appear the most often first, in as few steps as possible. +For example, I ran some stats on my server on the log entries of the past week; I got: -| systemd units | number of journal entries | -| ------------------------------- | -------------------------:| -| `prosody.service` | 518019 | -| `gitea.service` | 329389 | -| `uwsgi@nextcloud.service` | 217342 | -| `session-*.scope` | 89813 | -| `nginx.service` | 80762 | -| `dovecot.service` | 61898 | -| `exim.service` | 60743 | -| `init.scope` | 43021 | -| `nextcloud-maintenance.service` | 20775 | -| `haproxy.service` | 18445 | -| `user@*.service` | 7306 | -| `minidlna.service` | 6032 | -| `loolwsd.service` | 5797 | -| `sshd.service` | 4959 | -| `spamassassin-update.service` | 2383 | -| `systemd-nspawn@*.service` | 1497 | -| `nslcd.service` | 867 | -| `nfs-mountd.service` | 723 | -| `systemd-logind.service` | 696 | -| `nfs-server.service` | 293 | -| `systemd-networkd.service` | 121 | -| misc. units with < 100 entries | | +| SYSLOG_IDENTIFIER | number of journal entries | +| ----------------------- | -------------------------:| +| `uwsgi` (for Nextcloud) | 55930 | +| `gitea` | 38923 | +| `prosody` | 25596 | +| `haproxy` | 21877 | +| `postgres` | 12990 | +| `nginx` | 12808 | +| `dovecot` | 7062 | +| `exim` | 2540 | +| `systemd` | 1997 | +| `su` | 1458 | +| `ownCloud` (Nextcloud) | 1067 | +| `sshd` | 1051 | +| `mandb` | 953 | +| `spamd` | 855 | +| `pyruse` | 615 | +| `kernel` | 420 | +| `msmtp` | 295 | +| `sa-compile` | 255 | +| `ansible-*` | 103 | +| `systemd-logind` | 102 | +| `python` | 78 | +| `rpc.mountd` | 52 | +| `ldapwhoami` | 42 | +| `prosody_auth` | 42 | +| `minidlnad` | 39 | +| `kill` | 28 | +| `sudo` | 26 | +| `loolwsd` | 17 | +| `exportfs` | 15 | +| `dehydrated` | 6 | +| `sa-update` | 5 | +| `nslcd` | 4 | +| `rpc.idmapd` | 1 | For reference, here is the command that gives these statistics: @@ -37,6 +50,16 @@ $ bash ./extra/examples/get-systemd-stats.sh >~/systemd-units.stats.tsv One should also remember, that numeric comparison are faster that string comparison, which in turn are faster than regular expression pattern-matching. Further more, some log entries are not worth checking for, because they are too rare: it costs more to grab them with filters (that most log entries will have to pass through), than letting them get caught by the catch-all last execution chain, which typically uses the `action_dailyReport` module. -An efficient way to organize the configuration file is by handling units from the most verbose to the least verbose, and for each unit, filter-out useless entries based on the `PRIORITY` (which is an integer number) whenever it is possible. In short, filtering on the actual message, while not entirely avoidable, is the last-resort operation. +An efficient way to organize the configuration file is by handling Syslog-identifiers from the most verbose to the least verbose, and for each one, filter-out useless entries based on the `PRIORITY` (which is an integer number) whenever it is possible. +In short, filtering on the actual message, while not entirely avoidable, is the last-resort operation. + +NOTE: I used to group my log entries (and Pyruse execution chains) by `_SYSTEMD_UNIT`, which seemed logical at the time. +However, for some reason, there is some “leaking” of logs from some units to others; for example, I had Nginx logs appearing in the Exim `_SYSTEMD_UNIT`… The reason probably lies somewhere in inter-process communication, or with the launching of external commands. +Anyway, I found that grouping by `SYSLOG_IDENTIFIER` actually gives better results: + +* `SYSLOG_IDENTIFIER` names are shorter than `_SYSTEMD_UNIT` names, hence probably quicker to compare `:-p` +* Several `_SYSTEMD_UNIT` names from generic units (like `unit-name@instance-name`) end up into the same `SYSLOG_IDENTIFIER`, which allows to occasionaly replace `filter_pcre` with `filter_equals`. +* A single program often does several tasks, and `SYSLOG_IDENTIFIER` reflects this diversity, which makes writing rules much easier. +For example, Pyruse sends emails using msmtp; I do not care about `msmtp`’s logs, but I do about `pyruse`’s. Filtering-out logs from the `msmtp` `SYSLOG_IDENTIFIER` is much easier to do than getting rid of email-related logs from the `pyruse.service` systemd unit. An [example based on the above statistics](../extra/examples/full_pyruse.json) is available in the `extra/examples/` source directory. diff --git a/doc/dnat.md b/doc/dnat.md index 3fcd239..b76b38b 100644 --- a/doc/dnat.md +++ b/doc/dnat.md @@ -4,7 +4,7 @@ Pyruse provides two actions, namely `action_dnatCapture` and `action_dnatReplace`, that work together towards a single goal: giving to Pyruse’s filters and actions the illusion that client connections come directly to the examined services, instead of going through a firewall Network Address Translation or a reverse-proxy. -If for example you run a Prosody XMPP server (or anything that does not handle the [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)) behind an HAProxy reverse-proxy, or if all your network connections go through a NAT’ing proxy in your DMZ, then the following happens: the services report your proxy as being the client, ie. usually `127.0.0.1` (same machine) or your LAN’s gateway IP. +If for example you run a Prosody XMPP server (or anything that does not handle the [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)) behind an HAProxy reverse-proxy, or if all your network connections go through a NAT’ing router in your DMZ, then the following happens: the services report your proxy as being the client, ie. usually `127.0.0.1` (same machine) or your LAN’s router IP. Here is a simplified illustration of the network configuration: diff --git a/doc/install.md b/doc/install.md index 63104e8..92666a9 100644 --- a/doc/install.md +++ b/doc/install.md @@ -5,7 +5,7 @@ The software requirements are: * a modern systemd-based Linux operating system (eg. [Archlinux](https://archlinux.org/)- or [Fedora](https://getfedora.org/)-based distributions); * python, at least version 3.4 (or [more, depending on the modules](intro_tech.md) being used); * [python-systemd](https://www.freedesktop.org/software/systemd/python-systemd/journal.html); -* [nftables](http://wiki.nftables.org/) _if_ IP address bans are to be managed; +* [nftables](http://wiki.nftables.org/) or [ipset](http://ipset.netfilter.org/) _if_ IP address bans are to be managed; * a sendmail-like program _if_ emails are wanted. Besides, getting the software requires [Git](http://git-scm.com/), and packaging it requires [python-setuptools](http://pypi.python.org/pypi/setuptools). @@ -45,4 +45,6 @@ To install Pyruse on the system, run these commands as root, in the root directo The `package` line is the one that actually alters the system. Until Pyruse is packaged for your operating system, you may want to change this line to `checkinstall package`. [Checkinstall](https://en.wikipedia.org/wiki/CheckInstall) should be able to turn your Pyruse installation into a native Linux package. Then, to run Pyruse, start (and enable) `pyruse.service`. + If you use nftables bans, you should also start (and enable) `pyruse-boot@action_nftBan.service`. +Likewise, if you use ipset bans, you should start (and enable) `pyruse-boot@action_ipsetBan.service`. diff --git a/doc/intro_func.md b/doc/intro_func.md index f973975..b565360 100644 --- a/doc/intro_func.md +++ b/doc/intro_func.md @@ -2,7 +2,7 @@ Everyone knows [Fail2ban](http://www.fail2ban.org/). This program is excellent at what it does, which is matching patterns in a number of log files, and keeping track of counters in order to launch actions (ban an IP, send an email…) when thresholds are crossed. -[Fail2ban is extremely customizable](http://yalis.fr/cms/index.php/post/2014/11/02/Migrate-from-DenyHosts-to-Fail2ban)… to a point; and to my knowledge it cannot benefit from all the features of modern-day logging with systemd. +[Fail2ban is extremely customizable](http://yalis.fr/cms/index.php/post/2014/11/02/Migrate-from-DenyHosts-to-Fail2ban)… to a point; to my knowledge it cannot benefit from all the features of modern-day logging with systemd. Then, there is the less-known and aging [Epylog](http://freshmeat.sourceforge.net/projects/epylog/); several programs exist with the same features. Just like Fail2ban, this program continuously scans the logs to do pattern matching. diff --git a/doc/intro_tech.md b/doc/intro_tech.md index 905d606..bcad0ae 100644 --- a/doc/intro_tech.md +++ b/doc/intro_tech.md @@ -31,8 +31,10 @@ Pyruse is split into several Python files: * `workflow.py`: This is where the configuration file get translated into actual execution chains, linked together into a single static workflow. * `module.py`: Whenever the workflow needs to add a filter or an action to an execution chain, this module finds it in the filesystem. * `base.py`: All actions and filters inherit from the `Action` and `Filter` classes defined in there; they act as an abstraction layer between the workflow and the modules. -* `counter.py`: This utility class is parent to modules that manage a counter (Python supports multiple-inheritance). -* `email.py`: This utility class is parent to modules that send emails (Python supports multiple-inheritance). +* `ban.py`: This utility class is parent to modules that ban IP addresses using [Netfilter](https://netfilter.org/) (Python supports multiple-inheritance). +* `counter.py`: This utility class is parent to modules that manage a counter. +* `dnat.py`: This file contains utility parent classes for actions that try and restore the actual client IP addresses. +* `email.py`: This utility class is parent to modules that send emails. All else is actions and filters… Some are delivered with Pyruse itself; [more can be added](customize.md). diff --git a/doc/logandban.md b/doc/logandban.md index 5ef2f62..7411c99 100644 --- a/doc/logandban.md +++ b/doc/logandban.md @@ -30,9 +30,22 @@ Here are some examples: ## Ban IP addresses after they misbehaved Linux provides a number of firewall solutions: [iptables](http://www.netfilter.org/), its successor [nftables](http://wiki.nftables.org/), and many iptables frontends like [Shorewall](http://www.shorewall.net/) or RedHat’s [firewalld](http://www.firewalld.org/). -For Pyruse, **nftables** was chosen, because it is modern and light-weight, and provides interesting features. -Action `action_nftBan` takes three mandatory arguments: +For Pyruse, **nftables** was initially chosen, because it is modern and light-weight, and provides interesting features. Besides, only nftables is actually tested in real use. +Now, an **ipset**-based [alternative is also available](#iptables-with-ipset). + +### nftables + +Action `action_nftBan` requires that nftables is installed. +In addition, the binary to run (and parameters if needed) should be set in the configuration file; here is the default value: + +```json +"nftBan": { + "nft": [ "/usr/bin/nft" ] +} +``` + +This action takes three mandatory arguments: * `nftSetIPv4` and `nftSetIPv6` are the nftables sets where IP addresses will be added. The IP address is considered to be IPv6 if it contains at least one colon (`:`) character, else it is supposed to be IPv4. * `IP` gives the name of the entry field to read, in order to get the IP address. @@ -88,7 +101,7 @@ Here are examples: } ``` -### List the currently banned addresses +#### List the currently banned addresses To see what IP addresses are currently banned, here is the `nft` command: @@ -114,7 +127,7 @@ table ip Inet4 { _Note_: The un-rounded timeouts are post-reboot restored bans. -### Un-ban an IP address +#### Un-ban an IP address It is bound to happen some day: you will want to un-ban a banned IP address. @@ -129,7 +142,7 @@ To avoid that, also delete the corresponding record from the `action_nftBan.py.j To go further, you could tweak your configuration, so that your trusted IP addresses never reach `action_nftBan`. -### Manual ban of an IP address +#### Manual ban of an IP address To add a ban yourself, run a command like this: @@ -139,16 +152,99 @@ $ sudo nft 'add element ip Inet4 ssh_ban {192.168.1.1 timeout 5d} The `timeout …` part can be omitted to add a permanent ban. The timeout can be any combination of days (`d`), hours (`h`), minutes (`m`), and seconds (`s`), eg. “`3d31m16s`”. -In order to make the ban persistent across reboots, a corresponding record should also be appended to the `action_nftBan.py.json` file in Pyruse’s [storage directory](conffile.md) (the IP address, nft Set, days, hours, minutes, seconds, and actual path to the file should be adapted to your needs): +In order to make the ban persistent across reboots, a corresponding record should also be appended to the `action_nftBan.py.json` file in Pyruse’s [storage directory](conffile.md) (the IP address, Nftables Set, days, hours, minutes, seconds, and actual path to the file should be adapted to your needs): * either a time-limited ban: ```bash -$ sudo sed -i "\$s/.\$/$(date +', {"IP": "192.168.1.1", "nftSet": "ip Inet4 ssh_ban", "timestamp": %s.000000}' -d 'now +3day +31minute +16second')]/" /var/lib/pyruse/action_nftBan.py.json +$ sudo sed -i "\$s/.\$/$(date +', {"IP": "192.168.1.1", "nfSet": "ip Inet4 ssh_ban", "timestamp": %s.000000}' -d 'now +3day +31minute +16second')]/" /var/lib/pyruse/action_nftBan.py.json ``` * or an unlimited ban: ```bash -$ sudo sed -i '$s/.$/, {"IP": "192.168.1.1", "nftSet": "ip Inet4 ssh_ban", "timestamp": 0}]/' /var/lib/pyruse/action_nftBan.py.json +$ sudo sed -i '$s/.$/, {"IP": "192.168.1.1", "nfSet": "ip Inet4 ssh_ban", "timestamp": 0}]/' /var/lib/pyruse/action_nftBan.py.json +``` + +### iptables with ipset + +Action `action_ipsetBan` requires that ipset and iptables are installed. +In addition, the ipset binary to run (and parameters if needed) should be set in the configuration file; here is the default value: + +```json + "ipsetBan": { + "ipset": [ "/usr/bin/ipset", "-exist", "-quiet" ] + } +``` + +This action works exactly [like `action_nftBan`](#nftables), except parameters `nftSetIPv4` and `nftSetIPv6` are named `ipSetIPv4` and `ipSetIPv6` instead. + +The name of the set in the `ipSetIPv4` parameter must have been created before running Pyruse, with: + +```bash +$ sudo ipset create SET_NAME hash:ip family inet hashsize 1024 maxelem 65535 +``` + +Likewise, the set given by `ipSetIPv6` must have been created before running Pyruse, with: + +```bash +$ sudo ipset create SET_NAME hash:ip family inet6 hashsize 1024 maxelem 65535 +``` + +Here are examples of usage for this action: + +```json +{ + "action": "action_ipsetBan", + "args": { "IP": "thatIP", "banSeconds": 86400, "ipSetIPv4": "mail_ban4", "ipSetIPv6": "mail_ban6" } +} + +{ + "action": "action_ipsetBan", + "args": { "IP": "thatIP", "ipSetIPv4": "sshd_ban4", "ipSetIPv6": "sshd_ban6" } +} +``` + +#### List the currently banned addresses + +To see what IP addresses are currently banned, here is the `ipset` command: + +```bash +$ sudo ipset list mail_ban4' +``` + +#### Un-ban an IP address + +To remove an IP address from a set, here is the `ipset` command: + +```bash +$ sudo ipset del mail_ban4 10.0.0.10' +``` + +To make the change persistent across reboots, also delete the corresponding record from the `action_ipsetBan.py.json` file in Pyruse’s [storage directory](conffile.md). + +To go further, you could tweak your configuration, so that your trusted IP addresses never reach `action_ipsetBan`. + +#### Manual ban of an IP address + +To add a ban yourself, run a command like this: + +```bash +$ sudo ipset add ssh_ban4 192.168.1.1 timeout 261076 +``` + +The `timeout …` part can be omitted to add a permanent ban; otherwise it is a number of seconds. + +In order to make the ban persistent across reboots, a corresponding record should also be appended to the `action_ipsetBan.py.json` file in Pyruse’s [storage directory](conffile.md) (the IP address, Nftables Set, days, hours, minutes, seconds, and actual path to the file should be adapted to your needs): + +* either a time-limited ban: + +```bash +$ sudo sed -i "\$s/.\$/$(date +', {"IP": "192.168.1.1", "nfSet": "ssh_ban4", "timestamp": %s.000000}' -d 'now +3day +31minute +16second')]/" /var/lib/pyruse/action_ipsetBan.py.json +``` + +* or an unlimited ban: + +```bash +$ sudo sed -i '$s/.$/, {"IP": "192.168.1.1", "nfSet": "ssh_ban4", "timestamp": 0}]/' /var/lib/pyruse/action_ipsetBan.py.json ``` diff --git a/extra/examples/full_pyruse.json b/extra/examples/full_pyruse.json index 89a37c2..23aed57 100644 --- a/extra/examples/full_pyruse.json +++ b/extra/examples/full_pyruse.json @@ -7,7 +7,7 @@ }, { "filter": "filter_in", - "args": { "field": "_SYSTEMD_UNIT", "values": [ "gitea.service", "movim.service", "postgresql.service", "man-db.service", "rpc-statd.service", "rpc-statd-notify.service", "lvm2-monitor.service", "lvm2-pvscan@8:1.service", "lvm2-pvscan@179:2.service", "systemd-resolved.service", "systemd-logind.service", "nfs-server.service", "systemd-networkd.service", "systemd-journald.service", "dbus.service", "nfs-idmapd.service", "slapd.service", "systemd-udevd.service" ] }, + "args": { "field": "SYSLOG_IDENTIFIER", "values": [ "exportfs", "gitea", "haproxy", "kill", "ldapsearch", "ldapwhoami", "mandb", "mount.davfs", "movim", "msmtp", "postgres", "prosody_auth", "sa-compile", "systemd-fsck", "systemd-gpt-auto-generator", "systemd-logind" ] }, "then": "… NOOP" } ], @@ -18,131 +18,23 @@ }, { "filter": "filter_pcreAny", - "args": { "field": "_SYSTEMD_UNIT", "re": [ "^systemd-fsck@" ] }, + "args": { "field": "SYSLOG_IDENTIFIER", "re": [ "^ansible-" ] }, "then": "… NOOP" } ], - "Notify of unsecured XMPP servers": [ - { - "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "prosody.service" } - }, - { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "->(.*) closed: Encrypted server-to-server communication is required but was not offered$", "save": [ "xmppServer" ] }, - "else": "… NOOP if PRIORITY 3+" - }, - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "XMPP server {xmppServer} did not provide a secure connection" } - } - ], "Detect request errors with Nextcloud": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "uwsgi@nextcloud.service" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "uwsgi" } }, { "filter": "filter_pcre", "args": { "field": "MESSAGE", "re": "^\\[[^]]+\\] ([^ ]+) .*\\] ([A-Z]+ /[^?]*)(?:\\?.*)? => .*\\(HTTP/1.1 5..\\)", "save": [ "thatIP", "HTTPrequest" ] }, - "else": "… Discard Nextcloud coding errors" - }, - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "IP {thatIP} failed to {HTTPrequest} on Nextcloud" } - } - ], - "… Discard Nextcloud coding errors": [ - { - "filter": "filter_in", - "args": { "field": "PRIORITY", "values": [ 2, 3 ] }, - "then": "… NOOP", - "else": "… Discard Nextcloud-to-LDAP bind errors" - } - ], - "… Discard Nextcloud-to-LDAP bind errors": [ - { - "filter": "filter_equals", - "args": { "field": "MESSAGE", "value": "{user_ldap} Bind failed: 49: Invalid credentials" }, - "then": "… NOOP", - "else": "… Detect Nextcloud failed logins" - } - ], - "… Detect Nextcloud failed logins": [ - { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^\\{core\\} Login failed: '(.*)' \\(Remote IP: '(.*)'\\)", "save": [ "thatUser", "thatIP" ] }, - "else": "… Let Nextcloud core messages pass-through" - }, - { - "filter": "filter_userExists", - "args": { "field": "thatUser" }, - "else": "… Report inexisting Nextcloud user" - }, - { - "action": "action_email", - "args": { "subject": "Pyruse Warning", "message": "WARNING: Failed login as {thatUser}@{_HOSTNAME} on Nextcloud on {__REALTIME_TIMESTAMP}." } - }, - { - "action": "action_dailyReport", - "args": { "level": "WARN", "message": "Failed login as {thatUser}@{_HOSTNAME} on Nextcloud" }, - "then": "… Do not ban local Nextcloud users" - } - ], - "… Report inexisting Nextcloud user": [ - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Failed login as {thatUser}@{_HOSTNAME} on Nextcloud" }, - "then": "… Do not ban local Nextcloud users" - } - ], - "… Do not ban local Nextcloud users": [ - { - "action": "filter_inNetworks", - "args": { "field": "thatIP", "nets": [ "192.168.1.0/24", "fd00::/8" ] }, - "then": "… NOOP", - "else": "… Detect repeated Nextcloud login failures" - } - ], - "… Detect repeated Nextcloud login failures": [ - { - "action": "action_counterRaise", - "args": { "counter": "https", "for": "thatIP", "keepSeconds": 300, "save": "IPfailures" } - }, - { - "filter": "filter_greaterOrEquals", - "args": { "field": "IPfailures", "value": 6 }, - "else": "… NOOP" - }, - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Ban of IP {thatIP} for HTTP abuse" } - }, - { - "action": "action_nftBan", - "args": { "IP": "thatIP", "banSeconds": 900, "nftSetIPv4": "ip Inet4 https_ban", "nftSetIPv6": "ip6 Inet6 https_ban" } - } - ], - "… Let Nextcloud core messages pass-through": [ - { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^\\{" }, - "else": "… Report Nextcloud failed state" - } - ], - "… Report Nextcloud failed state": [ - { - "filter": "filter_equals", - "args": { "field": "MESSAGE", "value": "uwsgi@nextcloud.service: Unit entered failed state." }, "else": "… Report insufficient buffer-size for Nextcloud QUERY_STRING" }, - { - "action": "action_email", - "args": { "subject": "Nextcloud crashed", "message": "Service uwsgi@nextcloud.service failed on {_HOSTNAME} on {__REALTIME_TIMESTAMP}." } - }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Service uwsgi@nextcloud.service failed on {_HOSTNAME}" } + "args": { "level": "INFO", "message": "IP {thatIP} failed to {HTTPrequest} on Nextcloud", "details": "FIRSTLAST" } } ], "… Report insufficient buffer-size for Nextcloud QUERY_STRING": [ @@ -156,78 +48,62 @@ "args": { "level": "WARN", "message": "Nextcloud query failed because the buffer-size was too low" } } ], - "Warn of sudo errors": [ + "Detect successful XMPP logins": [ { - "filter": "filter_pcre", - "args": { "field": "_SYSTEMD_UNIT", "re": "^session-.*\\.scope$" } + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "prosody" } }, { "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^ (.*) : user NOT in sudoers ;", "save": [ "thatUser" ] }, - "else": "… Warn of su errors" - }, - { - "action": "action_email", - "args": { "subject": "SUDO error!", "message": "Sudo error from user {thatUser} on {_HOSTNAME} on {__REALTIME_TIMESTAMP}." } + "args": { "field": "MESSAGE", "re": "Authenticated as (.*)$", "save": [ "JID" ] }, + "else": "… Notify of unsecured XMPP servers" }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Sudo error from user {thatUser} on {_HOSTNAME}" } + "args": { "level": "INFO", "message": "Login as {JID} by XMPP", "details": "NONE" } } ], - "… Warn of su errors": [ + "… Notify of unsecured XMPP servers": [ { "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^FAILED SU \\([^)]+\\) (.*) on [^ ]+$", "save": [ "thatUser" ] }, - "else": "… Notify of su logins" - }, - { - "action": "action_email", - "args": { "subject": "SU error!", "message": "SU error from user {thatUser} on {_HOSTNAME} on {__REALTIME_TIMESTAMP}." } - }, - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "SU error from user {thatUser} on {_HOSTNAME}" } - } - ], - "… Notify of su logins": [ - { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^\\(to (.*)\\) (.*) on [^ ]+$", "save": [ "thatUser", "fromUser" ] }, - "else": "… Notify of sudo logins" - }, - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Login as {thatUser}@{_HOSTNAME} by {fromUser}:su" } - } - ], - "… Notify of sudo logins": [ - { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^pam_unix\\(sudo:session\\): session opened for user (.*) by [^(]*\\(uid=([^)]+)\\)$", "save": [ "thatUser", "fromUID" ] }, - "else": "… Notify of Nextcloud upgrades" - }, - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Login as {thatUser}@{_HOSTNAME} by {fromUID}:sudo" } - } - ], - "… Notify of Nextcloud upgrades": [ - { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^\\{core\\} starting upgrade from (.*) to (.*)$", "save": [ "fromVers", "toVers" ] }, + "args": { "field": "MESSAGE", "re": "->(.*) closed: Encrypted server-to-server communication is required but was not offered$", "save": [ "xmppServer" ] }, "else": "… NOOP if PRIORITY 3+" }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Nextcloud upgrade from {fromVers} to {toVers}" } + "args": { "level": "INFO", "message": "XMPP server {xmppServer} did not provide a secure connection" } } ], - "Discard HTTP debug entries": [ + "Notify of unexpected HTTP disconnections": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "nginx.service" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "nginx" } }, + { + "filter": "filter_pcreAny", + "args": { "field": "MESSAGE", "re": [ + "epoll_wait\\(\\) reported that client prematurely closed connection, so upstream connection is closed too while sending request to upstream, client: (?P[^,]+), server: , request: \"[^ ]+ (?P/[^/ \"]*)[^\"]*\"", + "client prematurely closed connection while sending to client, client: (?P[^,u][^,]+), server: , request: \"[^ ]+ (?P/[^/ \"]*)[^\"]*\"" + ] }, + "else": "… Warn of CONNECT attempts" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "Aborted connection from {thatIP} to {urlPrefix}", "details": "FIRSTLAST" } + } + ], + "… Warn of CONNECT attempts": [ + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": ", request: \"CONNECT [^ ]+ HTTP/[^\"]+\"$" }, + "else": "… Discard other HTTP debug entries" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "Nginx detected a CONNECT attempt on {_HOSTNAME}" } + } + ], + "… Discard other HTTP debug entries": [ { "filter": "filter_greaterOrEquals", "args": { "field": "PRIORITY", "value": 6 }, @@ -245,21 +121,25 @@ "action": "action_counterReset", "args": { "counter": "https", "for": "thatIP", "graceSeconds": 432000 } }, + { + "action": "action_counterReset", + "args": { "counter": "https_recidive", "for": "thatIP" } + }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Login as {thatUser}@{_HOSTNAME} by HTTPS" } + "args": { "level": "INFO", "message": "Login from {thatIP} as {thatUser}@{_HOSTNAME} by HTTPS", "details": "NONE" } } ], "… Detect failed HTTPS logins": [ { "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "Redirect to: https://[^/]*yalis\\.fr/sso/\\?r=(.*), client: (?P.*), server: , request: \"POST /sso/\\?r=\\1 HTTP/1\\.1\", host: \"[^/]*yalis\\.fr\", referrer: \"https://[^/]*yalis\\.fr/sso/\\?r=\\1\"$" }, + "args": { "field": "MESSAGE", "re": "Redirect to: https://[^/]*/sso/\\?r=(.*), client: (?P.*), server: , request: \"POST /sso/\\?r=\\1 HTTP/1\\.1\", host: \"[^/]*\", referrer: \"https://[^/]*/sso/\\?r=\\1\"$" }, "else": "… Detect abnormal HTTP 404 errors" }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Failed login on {_HOSTNAME} by HTTPS" }, - "then": "… Do not ban local HTTP users" + "args": { "level": "INFO", "message": "Failed login from {thatIP} on {_HOSTNAME} by HTTPS", "details": "FIRSTLAST" }, + "then": "… Check network before an HTTPS ban" } ], "… Detect abnormal HTTP 404 errors": [ @@ -269,14 +149,14 @@ "open\\(\\) \"[^\"]*\\.(?:cgi|php|pl|py|sh)\" failed \\(2: No such file or directory\\), client: (?P[^,]+),", "Unable to open primary script: .*\\.(?:cgi|php|pl|py|sh) \\(No such file or directory[^,]+, client: (?P[^,]+)," ] }, - "then": "… Do not ban local HTTP users", + "then": "… Check network before an HTTPS ban", "else": "… Immediate warning for connectivity errors" } ], - "… Do not ban local HTTP users": [ + "… Check network before an HTTPS ban": [ { - "action": "filter_inNetworks", - "args": { "field": "thatIP", "nets": [ "192.168.1.0/24", "fd00::/8" ] }, + "filter": "filter_inNetworks", + "args": { "field": "thatIP", "nets": [ "192.168.1.96/29", "127.0.0.0/8", "::1" ] }, "then": "… NOOP", "else": "… Detect repeated HTTPS failures" } @@ -295,6 +175,10 @@ "action": "action_dailyReport", "args": { "level": "INFO", "message": "Ban of IP {thatIP} for HTTP abuse" } }, + { + "action": "action_log", + "args": { "message": "nftBan from HTTP for {thatIP}" } + }, { "action": "action_nftBan", "args": { "IP": "thatIP", "banSeconds": 7200, "nftSetIPv4": "ip Inet4 https_ban", "nftSetIPv6": "ip6 Inet6 https_ban" } @@ -312,7 +196,7 @@ }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Nginx could not connect to {nginxUpstream}" } + "args": { "level": "INFO", "message": "Nginx could not connect to {nginxUpstream}", "details": "FIRSTLAST" } } ], "… Immediate warning for module version errors": [ @@ -327,7 +211,7 @@ }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Nginx could not load module {badModule}" } + "args": { "level": "INFO", "message": "Nginx could not load module {badModule}", "details": "FIRSTLAST" } } ], "… Immediate warning for LUA errors": [ @@ -342,7 +226,7 @@ }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Nginx file {luaFile} ran into error: {luaError}" } + "args": { "level": "INFO", "message": "Nginx file {luaFile} ran into error: {luaError}", "details": "FIRSTLAST" } } ], "… Warn of upstream HTTP disconnections": [ @@ -353,13 +237,13 @@ }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "Nginx got disconnected from {failedUpstream} on request {failedRequest}" } + "args": { "level": "WARN", "message": "Nginx got disconnected from {failedUpstream} on request {failedRequest}", "details": "FIRSTLAST" } } ], "Detect successful IMAP logins": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "dovecot.service" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "dovecot" } }, { "filter": "filter_pcre", @@ -370,9 +254,13 @@ "action": "action_counterReset", "args": { "counter": "mail", "for": "thatIP", "graceSeconds": 432000 } }, + { + "action": "action_counterReset", + "args": { "counter": "mail_recidive", "for": "thatIP" } + }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Login as {thatUser}@{_HOSTNAME} by IMAP" } + "args": { "level": "INFO", "message": "Login from {thatIP} as {thatUser}@{_HOSTNAME} by IMAP", "details": "NONE" } } ], "… Detect IMAP resource hogs": [ @@ -382,7 +270,7 @@ "^imap-login: Disconnected \\(no auth attempts in [0-9]{2,} secs\\): user=<>, rip=(?P[^,]+),", "^imap-login: Disconnected: Too many invalid commands.*, rip=(?P[^,]+)," ] }, - "then": "… Do not ban local mail users", + "then": "… Check network before an email ban", "else": "… Detect failed IMAP logins" } ], @@ -399,25 +287,25 @@ }, { "action": "action_email", - "args": { "subject": "Pyruse Warning", "message": "WARNING: Failed login as {thatUser}@{_HOSTNAME} by IMAP on {__REALTIME_TIMESTAMP}." } + "args": { "subject": "Pyruse Warning", "message": "WARNING: Failed login from {thatIP} as {thatUser}@{_HOSTNAME} by IMAP on {__REALTIME_TIMESTAMP}." } }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "Failed login as {thatUser}@{_HOSTNAME} by IMAP" }, - "then": "… Do not ban local mail users" + "args": { "level": "WARN", "message": "Failed login from {thatIP} as {thatUser}@{_HOSTNAME} by IMAP", "details": "FIRSTLAST" }, + "then": "… Check network before an email ban" } ], "… Report inexisting IMAP user": [ { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Failed login as {thatUser}@{_HOSTNAME} by IMAP" }, - "then": "… Do not ban local mail users" + "args": { "level": "INFO", "message": "Failed login from {thatIP} as {thatUser}@{_HOSTNAME} by IMAP", "details": "FIRSTLAST" }, + "then": "… Check network before an email ban" } ], - "… Do not ban local mail users": [ + "… Check network before an email ban": [ { - "action": "filter_inNetworks", - "args": { "field": "thatIP", "nets": [ "192.168.1.0/24", "fd00::/8" ] }, + "filter": "filter_inNetworks", + "args": { "field": "thatIP", "nets": [ "192.168.1.96/29", "127.0.0.0/8", "::1" ] }, "then": "… NOOP", "else": "… Detect repeated mail failures" } @@ -436,6 +324,10 @@ "action": "action_dailyReport", "args": { "level": "INFO", "message": "Ban of IP {thatIP} for mail abuse" } }, + { + "action": "action_log", + "args": { "message": "nftBan from email for {thatIP}" } + }, { "action": "action_nftBan", "args": { "IP": "thatIP", "banSeconds": 432000, "nftSetIPv4": "ip Inet4 mail_ban", "nftSetIPv6": "ip6 Inet6 mail_ban" } @@ -457,26 +349,37 @@ }, { "action": "action_email", - "args": { "subject": "Dovecot-to-LDAP error", "message": "Dovecot could connect to LDAP (ldapi) on {__REALTIME_TIMESTAMP}." } + "args": { "subject": "Dovecot-to-LDAP error", "message": "Dovecot could not connect to LDAP (ldapi) on {__REALTIME_TIMESTAMP}." } }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Dovecot could connect to LDAP (ldapi)" } + "args": { "level": "INFO", "message": "Dovecot could not connect to LDAP (ldapi)", "details": "FIRSTLAST" } } ], "Notify of Exim smarthost deliveries": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "exim.service" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "exim" } }, { "filter": "filter_pcre", "args": { "field": "MESSAGE", "re": " => [^ ]+ R=smarthost T=remote_smtp H=([^ ]+ \\[[^]]+\\]) C=\"250 ", "save": [ "smarthost" ] }, + "else": "… Notify of Exim local deliveries" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "Email message sent through {smarthost}", "details": "NONE" } + } + ], + "… Notify of Exim local deliveries": [ + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "> [^ ]+ Saved\"$" }, "else": "… Frozen Exim email" }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Email message sent through {smarthost}" } + "args": { "level": "INFO", "message": "Local email message delivered", "details": "NONE" } } ], "… Frozen Exim email": [ @@ -487,7 +390,7 @@ }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Frozen email on {_HOSTNAME}." } + "args": { "level": "INFO", "message": "Frozen email on {_HOSTNAME}.", "details": "FIRSTLAST" } } ], "… Warn of a failure for Exim": [ @@ -505,24 +408,39 @@ }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Exim detected a failure ({failReason})" } + "args": { "level": "INFO", "message": "Exim detected a failure ({failReason})", "details": "FIRSTLAST" } } ], "… Immediate ban of crackers": [ { "filter": "filter_pcre", "args": { "field": "MESSAGE", "re": "\\[([^ ]+)\\] NULL character\\(s\\) present \\(shown as '\\?'\\)$", "save": [ "thatIP" ] }, - "else": "… Detect some SMTP spammers" + "else": "… Some leniency to allow for manual SMTP" }, { "action": "action_dailyReport", "args": { "level": "INFO", "message": "Ban of IP {thatIP} for mail abuse" } }, + { + "action": "action_log", + "args": { "message": "nftBan from email for {thatIP}" } + }, { "action": "action_nftBan", "args": { "IP": "thatIP", "banSeconds": 432000, "nftSetIPv4": "ip Inet4 mail_ban", "nftSetIPv6": "ip6 Inet6 mail_ban" } } ], + "… Some leniency to allow for manual SMTP": [ + { + "filter": "filter_pcreAny", + "args": { "field": "MESSAGE", "re": [ + "^.{19} SMTP syntax error in \"[^\"]*\" H=(?:\\([^)]*\\) )?\\[(?P[^]]+)\\]", + "SMTP command timeout on connection from (?:\\([^)]*\\) )?\\[(?P[^]]+)\\]$" + ] }, + "then": "… Check network before an email ban", + "else": "… Detect some SMTP spammers" + } + ], "… Detect some SMTP spammers": [ { "filter": "filter_pcreAny", @@ -538,14 +456,14 @@ "^.{19} rejected [EH]{2}LO from (?:\\([^)]*\\) )?\\[(?P[^]]+)\\]: syntactically invalid", "\\[(?P[^ ]+)\\] dropped: too many nonmail commands" ] }, - "then": "… Do not ban local mail users", + "then": "… Check network before an email ban", "else": "… NOOP if PRIORITY 5+" } ], "Notify of new custom systemd services": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "init.scope" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "systemd" } }, { "filter": "filter_pcre", @@ -561,36 +479,58 @@ { "filter": "filter_pcre", "args": { "field": "MESSAGE", "re": "^Directory (/.*) to mount over is not empty, mounting anyway\\.$", "save": [ "mountPath" ] }, - "else": "… Warn of time-outs" + "else": "… Notify of systemd-gpt-auto-generator errors" }, { "action": "action_dailyReport", "args": { "level": "WARN", "message": "Device mounted on non-empty {mountPath}" } } ], + "… Notify of systemd-gpt-auto-generator errors": [ + { + "filter": "filter_equals", + "args": { "field": "MESSAGE", "value": "/usr/lib/systemd/system-generators/systemd-gpt-auto-generator failed with error code 1." }, + "else": "… Warn of time-outs" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "{MESSAGE}", "details": "NONE" } + } + ], "… Warn of time-outs": [ { "filter": "filter_pcre", "args": { "field": "MESSAGE", "re": "^(/.*): Start operation timed out\\. Terminating\\.$", "save": [ "systemdUnit" ] }, - "else": "… Warn of failed mounts" + "else": "… Notify of user logins" }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "Unit {systemdUnit} timed out while starting" } + "args": { "level": "WARN", "message": "Unit {systemdUnit}/{_HOSTNAME} timed out while starting" } } ], - "… Warn of failed mounts": [ + "… Notify of user logins": [ { "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^(/.*\\.mount): Failed ", "save": [ "mountUnit" ] }, - "else": "… Discard other init.scope debug entries" + "args": { "field": "MESSAGE", "re": "^pam_unix\\(systemd-user:session\\): session opened for user (.*) by root\\(uid=0\\)$", "save": [ "thatUser" ] }, + "else": "… Warn of failed systemd units" }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "Unit {mountUnit} failed to mount" } + "args": { "level": "INFO", "message": "Login as {thatUser}@{_HOSTNAME} by systemd-user:session" } } ], - "… Discard other init.scope debug entries": [ + "… Warn of failed systemd units": [ + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^(/.*\\.mount|.*\\.service): Failed ", "save": [ "systemdUnit" ] }, + "else": "… Discard other systemd debug entries" + }, + { + "action": "action_dailyReport", + "args": { "level": "WARN", "message": "Unit {systemdUnit}/{_HOSTNAME} failed" } + } + ], + "… Discard other systemd debug entries": [ { "filter": "filter_greaterOrEquals", "args": { "field": "PRIORITY", "value": 4 }, @@ -608,107 +548,137 @@ "args": { "level": "INFO", "message": "{MESSAGE}" } } ], - "Warn of Nextcloud maintenance errors": [ + "Warn of su errors": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "nextcloud-maintenance.service" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "su" } }, { - "filter": "filter_equals", - "args": { "field": "MESSAGE", "value": "Cannot write into \"config\" directory!" }, - "else": "… NOOP if PRIORITY 5+" + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^FAILED SU \\([^)]+\\) (.*) on [^ ]+$", "save": [ "thatUser" ] }, + "else": "… Notify of su logins" }, { "action": "action_email", - "args": { "subject": "Nextcloud config is read-only!", "message": "Nextcloud maintenance could not write to the configuration file on {__REALTIME_TIMESTAMP}." } + "args": { "subject": "SU error!", "message": "SU error from user {thatUser} on {_HOSTNAME} on {__REALTIME_TIMESTAMP}." } }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Nextcloud maintenance could not write to the configuration file" } + "args": { "level": "INFO", "message": "SU error from user {thatUser} on {_HOSTNAME}" } } ], - "Detect HAProxy problems": [ + "… Notify of su logins": [ + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^\\(to (.*)\\) (.*) on [^ ]+$", "save": [ "thatUser", "fromUser" ] }, + "else": "… NOOP if PRIORITY 5+" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "Login as {thatUser}@{_HOSTNAME} by {fromUser}:su", "details": "NONE" } + } + ], + "Discard Nextcloud coding errors": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "haproxy.service" }, - "then": "… NOOP if PRIORITY 5+" - } - ], - "Notify of user logins": [ - { - "filter": "filter_pcre", - "args": { "field": "_SYSTEMD_UNIT", "re": "^user@" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "ownCloud" } }, { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "session opened for user (.*) by root\\(uid=0\\)$", "save": [ "thatUser" ] }, - "else": "… NOOP if PRIORITY 4+" - }, - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Login as {thatUser}@{_HOSTNAME} by systemd-user:session" } + "filter": "filter_in", + "args": { "field": "PRIORITY", "values": [ 2, 3 ] }, + "then": "… NOOP", + "else": "… Discard Nextcloud-to-LDAP bind errors" } ], - "Warn of minidlna errors while reading media files": [ + "… Discard Nextcloud-to-LDAP bind errors": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "minidlna.service" } - }, + "args": { "field": "MESSAGE", "value": "{user_ldap} Bind failed: 49: Invalid credentials" }, + "then": "… NOOP", + "else": "… Notify of Nextcloud upgrades" + } + ], + "… Notify of Nextcloud upgrades": [ { "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^metadata\\.c:.*Opening (.*) failed! \\[", "save": [ "torrentName" ] }, - "else": "… Notify of unhandled formats" + "args": { "field": "MESSAGE", "re": "^\\{core\\} starting upgrade from (.*) to (.*)$", "save": [ "fromVers", "toVers" ] }, + "else": "… Detect Nextcloud failed logins" }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "Minidlna error for {torrentName}" } + "args": { "level": "INFO", "message": "Nextcloud upgrade from {fromVers} to {toVers}" } } ], - "… Notify of unhandled formats": [ + "… Detect Nextcloud failed logins": [ { "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^metadata\\.c:[0-9]+: warn: (.*): Unhandled format: (.*)$", "save": [ "torrentName", "mediaFormat" ] }, - "else": "… Warn of permission errors for minidlna" + "args": { "field": "MESSAGE", "re": "^\\{core\\} Login failed: '(.*)' \\(Remote IP: '(.*)'\\)", "save": [ "thatUser", "thatIP" ] }, + "else": "… Let Nextcloud core messages pass-through" + }, + { + "filter": "filter_userExists", + "args": { "field": "thatUser" }, + "else": "… Report inexisting Nextcloud user" + }, + { + "action": "action_email", + "args": { "subject": "Pyruse Warning", "message": "WARNING: Failed login from {thatIP} as {thatUser}@{_HOSTNAME} on Nextcloud on {__REALTIME_TIMESTAMP}." } }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Minidlna does not handle {mediaFormat} for {torrentName}" } + "args": { "level": "WARN", "message": "Failed login from {thatIP} as {thatUser}@{_HOSTNAME} on Nextcloud", "details": "FIRSTLAST" }, + "then": "… Check network before a Nextcloud ban" } ], - "… Warn of permission errors for minidlna": [ - { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^monitor\\.c:[0-9]+: error: inotify_add_watch\\((.*)\\) \\[Permission non accordée\\]$", "save": [ "torrentName" ] }, - "else": "… NOOP if PRIORITY 4+" - }, + "… Report inexisting Nextcloud user": [ { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "Minidlna is not allowed to read {torrentName}" } + "args": { "level": "INFO", "message": "Failed login from {thatIP} as {thatUser}@{_HOSTNAME} on Nextcloud", "details": "FIRSTLAST" }, + "then": "… Check network before a Nextcloud ban" } ], - "Warn of package errors with loolwsd": [ + "… Check network before a Nextcloud ban": [ { - "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "loolwsd.service" } + "filter": "filter_inNetworks", + "args": { "field": "thatIP", "nets": [ "192.168.1.96/29", "127.0.0.0/8", "::1" ] }, + "then": "… NOOP", + "else": "… Detect repeated Nextcloud login failures" + } + ], + "… Detect repeated Nextcloud login failures": [ + { + "action": "action_counterRaise", + "args": { "counter": "https", "for": "thatIP", "keepSeconds": 300, "save": "IPfailures" } }, { - "filter": "filter_pcreAny", - "args": { "field": "MESSAGE", "re": [ - "^/usr/bin/loolwsd: error ", - "^FATAL:", - "^Failed " - ] }, + "filter": "filter_greaterOrEquals", + "args": { "field": "IPfailures", "value": 6 }, "else": "… NOOP" }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "CollaboraOnline: {MESSAGE}" } + "args": { "level": "INFO", "message": "Ban of IP {thatIP} for HTTP abuse" } + }, + { + "action": "action_log", + "args": { "message": "nftBan from HTTP for {thatIP}" } + }, + { + "action": "action_nftBan", + "args": { "IP": "thatIP", "banSeconds": 900, "nftSetIPv4": "ip Inet4 https_ban", "nftSetIPv6": "ip6 Inet6 https_ban" } + } + ], + "… Let Nextcloud core messages pass-through": [ + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^\\{" }, + "else": "… NOOP if PRIORITY 5+" } ], "Warn of bad SSH configuration": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "sshd.service" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "sshd" } }, { "filter": "filter_pcre", @@ -717,22 +687,26 @@ }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "SSH: {MESSAGE}" } + "args": { "level": "WARN", "message": "SSH: {MESSAGE}", "details": "FIRSTLAST" } } ], "… Detect successful SSH logins": [ { "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^Accepted (?:password|publickey) for (.*) from ([^ ]*) port ", "save": [ "thatUser", "thatIP" ] }, + "args": { "field": "MESSAGE", "re": "^Accepted (password|publickey) for (.*) from ([^ ]*) port ", "save": [ "SSHmethod", "thatUser", "thatIP" ] }, "else": "… Detect failed SSH logins" }, { "action": "action_counterReset", "args": { "counter": "sshd", "for": "thatIP", "graceSeconds": 432000 } }, + { + "action": "action_counterReset", + "args": { "counter": "sshd_recidive", "for": "thatIP" } + }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Login as {thatUser}@{_HOSTNAME} by SSH" } + "args": { "level": "INFO", "message": "Login from {thatIP} as {thatUser}@{_HOSTNAME} by SSH {SSHmethod}" } } ], "… Detect failed SSH logins": [ @@ -752,33 +726,33 @@ }, { "action": "action_email", - "args": { "subject": "Pyruse Warning", "message": "WARNING: Failed login as {thatUser}@{_HOSTNAME} by SSH on {__REALTIME_TIMESTAMP}." } + "args": { "subject": "Pyruse Warning", "message": "WARNING: Failed login from {thatIP} as {thatUser}@{_HOSTNAME} by SSH on {__REALTIME_TIMESTAMP}." } }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "Failed login as {thatUser}@{_HOSTNAME} by SSH" }, - "then": "… Do not ban local SSH users" + "args": { "level": "WARN", "message": "Failed login from {thatIP} as {thatUser}@{_HOSTNAME} by SSH", "details": "FIRSTLAST" }, + "then": "… Check network before an SSH ban" } ], "… Report inexisting SSH user": [ { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Failed login as {thatUser}@{_HOSTNAME} by SSH" }, - "then": "… Do not ban local SSH users" + "args": { "level": "INFO", "message": "Failed login from {thatIP} as {thatUser}@{_HOSTNAME} by SSH", "details": "FIRSTLAST" }, + "then": "… Check network before an SSH ban" } ], "… Forbid antiquated clients": [ { "filter": "filter_pcre", "args": { "field": "MESSAGE", "re": "^Unable to negotiate with ([^ ]*) port", "save": [ "thatIP" ] }, - "then": "… Do not ban local SSH users", + "then": "… Check network before an SSH ban", "else": "… NOOP if PRIORITY 6+" } ], - "… Do not ban local SSH users": [ + "… Check network before an SSH ban": [ { - "action": "filter_inNetworks", - "args": { "field": "thatIP", "nets": [ "192.168.1.0/24", "fd00::/8" ] }, + "filter": "filter_inNetworks", + "args": { "field": "thatIP", "nets": [ "192.168.1.96/29", "127.0.0.0/8", "::1" ] }, "then": "… NOOP", "else": "… Detect repeated SSH login failures" } @@ -797,67 +771,222 @@ "action": "action_dailyReport", "args": { "level": "INFO", "message": "Ban of IP {thatIP} for SSH abuse" } }, + { + "action": "action_log", + "args": { "message": "nftBan from SSH for {thatIP}" } + }, { "action": "action_nftBan", "args": { "IP": "thatIP", "banSeconds": 432000, "nftSetIPv4": "ip Inet4 sshd_ban", "nftSetIPv6": "ip6 Inet6 sshd_ban" } } ], - "Warn of SpamAssassin update failures": [ + "Notify of identified SPAM messages": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "spamassassin-update.service" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "spamd" } }, { - "filter": "filter_equals", - "args": { "field": "MESSAGE", "value": "channel: could not find working mirror, channel failed" }, + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^spamd: identified spam" }, "else": "… NOOP if PRIORITY 4+" }, { "action": "action_dailyReport", - "args": { "level": "INFO", "message": "SpamAssassin update failed" } + "args": { "level": "INFO", "message": "Spam identified", "details": "NONE" } } ], - "Warn of systemd-nspawn failures": [ + "Manage recidives for Pyruse bans from HTTPS": [ { - "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "systemd-nspawn@seuil3.service" } + "filter": "filter_in", + "args": { "field": "SYSLOG_IDENTIFIER", "values": [ "python", "pyruse" ] } }, { "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^(?:\\[FAILED\\] )?Failed to" }, + "args": { "field": "MESSAGE", "re": "^nftBan from HTTP for (?P.*)$" }, + "else": "… Manage recidives for Pyruse bans from email" + }, + { + "action": "action_counterRaise", + "args": { "counter": "https_recidive", "for": "thatIP", "keepSeconds": 432000, "save": "IPfailures" } + }, + { + "filter": "filter_greaterOrEquals", + "args": { "field": "IPfailures", "value": 7 }, + "else": "… NOOP" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "Ban of IP {thatIP} for repeated HTTP abuse" } + }, + { + "action": "action_log", + "args": { "message": "Strong nftBan from HTTP for {thatIP}", "level": "WARNING" } + }, + { + "action": "action_nftBan", + "args": { "IP": "thatIP", "banSeconds": 2592000, "nftSetIPv4": "ip Inet4 https_ban", "nftSetIPv6": "ip6 Inet6 https_ban" } + } + ], + "… Manage recidives for Pyruse bans from email": [ + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^nftBan from email for (?P.*)$" }, + "else": "… Manage recidives for Pyruse bans from SSH" + }, + { + "action": "action_counterRaise", + "args": { "counter": "mail_recidive", "for": "thatIP", "keepSeconds": 2592000, "save": "IPfailures" } + }, + { + "filter": "filter_greaterOrEquals", + "args": { "field": "IPfailures", "value": 7 }, + "else": "… NOOP" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "Ban of IP {thatIP} for repeated mail abuse" } + }, + { + "action": "action_log", + "args": { "message": "Strong nftBan from email for {thatIP}", "level": "WARNING" } + }, + { + "action": "action_nftBan", + "args": { "IP": "thatIP", "banSeconds": 2592000, "nftSetIPv4": "ip Inet4 mail_ban", "nftSetIPv6": "ip6 Inet6 mail_ban" } + } + ], + "… Manage recidives for Pyruse bans from SSH": [ + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^nftBan from SSH for (?P.*)$" } + }, + { + "action": "action_counterRaise", + "args": { "counter": "sshd_recidive", "for": "thatIP", "keepSeconds": 2592000, "save": "IPfailures" } + }, + { + "filter": "filter_greaterOrEquals", + "args": { "field": "IPfailures", "value": 2 }, + "else": "… NOOP" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "Ban of IP {thatIP} for repeated SSH abuse" } + }, + { + "action": "action_log", + "args": { "message": "Strong nftBan from SSH for {thatIP}", "level": "WARNING" } + }, + { + "action": "action_nftBan", + "args": { "IP": "thatIP", "banSeconds": 2592000, "nftSetIPv4": "ip Inet4 sshd_ban", "nftSetIPv6": "ip6 Inet6 sshd_ban" } + } + ], + "Notify of NFS access by clients": [ + { + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "rpc.mountd" } + }, + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^authenticated mount request from (.*):[0-9]+ for (.*) \\(.*\\)$", "save": [ "nfsClient", "nfsPath" ] }, + "else": "… NOOP if PRIORITY 5+" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "NFS access from {nfsClient} to {nfsPath}", "details": "NONE" } + } + ], + "Warn of minidlna errors while reading media files": [ + { + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "minidlnad" } + }, + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^metadata\\.c:.*Opening (.*) failed! \\[", "save": [ "fileName" ] }, + "else": "… Notify of unhandled formats" + }, + { + "action": "action_dailyReport", + "args": { "level": "WARN", "message": "Minidlna error for {fileName}" } + } + ], + "… Notify of unhandled formats": [ + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^metadata\\.c:[0-9]+: warn: (.*): Unhandled format: (.*)$", "save": [ "fileName", "mediaFormat" ] }, + "else": "… Warn of permission errors for minidlna" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "Minidlna does not handle {mediaFormat} for {fileName}", "details": "NONE" } + } + ], + "… Warn of permission errors for minidlna": [ + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^monitor\\.c:[0-9]+: error: inotify_add_watch\\((.*)\\) \\[Permission non accordée\\]$", "save": [ "fileName" ] }, "else": "… NOOP if PRIORITY 4+" }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "seuil3: {MESSAGE}" } + "args": { "level": "WARN", "message": "Minidlna is not allowed to read {fileName}", "details": "FIRSTLAST" } } ], - "Warn of local authentication errors": [ + "Warn of sudo errors": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "nslcd.service" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "sudo" } }, { "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^\\[[^]]+\\] <([^>]+)> .*Can't contact LDAP server: (.*)$", "save": [ "nslcdClient", "nslcdError" ] }, - "else": "… NOOP if PRIORITY 3+" + "args": { "field": "MESSAGE", "re": "^ (.*) : user NOT in sudoers ;", "save": [ "thatUser" ] }, + "else": "… Notify of sudo logins" + }, + { + "action": "action_email", + "args": { "subject": "SUDO error!", "message": "Sudo error from user {thatUser} on {_HOSTNAME} on {__REALTIME_TIMESTAMP}." } }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "nslcd: {nslcdError} for {nslcdClient}@{_HOSTNAME}" } + "args": { "level": "INFO", "message": "Sudo error from user {thatUser} on {_HOSTNAME}" } } ], - "Discard useless nfs-mountd entries": [ + "… Notify of sudo logins": [ + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^pam_unix\\(sudo:session\\): session opened for user (.*) by [^(]*\\(uid=([^)]+)\\)$", "save": [ "thatUser", "fromUID" ] }, + "else": "… NOOP if PRIORITY 5+" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "Login as {thatUser}@{_HOSTNAME} by {fromUID}:sudo", "details": "NONE" } + } + ], + "Warn of package errors with loolwsd": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "nfs-mountd.service" }, - "then": "… NOOP if PRIORITY 5+" + "args": { "field": "SYSLOG_IDENTIFIER", "value": "loolwsd" } + }, + { + "filter": "filter_pcreAny", + "args": { "field": "MESSAGE", "re": [ + "^/usr/bin/loolwsd: error ", + "^FATAL:", + "^Failed " + ] }, + "else": "… NOOP" + }, + { + "action": "action_dailyReport", + "args": { "level": "WARN", "message": "CollaboraOnline: {MESSAGE}" } } ], "Notify of certificate renewals": [ { "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "dehydrated.service" } + "args": { "field": "SYSLOG_IDENTIFIER", "value": "dehydrated" } }, { "filter": "filter_pcre", @@ -877,120 +1006,99 @@ }, { "action": "action_dailyReport", - "args": { "level": "WARN", "message": "ACME: {MESSAGE}" } + "args": { "level": "WARN", "message": "ACME: {MESSAGE}", "details": "FIRSTLAST" } + } + ], + "Warn of SpamAssassin update failures": [ + { + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "sa-update" } + }, + { + "filter": "filter_equals", + "args": { "field": "MESSAGE", "value": "channel: could not find working mirror, channel failed" }, + "else": "… NOOP if PRIORITY 4+" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "SpamAssassin update failed", "details": "FIRSTLAST" } + } + ], + "Warn of local authentication errors": [ + { + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "nslcd" } + }, + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^\\[[^]]+\\] <([^>]+)> .*Can't contact LDAP server: (.*)$", "save": [ "nslcdClient", "nslcdError" ] }, + "else": "… NOOP if PRIORITY 3+" + }, + { + "action": "action_dailyReport", + "args": { "level": "WARN", "message": "nslcd: {nslcdError} for {nslcdClient}@{_HOSTNAME}", "details": "FIRSTLAST" } + } + ], + "Warn of Nextcloud maintenance errors": [ + { + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "php" } + }, + { + "filter": "filter_equals", + "args": { "field": "MESSAGE", "value": "Cannot write into \"config\" directory!" }, + "else": "… NOOP if PRIORITY 5+" + }, + { + "action": "action_email", + "args": { "subject": "Nextcloud config is read-only!", "message": "Nextcloud maintenance could not write to the configuration file on {__REALTIME_TIMESTAMP}." } + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "Nextcloud maintenance could not write to the configuration file", "details": "FIRSTLAST" } + } + ], + "Warn of systemd-nspawn failures": [ + { + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "systemd-nspawn" } + }, + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^(?:\\[FAILED\\] )?Failed to" }, + "else": "… NOOP if PRIORITY 4+" + }, + { + "action": "action_dailyReport", + "args": { "level": "WARN", "message": "nspawn: {MESSAGE}", "details": "FIRSTLAST" } + } + ], + "Discard ddclient debug entries": [ + { + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "ddclient" }, + "then": "… NOOP if PRIORITY 6+" } ], "Warn of core dumps": [ + { + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "systemd-coredump" } + }, { "filter": "filter_pcre", "args": { "field": "MESSAGE", "re": "of user (.*) dumped core\\.$", "save": [ "thatUser" ] }, - "else": "… Discard other coredump entries" + "else": "… NOOP" }, { "action": "action_dailyReport", "args": { "level": "WARN", "message": "Core dump for {thatUser}@{_HOSTNAME}" } } ], - "… Discard other coredump entries": [ - { - "filter": "filter_pcre", - "args": { "field": "_SYSTEMD_UNIT", "re": "^systemd-coredump@" }, - "then": "… NOOP" - } - ], - "Discard ddclient debug entries": [ - { - "filter": "filter_pcre", - "args": { "field": "_SYSTEMD_UNIT", "re": "^ddclient@" }, - "then": "… NOOP if PRIORITY 6+" - } - ], - "Notify of important PHP debug messages": [ - { - "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "php-fpm.service" } - }, - { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^\\[[A-Z](?!OTICE)(?!EBUG)" }, - "else": "… Notify of PHP error messages" - }, - { - "action": "action_dailyReport", - "args": { "level": "WARN", "message": "PHP: {MESSAGE}" } - } - ], - "… Notify of PHP error messages": [ - { - "filter": "filter_lowerOrEquals", - "args": { "field": "PRIORITY", "value": 3 }, - "else": "… NOOP" - }, - { - "action": "action_dailyReport", - "args": { "level": "WARN", "message": "PHP: {MESSAGE}" } - } - ], - "Notify of bad torrents": [ - { - "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "transmission.service" } - }, - { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^\\[.{23}\\] (.*[^:]) (?:Scrape error: )?Could not connect to tracker", "save": [ "torrentName" ] }, - "else": "… Warn of Transmission errors" - }, - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Transmission could not connect to tracker for {torrentName}" } - } - ], - "… Warn of Transmission errors": [ - { - "filter": "filter_pcreAny", - "args": { "field": "MESSAGE", "re": [ - "(?PAll nameservers have failed) \\([^():]+:[0-9]+\\)$", - "(?PNo such file or directory) \\([^():]+:[0-9]+\\)$", - "(?PToo many open files) \\([^():]+:[0-9]+\\)$", - "(?PPermission denied) \\([^():]+:[0-9]+\\)$" - ] }, - "else": "… Filter-out uninteresting Transmission events" - }, - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Transmission error: {errMsg}" } - } - ], - "… Filter-out uninteresting Transmission events": [ - { - "filter": "filter_pcreAny", - "args": { "field": "MESSAGE", "re": [ - "^\\[.{23}\\] (?:Bound socket|Cache Maximum cache size set to|RPC Server (?:Adding|Serving|Started|Stopped)|DHT (?:Bootstrapping|Finished bootstrapping|DHT initialized|Initializing|Reusing|Done uninitializing DHT|Saving|Not saving nodes|Uninitializing)|Port Forwarding Stopped|Saved \"|Using settings from|Watching \"|Searching for web interface file \"|Deleting input \\.torrent file|Parsing \\.torrent file successful|watchdir Callback decided to accept|Changed open file limit|(?:SO_RCVBUF|SO_SNDBUF) size is|Closing libevent|Loaded [0-9]+ torrent|watchdir Callback decided|Nameserver |Preallocated file \"|UDP Couldn't parse UDP tracker packet)", - "(?:Queued for verification|bytes per second\\)|[vV]erifying torrent\\.*|Announcing to tracker|Retrying (?:announce|scrape) in [0-9]+ seconds\\.|seconds from now\\.|Got [0-9]+ peers from tracker|checking just-completed piece [0-9]+|Starting IPv4 DHT announce \\([^)]+\\)|IPv4 peers from DHT|Pausing|Removing torrent|started|peers from resume file|\\.resume\"|files marked for download|Requested download is not authorized for use with this tracker\\.|Connection failed|\\(No Response\\)|(?:State changed from|moving) \"[^\"]+\" to \"[^\"]+\"|DHT announce done|failed its checksum test|403 \\(Forbidden\\)|404 \\(Not Found\\)|Tracker did not respond) \\([^():]+:[0-9]+\\)$" - ] }, - "then": "… NOOP" - } - ], - "Notify of identified SPAM messages": [ - { - "filter": "filter_equals", - "args": { "field": "_SYSTEMD_UNIT", "value": "spamassassin.service" } - }, - { - "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^spamd: identified spam" }, - "else": "… NOOP if PRIORITY 4+" - }, - { - "action": "action_dailyReport", - "args": { "level": "INFO", "message": "Spam identified" } - } - ], "Notify of getty user logins": [ { "filter": "filter_pcre", - "args": { "field": "_SYSTEMD_UNIT", "re": "^getty@" } + "args": { "field": "SYSLOG_IDENTIFIER", "re": "login" } }, { "filter": "filter_pcre", @@ -1017,6 +1125,32 @@ "args": { "level": "INFO", "message": "Failed getty login on {_HOSTNAME}" } } ], + "Notify of important PHP debug messages": [ + { + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "php-fpm" } + }, + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^\\[[A-Z](?!OTICE)(?!EBUG)" }, + "else": "… Notify of PHP error messages" + }, + { + "action": "action_dailyReport", + "args": { "level": "WARN", "message": "PHP: {MESSAGE}", "details": "FIRSTLAST" } + } + ], + "… Notify of PHP error messages": [ + { + "filter": "filter_lowerOrEquals", + "args": { "field": "PRIORITY", "value": 3 }, + "else": "… NOOP" + }, + { + "action": "action_dailyReport", + "args": { "level": "WARN", "message": "PHP: {MESSAGE}", "details": "FIRSTLAST" } + } + ], "… NOOP if PRIORITY 3+": [ { "filter": "filter_greaterOrEquals", @@ -1053,7 +1187,7 @@ "all_filters_failed": [ { "action": "action_dailyReport", - "args": { "level": "OTHER", "message": "[{PRIORITY}/{SYSLOG_IDENTIFIER}] {_UID}:{_GID}@{_HOSTNAME}:{_CMDLINE} ({_SYSTEMD_UNIT})\n {MESSAGE}" } + "args": { "level": "OTHER", "message": "[{PRIORITY}/{SYSLOG_IDENTIFIER}] {_UID}:{_GID}@{_HOSTNAME}:{_CMDLINE} ({_SYSTEMD_UNIT})\n{MESSAGE}" } } ] }, @@ -1068,6 +1202,9 @@ "nftBan": { "nft": [ "/usr/bin/nft" ] }, + "ipsetBan": { + "ipset": [ "/usr/bin/ipset", "-exist", "-quiet" ] + }, "8bit-message-encoding": "iso-8859-15", "storage": "/var/lib/pyruse", "debug": false diff --git a/extra/examples/get-systemd-stats.sh b/extra/examples/get-systemd-stats.sh index 539a812..bb7fcba 100755 --- a/extra/examples/get-systemd-stats.sh +++ b/extra/examples/get-systemd-stats.sh @@ -1,11 +1,15 @@ #!/bin/bash -# $1 (optional): systemd-nspawn machine name +# $1 (optional): grouping criteria: SYSLOG_IDENTIFIER (default) or _SYSTEMD_UNIT +# $+ (optional): journalctl options (-M machine, -S date…) + +CRIT=${1:-SYSLOG_IDENTIFIER} +shift { - printf 'Units\tTotal\tP7\tP6\tP5\tP4\tP3\tP2\tP1\tP0\n' - sudo journalctl ${1:+-M "$1"} -o json-pretty --output-fields=_SYSTEMD_UNIT,PRIORITY \ + printf '%s\tTotal\tP7\tP6\tP5\tP4\tP3\tP2\tP1\tP0\n' $CRIT + sudo journalctl "$@" -o json-pretty --output-fields=${CRIT},PRIORITY \ | tr -d $'"\t, ' \ - | awk -F: -vOFS=: ' + | awk -F: -vOFS=: -vCRIT=$CRIT ' /^\{/ { u = "" p = -1 @@ -13,7 +17,7 @@ $1 == "PRIORITY" { p = $2 } - $1 == "_SYSTEMD_UNIT" { + $1 == CRIT { u = gensub(\ "@.*(\\.[^.]*)$",\ "@*\\1",\ diff --git a/extra/systemd/action_ipsetBan.conf b/extra/systemd/action_ipsetBan.conf new file mode 100644 index 0000000..e235af2 --- /dev/null +++ b/extra/systemd/action_ipsetBan.conf @@ -0,0 +1,3 @@ +[Unit] +Requires=iptables.service +After=iptables.service diff --git a/pyruse/actions/action_ipsetBan.py b/pyruse/actions/action_ipsetBan.py new file mode 100644 index 0000000..b035d20 --- /dev/null +++ b/pyruse/actions/action_ipsetBan.py @@ -0,0 +1,37 @@ +# pyruse is intended as a replacement to both fail2ban and epylog +# Copyright © 2017–2018 Y. Gablin +# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing. +import os +import subprocess +from pyruse import ban, base, config + +class Action(base.Action, ban.NetfilterBan): + _storage = config.Config().asMap().get("storage", "/var/lib/pyruse") \ + + "/" + os.path.basename(__file__) + ".json" + _ipset = config.Config().asMap().get("ipsetBan", {}).get("ipset", ["/usr/bin/ipset", "-exist", "-quiet"]) + + def __init__(self, args): + base.Action.__init__(self) + ban.NetfilterBan.__init__(self, Action._storage) + if args is None: + return # on-boot configuration + ipv4Set = args["ipSetIPv4"] + ipv6Set = args["ipSetIPv6"] + field = args["IP"] + banSeconds = args.get("banSeconds", None) + self.initSelf(ipv4Set, ipv6Set, field, banSeconds) + + def act(self, entry): + ban.NetfilterBan.act(self, entry) + + def setBan(self, nfSet, ip, seconds): + cmd = list(Action._ipset) + cmd.extend(["add", nfSet, ip]) + if seconds > 0: + cmd.extend(["timeout", str(seconds)]) + subprocess.run(cmd) + + def cancelBan(self, nfSet, ip): + cmd = list(Action._ipset) + cmd.extend(["del", nfSet, ip]) + subprocess.run(cmd) diff --git a/pyruse/actions/action_nftBan.py b/pyruse/actions/action_nftBan.py index 6192f59..fe9a13b 100644 --- a/pyruse/actions/action_nftBan.py +++ b/pyruse/actions/action_nftBan.py @@ -1,97 +1,39 @@ # pyruse is intended as a replacement to both fail2ban and epylog # Copyright © 2017–2018 Y. Gablin # Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing. -import datetime -import json import os import subprocess -from pyruse import base, config +from pyruse import ban, base, config -class Action(base.Action): +class Action(base.Action, ban.NetfilterBan): _storage = config.Config().asMap().get("storage", "/var/lib/pyruse") \ + "/" + os.path.basename(__file__) + ".json" _nft = config.Config().asMap().get("nftBan", {}).get("nft", ["/usr/bin/nft"]) def __init__(self, args): - super().__init__() + base.Action.__init__(self) + ban.NetfilterBan.__init__(self, Action._storage) if args is None: return # on-boot configuration - self.ipv4Set = args["nftSetIPv4"] - self.ipv6Set = args["nftSetIPv6"] - self.field = args["IP"] - self.banSeconds = args.get("banSeconds", None) + ipv4Set = args["nftSetIPv4"] + ipv6Set = args["nftSetIPv6"] + field = args["IP"] + banSeconds = args.get("banSeconds", None) + self.initSelf(ipv4Set, ipv6Set, field, banSeconds) def act(self, entry): - ip = entry[self.field] - nftSet = self.ipv6Set if ":" in ip else self.ipv4Set - newBan = {"IP": ip, "nftSet": nftSet} + ban.NetfilterBan.act(self, entry) - now = datetime.datetime.utcnow() - bans = [] - previousTS = None - try: - with open(Action._storage) as dataFile: - for ban in json.load(dataFile): - if ban["timestamp"] > 0 and ban["timestamp"] <= now.timestamp(): - continue - elif {k: ban[k] for k in newBan.keys()} == newBan: - # should not happen, since the IP is banned… - previousTS = ban["timestamp"] - else: - bans.append(ban) - except IOError: - pass # new file - - if previousTS is not None: - try: - cmd = list(Action._nft) - cmd.append("delete element %s {%s}" % (nftSet, ip)) - subprocess.run(cmd) - except Exception: - pass # too late: not a problem - - if self.banSeconds: - until = now + datetime.timedelta(seconds = self.banSeconds) - newBan["timestamp"] = until.timestamp() - timeout = self.banSeconds - else: - newBan["timestamp"] = 0 - timeout = 0 - - self._doBan(timeout, ip, nftSet) - bans.append(newBan) - with open(Action._storage, "w") as dataFile: - json.dump(bans, dataFile) - - def boot(self): - now = datetime.datetime.utcnow() - bans = [] - try: - with open(Action._storage) as dataFile: - for ban in json.load(dataFile): - if ban["timestamp"] == 0: - self._doBan(0, ban["IP"], ban["nftSet"]) - bans.append(ban) - elif ban["timestamp"] <= now.timestamp(): - continue - else: - until = datetime.datetime.utcfromtimestamp(ban["timestamp"]) - timeout = (until - now).total_seconds() - self._doBan(int(timeout), ban["IP"], ban["nftSet"]) - bans.append(ban) - except IOError: - pass # no file - - with open(Action._storage, "w") as dataFile: - json.dump(bans, dataFile) - - def _doBan(self, seconds, ip, nftSet): - if seconds < 0: - return # can happen when the threshold is crossed while computing the duration + def setBan(self, nfSet, ip, seconds): if seconds == 0: timeout = "" else: timeout = " timeout %ss" % seconds cmd = list(Action._nft) - cmd.append("add element %s {%s%s}" % (nftSet, ip, timeout)) + cmd.append("add element %s {%s%s}" % (nfSet, ip, timeout)) + subprocess.run(cmd) + + def cancelBan(self, nfSet, ip): + cmd = list(Action._nft) + cmd.append("delete element %s {%s}" % (nfSet, ip)) subprocess.run(cmd) diff --git a/pyruse/ban.py b/pyruse/ban.py new file mode 100644 index 0000000..dd120a4 --- /dev/null +++ b/pyruse/ban.py @@ -0,0 +1,85 @@ +# pyruse is intended as a replacement to both fail2ban and epylog +# Copyright © 2017–2018 Y. Gablin +# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing. +import abc +import datetime +import json + +class NetfilterBan(abc.ABC): + def __init__(self, storage): + self.storage = storage + + def initSelf(self, ipv4Set, ipv6Set, ipField, banSeconds): + self.ipv4Set = ipv4Set + self.ipv6Set = ipv6Set + self.field = ipField + self.banSeconds = banSeconds + + @abc.abstractmethod + def setBan(self, nfSet, ip, seconds): + pass + + @abc.abstractmethod + def cancelBan(self, nfSet, ip): + pass + + def act(self, entry): + ip = entry[self.field] + nfSet = self.ipv6Set if ":" in ip else self.ipv4Set + newBan = {"IP": ip, "nfSet": nfSet} + + now = datetime.datetime.utcnow() + bans = [] + previousTS = None + try: + with open(self.storage) as dataFile: + for ban in json.load(dataFile): + if ban["timestamp"] > 0 and ban["timestamp"] <= now.timestamp(): + continue + elif {k: ban[k] for k in newBan.keys()} == newBan: + # should not happen, since the IP is banned… + previousTS = ban["timestamp"] + else: + bans.append(ban) + except IOError: + pass # new file + + if previousTS is not None: + try: + self.cancelBan(nfSet, ip) + except Exception: + pass # too late: not a problem + + if self.banSeconds: + until = now + datetime.timedelta(seconds = self.banSeconds) + newBan["timestamp"] = until.timestamp() + timeout = self.banSeconds + else: + newBan["timestamp"] = 0 + timeout = 0 + + self.setBan(nfSet, ip, timeout) + bans.append(newBan) + with open(self.storage, "w") as dataFile: + json.dump(bans, dataFile) + + def boot(self): + now = int(datetime.datetime.utcnow().timestamp()) + bans = [] + try: + with open(self.storage) as dataFile: + for ban in json.load(dataFile): + if ban["timestamp"] == 0: + self.setBan(ban["nfSet"], ban["IP"], 0) + bans.append(ban) + elif int(ban["timestamp"]) <= now: + continue + else: + timeout = int(ban["timestamp"]) - now + self.setBan(ban["nfSet"], ban["IP"], timeout) + bans.append(ban) + except IOError: + pass # no file + + with open(self.storage, "w") as dataFile: + json.dump(bans, dataFile) diff --git a/tests/action_ipsetBan.py b/tests/action_ipsetBan.py new file mode 100644 index 0000000..726daad --- /dev/null +++ b/tests/action_ipsetBan.py @@ -0,0 +1,147 @@ +# pyruse is intended as a replacement to both fail2ban and epylog +# Copyright © 2017–2018 Y. Gablin +# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing. +import json +import os +import time +from pyruse.actions.action_ipsetBan import Action + +ipBanCmd = "ipsetBan.cmd" +ipBanState = "action_ipsetBan.py.json" + +def _clean(): + if os.path.exists(ipBanCmd): + os.remove(ipBanCmd) + if os.path.exists(ipBanState): + os.remove(ipBanState) + +def whenBanIPv4ThenAddToIPv4Set(): + _clean() + Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"}).act({"ip": "10.0.0.1"}) + assert os.path.exists(ipBanCmd) + assert os.path.exists(ipBanState) + nbLines = 0 + with open(ipBanCmd, "rt") as c: + for line in c: + assert line == "add I4ban 10.0.0.1\n", line + nbLines += 1 + assert nbLines == 1, nbLines + nbBans = 0 + with open(ipBanState) as s: + for ban in json.load(s): + assert ban["IP"] == "10.0.0.1" and ban["nfSet"] == "I4ban", str(ban) + nbBans += 1 + assert nbBans == 1, nbBans + _clean() + +def whenBanIPv6ThenAddToIPv6Set(): + _clean() + Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"}).act({"ip": "::1"}) + assert os.path.exists(ipBanCmd) + assert os.path.exists(ipBanState) + nbLines = 0 + with open(ipBanCmd, "rt") as c: + for line in c: + assert line == "add I6ban ::1\n", line + nbLines += 1 + assert nbLines == 1, nbLines + nbBans = 0 + with open(ipBanState) as s: + for ban in json.load(s): + assert ban["IP"] == "::1" and ban["nfSet"] == "I6ban", str(ban) + nbBans += 1 + assert nbBans == 1, nbBans + _clean() + +def whenBanTwoIPThenTwoLinesInState(): + _clean() + action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"}) + action.act({"ip": "10.0.0.1"}) + action.act({"ip": "::1"}) + action.act({"ip": "10.0.0.1"}) + assert os.path.exists(ipBanState) + nbBans = 0 + with open(ipBanState) as s: + for ban in json.load(s): + if ban["IP"] == "10.0.0.1": + assert ban["nfSet"] == "I4ban", str(ban) + elif ban["IP"] == "::1": + assert ban["nfSet"] == "I6ban", str(ban) + else: + assert false, str(ban) + nbBans += 1 + assert nbBans == 2, nbBans + _clean() + +def whenBanAnewThenNoDuplicate(): + _clean() + action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban"}) + action.act({"ip": "10.0.0.1"}) + action.act({"ip": "10.0.0.1"}) + assert os.path.exists(ipBanCmd) + assert os.path.exists(ipBanState) + lineCount = 0 + with open(ipBanCmd, "rt") as c: + for line in c: + lineCount += 1 + if lineCount == 1: + assert line == "add I4ban 10.0.0.1\n", line + elif lineCount == 2: + assert line == "del I4ban 10.0.0.1\n", line + elif lineCount == 3: + assert line == "add I4ban 10.0.0.1\n", line + assert lineCount == 3, lineCount + nbBans = 0 + with open(ipBanState) as s: + for ban in json.load(s): + if ban["IP"] == "10.0.0.1": + assert ban["nfSet"] == "I4ban", str(ban) + nbBans += 1 + assert nbBans == 1, nbBans + _clean() + +def whenFinishedBanThenAsIfNotThere(): + _clean() + action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban", "banSeconds": 1}) + action.act({"ip": "10.0.0.1"}) + time.sleep(1) + action.act({"ip": "10.0.0.1"}) + assert os.path.exists(ipBanCmd) + lineCount = 0 + with open(ipBanCmd, "rt") as c: + for line in c: + lineCount += 1 + if lineCount == 1: + assert line == "add I4ban 10.0.0.1 timeout 1\n", line + elif lineCount == 2: + assert line == "add I4ban 10.0.0.1 timeout 1\n", line + assert lineCount == 2, lineCount + _clean() + +def whenUnfinishedBanThenTimeoutReset(): + _clean() + action = Action({"IP": "ip", "ipSetIPv4": "I4ban", "ipSetIPv6": "I6ban", "banSeconds": 2}) + action.act({"ip": "10.0.0.1"}) + time.sleep(1) + action.act({"ip": "10.0.0.1"}) + assert os.path.exists(ipBanCmd) + lineCount = 0 + with open(ipBanCmd, "rt") as c: + for line in c: + lineCount += 1 + if lineCount == 1: + assert line == "add I4ban 10.0.0.1 timeout 2\n", line + elif lineCount == 2: + assert line == "del I4ban 10.0.0.1\n", line + elif lineCount == 3: + assert line == "add I4ban 10.0.0.1 timeout 2\n", line + assert lineCount == 3, lineCount + _clean() + +def unitTests(): + whenBanIPv4ThenAddToIPv4Set() + whenBanIPv6ThenAddToIPv6Set() + whenBanTwoIPThenTwoLinesInState() + whenBanAnewThenNoDuplicate() + whenFinishedBanThenAsIfNotThere() + whenUnfinishedBanThenTimeoutReset() diff --git a/tests/action_nftBan.py b/tests/action_nftBan.py index 0e1e373..df035ab 100644 --- a/tests/action_nftBan.py +++ b/tests/action_nftBan.py @@ -29,7 +29,7 @@ def whenBanIPv4ThenAddToIPv4Set(): nbBans = 0 with open(nftBanState) as s: for ban in json.load(s): - assert ban["IP"] == "10.0.0.1" and ban["nftSet"] == "ip I4 ban", str(ban) + assert ban["IP"] == "10.0.0.1" and ban["nfSet"] == "ip I4 ban", str(ban) nbBans += 1 assert nbBans == 1, nbBans _clean() @@ -48,7 +48,7 @@ def whenBanIPv6ThenAddToIPv6Set(): nbBans = 0 with open(nftBanState) as s: for ban in json.load(s): - assert ban["IP"] == "::1" and ban["nftSet"] == "ip6 I6 ban", str(ban) + assert ban["IP"] == "::1" and ban["nfSet"] == "ip6 I6 ban", str(ban) nbBans += 1 assert nbBans == 1, nbBans _clean() @@ -64,9 +64,9 @@ def whenBanTwoIPThenTwoLinesInState(): with open(nftBanState) as s: for ban in json.load(s): if ban["IP"] == "10.0.0.1": - assert ban["nftSet"] == "ip I4 ban", str(ban) + assert ban["nfSet"] == "ip I4 ban", str(ban) elif ban["IP"] == "::1": - assert ban["nftSet"] == "ip6 I6 ban", str(ban) + assert ban["nfSet"] == "ip6 I6 ban", str(ban) else: assert false, str(ban) nbBans += 1 @@ -95,7 +95,7 @@ def whenBanAnewThenNoDuplicate(): with open(nftBanState) as s: for ban in json.load(s): if ban["IP"] == "10.0.0.1": - assert ban["nftSet"] == "ip I4 ban", str(ban) + assert ban["nfSet"] == "ip I4 ban", str(ban) nbBans += 1 assert nbBans == 1, nbBans _clean() diff --git a/tests/main.py b/tests/main.py index 6b33a2e..3e3d403 100644 --- a/tests/main.py +++ b/tests/main.py @@ -20,7 +20,7 @@ def main(): # Unit tests import filter_equals, filter_greaterOrEquals, filter_in, filter_inNetworks, filter_lowerOrEquals, filter_pcre, filter_pcreAny, filter_userExists - import action_counterRaise, action_counterReset, action_dailyReport, action_dnatCapture, action_dnatReplace, action_email, action_log, action_nftBan + import action_counterRaise, action_counterReset, action_dailyReport, action_dnatCapture, action_dnatReplace, action_email, action_ipsetBan, action_log, action_nftBan filter_equals.unitTests() filter_greaterOrEquals.unitTests() @@ -36,6 +36,7 @@ def main(): action_dnatCapture.unitTests() action_dnatReplace.unitTests() action_email.unitTests() + action_ipsetBan.unitTests() action_log.unitTests() action_nftBan.unitTests() diff --git a/tests/pyruse.json b/tests/pyruse.json index 2c62b56..a6bc640 100644 --- a/tests/pyruse.json +++ b/tests/pyruse.json @@ -87,5 +87,8 @@ "nftBan": { "nft": [ "/bin/sh", "-c", "echo \"$0\" >>\"nftBan.cmd\"" ] }, + "ipsetBan": { + "ipset": [ "/bin/sh", "-c", "echo \"$0 $*\" >>\"ipsetBan.cmd\"" ] + }, "storage": "." }