Niemand möchte, das sein Server gehackt wird. Eine 100%-Lösung dagegen wird es kaum geben, außer den Server offline zu nehmen. Alles was du tun kannst, ist dich um möglichst viel Sicherheit bemühen. Und da setzen wir in diesem Blogbeitrag an.
Fail2Ban ist ein kleiner, netter Dienst, der periodisch Logfiles mit definierten Filtern nach suspekten Einträgen durchsucht und dann mittels iptables die verursachende IP für einen bestimmten Zeitraum sperrt. Simple, oder?
Zuerst installieren wir uns fail2ban und werfen einen Blick auf die Konfigurationsdateien:
# apt install fail2ban
# cd /etc/fail2ban/
# cp jail.conf jail.local
# ls -la
total 100
drwxr-xr-x 6 root root 4096 Nov 2 14:41 .
drwxr-xr-x 90 root root 4096 Oct 27 07:09 ..
drwxr-xr-x 2 root root 4096 Nov 2 14:14 action.d
-rw-r--r-- 1 root root 2816 Nov 23 2020 fail2ban.conf
drwxr-xr-x 2 root root 4096 Jul 12 06:52 fail2ban.d
drwxr-xr-x 3 root root 4096 Nov 2 14:13 filter.d
-rw-r--r-- 1 root root 24996 Nov 23 2020 jail.conf
drwxr-xr-x 2 root root 4096 Oct 15 07:37 jail.d
-rw-r--r-- 1 root root 25178 Nov 2 14:41 jail.local
-rw-r--r-- 1 root root 645 Nov 23 2020 paths-arch.conf
-rw-r--r-- 1 root root 2827 Nov 23 2020 paths-common.conf
-rw-r--r-- 1 root root 573 Nov 23 2020 paths-debian.conf
-rw-r--r-- 1 root root 738 Nov 23 2020 paths-opensuse.conf
# ls filter.d/
3proxy.conf apache-shellshock.conf directadmin.conf gitlab.conf monit.conf oracleims.conf roundcube-auth.conf squid.conf wuftpd.conf
apache-auth.conf assp.conf domino-smtp.conf grafana.conf murmur.conf pam-generic.conf screensharingd.conf squirrelmail.conf xinetd-fail.conf
apache-badbots.conf asterisk.conf dovecot.conf groupoffice.conf mysqld-auth.conf perdition.conf selinux-common.conf sshd.conf znc-adminlog.conf
apache-botsearch.conf bitwarden.conf dropbear.conf gssftpd.conf nagios.conf phpmyadmin-syslog.conf selinux-ssh.conf stunnel.conf zoneminder.conf
apache-common.conf botsearch-common.conf drupal-auth.conf guacamole.conf named-refused.conf php-url-fopen.conf sendmail-auth.conf suhosin.conf
apache-fakegooglebot.conf centreon.conf ejabberd-auth.conf haproxy-http-auth.conf nginx-botsearch.conf portsentry.conf sendmail-reject.conf tine20.conf
apache-modsecurity.conf common.conf exim-common.conf horde.conf nginx-http-auth.conf postfix.conf sieve.conf traefik-auth.conf
apache-nohome.conf counter-strike.conf exim.conf ignorecommands nginx-limit-req.conf proftpd.conf slapd.conf uwimap-auth.conf
apache-noscript.conf courier-auth.conf exim-spam.conf kerio.conf nsd.conf pure-ftpd.conf softethervpn.conf vsftpd.conf
apache-overflows.conf courier-smtp.conf freeswitch.conf lighttpd-auth.conf openhab.conf qmail.conf sogo-auth.conf webmin-auth.conf
apache-pass.conf cyrus-imap.conf froxlor-auth.conf mongodb-auth.conf openwebmail.conf recidive.conf solid-pop3d.conf wplogin.conf
# ls action.d/
abuseipdb.conf firewallcmd-allports.conf iptables-allports.conf mail.conf nsupdate.conf sendmail-whois-ipmatches.conf
apf.conf firewallcmd-common.conf iptables-common.conf mail-whois-common.conf osx-afctl.conf sendmail-whois-lines.conf
badips.conf firewallcmd-ipset.conf iptables.conf mail-whois.conf osx-ipfw.conf sendmail-whois-matches.conf
badips.py firewallcmd-multiport.conf iptables-ipset-proto4.conf mail-whois-lines.conf pf.conf shorewall.conf
blocklist_de.conf firewallcmd-new.conf iptables-ipset-proto6-allports.conf mynetwatchman.conf route.conf shorewall-ipset-proto6.conf
bsd-ipfw.conf firewallcmd-rich-logging.conf iptables-ipset-proto6.conf netscaler.conf sendmail-buffered.conf smtp.py
cloudflare.conf firewallcmd-rich-rules.conf iptables-multiport.conf nftables-allports.conf sendmail-common.conf symbiosis-blacklist-allports.conf
complain.conf helpers-common.conf iptables-multiport-log.conf nftables.conf sendmail.conf ufw.conf
docker-action.conf hostsdeny.conf iptables-new.conf nftables-multiport.conf sendmail-geoip-lines.conf xarf-login-attack.conf
dshield.conf ipfilter.conf iptables-xt_recent-echo.conf nginx-block-map.conf sendmail-whois.conf
dummy.conf ipfw.conf mail-buffered.conf npf.conf sendmail-whois-ipjailmatches.conf
# ls jail.d/
defaults-debian.conf
# cat jail.d/defaults-debian.conf
[sshd]
enabled = true
Wie oben bereits erwähnt, passieren grundsätzlich 3 Dinge:
- Aufgabe definieren: Welches Logfile soll nach was durchsucht werden und dann soll was passieren (wird in jail.conf bzw. besser in jail.local (lokale Kopie) konfiguriert)
- mit Filtern nach bestimmten Ausdrücken suchen (Filter werden in einer Datei unter /etc/fail2ban/filter.d/ definiert)
- bestimmte Aktionen ausführen (Aktionen werden in einer Datei unter /etc/fail2ban/action.d/ definiert)
Wie man sieht, gibt es bereits etliche vorkonfigurierte „Jails“ für Apache, MySQL, Asterisk, sendmail, WordPress, SSH und und und. Unter Debian ist die Überwachung des Dienstes SSH bereits aktiviert (siehe Datei /etc/fail2ban/jail.d/defaults-debian.conf). Schön, dann schauen wir uns mal die Wirkungsweise von fail2ban an Hand von SSH näher an.
Beginnen wir mit dem Abschnitt für sshd in der Datei jail.local. Es wird empfohlen, die jail.conf nicht direkt zu bearbeiten, sondern mittels cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local eine Kopie der Datei anzulegen und diese zukünftig zu bearbeiten, denn das System greift beim Vorhandensein dieser Datei selbstständig auf die jail.local zurück (lies dazu auch die Kopfzeilen der jail.conf bzw. dann jail.local):
# nano /etc/jail.local
...
[sshd]
# To use more aggressive sshd modes set filter parameter "mode" in jail.local:
# normal (default), ddos, extra or aggressive (combines all).
# See "tests/files/logs/sshd" or "filter.d/sshd.conf" for usage example and details.
#mode = normal
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
...
Seit fail2ban 0.9 sind die Pfade zu den Logdateien und Backends nicht mehr direkt in der jail.conf verlinkt, sondern in der Datei /etc/fail2ban/paths-common.conf bzw. als paths-debian.conf (Settings für arch und opensuse sind auch vorhanden) hinterlegt. So wird u.a. der Pfad zur Datei /var/log/auth.log definiert, die alle ssh-Loginversuche protokolliert. Dann schauen wir uns das Logfile für SSH-Anmeldungen unter Debian genauer an:
# tail -n 18 /var/log/auth.log
Nov 3 10:49:02 debian sshd[4158907]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.178.202 user=root
...
Nov 3 10:49:25 debian sshd[4158907]: error: maximum authentication attempts exceeded for root from 192.168.178.202 port 19005 ssh2 [preauth]
Nov 3 10:49:25 debian sshd[4158907]: Disconnecting authenticating user root 192.168.178.202 port 19005: Too many authentication failures [preauth]
Nov 3 10:49:25 debian sshd[4158907]: PAM 2 more authentication failures; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.178.202 user=root
Nov 3 10:49:29 debian sshd[4159096]: Accepted password for bob from 192.168.178.202 port 19008 ssh2
Nov 3 10:49:29 debian sshd[4159096]: pam_unix(sshd:session): session opened for user bob(uid=1000) by (uid=0)
Nov 3 10:49:29 debian systemd-logind[539]: New session 17842 of user bob.
Nov 3 10:49:29 debian sshd[4159106]: Accepted password for bob from 192.168.178.202 port 19009 ssh2
Nov 3 10:49:29 debian sshd[4159106]: pam_unix(sshd:session): session opened for user bob(uid=1000) by (uid=0)
Nov 3 10:49:29 debian systemd-logind[539]: New session 17843 of user bob.
Nov 3 10:49:41 debian sudo: bob : TTY=pts/2 ; PWD=/home/bob ; USER=root ; COMMAND=/usr/bin/cat /var/log/auth.log
Nov 3 10:49:41 debian sudo: pam_unix(sudo:session): session opened for user root(uid=0) by bob(uid=1000)
Zuerst siehst du ein Login-Versuch per SSH durch Benutzer root, dieser schlägt nach mehreren Versuchen fehl. Anschließend versucht es Benutzer bob, er hat Erfolg. Anschließend versucht bob mit sudo-Rechten die Datei /var/log/auth.log mittels des Programms cat unter /usr/bin/cat auszugeben – auch erfolgreich. Ganz schön viele Infos, oder? Was wir jetzt suchen, sind Einträge über misslungene Login-Versuche und vor allem dessen Anzahl und dessen zeitlichen Zusammenhang. Dies ist wie bereits erwähnt in der Datei sshd.conf vorkonfiguriert.
# cat /etc/fail2ban/filter.d/sshd.conf
# Fail2Ban filter for openssh
#
# If you want to protect OpenSSH from being bruteforced by password
# authentication then get public key authentication working before disabling
# PasswordAuthentication in sshd_config.
#
#
# "Connection from <HOST> port \d+" requires LogLevel VERBOSE in sshd_config
#
[INCLUDES]
# Read common prefixes. If any customizations available -- read them from
# common.local
before = common.conf
[DEFAULT]
_daemon = sshd
# optional prefix (logged from several ssh versions) like "error: ", "error: PAM: " or "fatal: "
__pref = (?:(?:error|fatal): (?:PAM: )?)?
# optional suffix (logged from several ssh versions) like " [preauth]"
#__suff = (?: port \d+)?(?: \[preauth\])?\s*
__suff = (?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*
__on_port_opt = (?: (?:port \d+|on \S+)){0,2}
# close by authenticating user:
__authng_user = (?: (?:invalid|authenticating) user <F-USER>\S+|.*?</F-USER>)?
# for all possible (also future) forms of "no matching (cipher|mac|MAC|compression method|key exchange method|host key type) found",
# see ssherr.c for all possible SSH_ERR_..._ALG_MATCH errors.
__alg_match = (?:(?:\w+ (?!found\b)){0,2}\w+)
# PAM authentication mechanism, can be overridden, e. g. `filter = sshd[__pam_auth='pam_ldap']`:
__pam_auth = pam_[a-z]+
[Definition]
prefregex = ^<F-MLFID>%(__prefix_line)s</F-MLFID>%(__pref)s<F-CONTENT>.+</F-CONTENT>$
cmnfailre = ^[aA]uthentication (?:failure|error|failed) for <F-USER>.*</F-USER> from <HOST>( via \S+)?%(__suff)s$
^User not known to the underlying authentication module for <F-USER>.*</F-USER> from <HOST>%(__suff)s$
<cmnfailre-failed-pub-<publickey>>
^Failed <cmnfailed> for (?P<cond_inv>invalid user )?<F-USER>(?P<cond_user>\S+)|(?(cond_inv)(?:(?! from ).)*?|[^:]+)</F-USER> from <HOST>%(__on_port_opt)s(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)
^<F-USER>ROOT</F-USER> LOGIN REFUSED FROM <HOST>
^[iI](?:llegal|nvalid) user <F-USER>.*?</F-USER> from <HOST>%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> from <HOST> not allowed because not listed in AllowUsers%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> from <HOST> not allowed because listed in DenyUsers%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> from <HOST> not allowed because not in any group%(__suff)s$
^refused connect from \S+ \(<HOST>\)
^Received <F-MLFFORGET>disconnect</F-MLFFORGET> from <HOST>%(__on_port_opt)s:\s*3: .*: Auth fail%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> from <HOST> not allowed because a group is listed in DenyGroups%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> from <HOST> not allowed because none of user's groups are listed in AllowGroups%(__suff)s$
^<F-NOFAIL>%(__pam_auth)s\(sshd:auth\):\s+authentication failure;</F-NOFAIL>(?:\s+(?:(?:logname|e?uid|tty)=\S*)){0,4}\s+ruser=<F-ALT_USER>\S*</F-ALT_USER>\s+rhost=<HOST>(?:\s+user=<F-USER>\S*</F-USER>)?%(__suff)s$
^maximum authentication attempts exceeded for <F-USER>.*</F-USER> from <HOST>%(__on_port_opt)s(?: ssh\d*)?%(__suff)s$
^User <F-USER>\S+|.*?</F-USER> not allowed because account is locked%(__suff)s
^<F-MLFFORGET>Disconnecting</F-MLFFORGET>(?: from)?(?: (?:invalid|authenticating)) user <F-USER>\S+</F-USER> <HOST>%(__on_port_opt)s:\s*Change of username or service not allowed:\s*.*\[preauth\]\s*$
^Disconnecting: Too many authentication failures(?: for <F-USER>\S+|.*?</F-USER>)?%(__suff)s$
^<F-NOFAIL>Received <F-MLFFORGET>disconnect</F-MLFFORGET></F-NOFAIL> from <HOST>%(__on_port_opt)s:\s*11:
<mdre-<mode>-other>
^<F-MLFFORGET><F-MLFGAINED>Accepted \w+</F-MLFGAINED></F-MLFFORGET> for <F-USER>\S+</F-USER> from <HOST>(?:\s|$)
cmnfailed-any = \S+
cmnfailed-ignore = \b(?!publickey)\S+
cmnfailed-invalid = <cmnfailed-ignore>
cmnfailed-nofail = (?:<F-NOFAIL>publickey</F-NOFAIL>|\S+)
cmnfailed = <cmnfailed-<publickey>>
mdre-normal =
# used to differentiate "connection closed" with and without `[preauth]` (fail/nofail cases in ddos mode)
mdre-normal-other = ^<F-NOFAIL><F-MLFFORGET>(Connection closed|Disconnected)</F-MLFFORGET></F-NOFAIL> (?:by|from)%(__authng_user)s <HOST>(?:%(__suff)s|\s*)$
mdre-ddos = ^Did not receive identification string from <HOST>
^kex_exchange_identification: (?:[Cc]lient sent invalid protocol identifier|[Cc]onnection closed by remote host)
^Bad protocol version identification '.*' from <HOST>
^<F-NOFAIL>SSH: Server;Ltype:</F-NOFAIL> (?:Authname|Version|Kex);Remote: <HOST>-\d+;[A-Z]\w+:
^Read from socket failed: Connection <F-MLFFORGET>reset</F-MLFFORGET> by peer
# same as mdre-normal-other, but as failure (without <F-NOFAIL>) and [preauth] only:
mdre-ddos-other = ^<F-MLFFORGET>(Connection (?:closed|reset)|Disconnected)</F-MLFFORGET> (?:by|from)%(__authng_user)s <HOST>%(__on_port_opt)s\s+\[preauth\]\s*$
mdre-extra = ^Received <F-MLFFORGET>disconnect</F-MLFFORGET> from <HOST>%(__on_port_opt)s:\s*14: No(?: supported)? authentication methods available
^Unable to negotiate with <HOST>%(__on_port_opt)s: no matching <__alg_match> found.
^Unable to negotiate a <__alg_match>
^no matching <__alg_match> found:
# part of mdre-ddos-other, but user name is supplied (invalid/authenticating) on [preauth] phase only:
mdre-extra-other = ^<F-MLFFORGET>Disconnected</F-MLFFORGET>(?: from)?(?: (?:invalid|authenticating)) user <F-USER>\S+|.*?</F-USER> <HOST>%(__on_port_opt)s \[preauth\]\s*$
mdre-aggressive = %(mdre-ddos)s
%(mdre-extra)s
# mdre-extra-other is fully included within mdre-ddos-other:
mdre-aggressive-other = %(mdre-ddos-other)s
# Parameter "publickey": nofail (default), invalid, any, ignore
publickey = nofail
# consider failed publickey for invalid users only:
cmnfailre-failed-pub-invalid = ^Failed publickey for invalid user <F-USER>(?P<cond_user>\S+)|(?:(?! from ).)*?</F-USER> from <HOST>%(__on_port_opt)s(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)
# consider failed publickey for valid users too (don't need RE, see cmnfailed):
cmnfailre-failed-pub-any =
# same as invalid, but consider failed publickey for valid users too, just as no failure (helper to get IP and user-name only, see cmnfailed):
cmnfailre-failed-pub-nofail = <cmnfailre-failed-pub-invalid>
# don't consider failed publickey as failures (don't need RE, see cmnfailed):
cmnfailre-failed-pub-ignore =
cfooterre = ^<F-NOFAIL>Connection from</F-NOFAIL> <HOST>
failregex = %(cmnfailre)s
<mdre-<mode>>
%(cfooterre)s
# Parameter "mode": normal (default), ddos, extra or aggressive (combines all)
# Usage example (for jail.local):
# [sshd]
# mode = extra
# # or another jail (rewrite filter parameters of jail):
# [sshd-aggressive]
# filter = sshd[mode=aggressive]
#
mode = normal
#filter = sshd[mode=aggressive]
ignoreregex =
maxlines = 1
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd
# DEV Notes:
#
# "Failed \S+ for .*? from <HOST>..." failregex uses non-greedy catch-all because
# it is coming before use of <HOST> which is not hard-anchored at the end as well,
# and later catch-all's could contain user-provided input, which need to be greedily
# matched away first.
#
# Author: Cyril Jaquier, Yaroslav Halchenko, Petr Voralek, Daniel Black and Sergey Brester aka sebres
# Rewritten using prefregex (and introduced "mode" parameter) by Serg G. Brester.
Jetzt die Datei /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf im Detail zu erläutern, würde schlichtweg zu weit führen. Gerade bei ssh und dem Umfang der auth.log sowie den vielfältigen Varianten der Verbindungsaufnahme gibt es zu viele Möglichkeiten, um alle im Detail durchzugehen. Zum Glück gibt es da bereits vorgefertigte Filter. Aber ich erläutere dir dies gleich noch etwas einfacher am Beispiel von einer WordPress-Docker-Instanz.
Im Abschnitt [Definitions] findet sich unter anderem im Unterabschnitt cmnfailre die Suche nach dem Ausdruck „authentication failure“. Übersichtlicher schaut das aus, wenn du mit dem Kommando fail2ban-regex speziell Logdateien mit dem passenden Filter abgleichst. Auch hier wird dir der Ausdruck „authentication failure“ ausgespuckt und es finden sich 2 Einträge von root dazu.
# fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
Running tests
=============
Use failregex filter file : sshd, basedir: /etc/fail2ban
Use maxlines : 1
Use datepattern : {^LN-BEG} : Default Detectors
Use log file : /var/log/auth.log
Use encoding : UTF-8
Results
=======
Prefregex: 10544 total
| ^(?P<mlfid>(?:\[\])?\s*(?:<[^.]+\.[^.]+>\s+)?(?:\S+\s+)?(?:kernel:\s?\[ *\d+\.\d+\]:?\s+)?(?:@vserver_\S+\s+)?(?:(?:(?:\[\d+\])?:\s+[\[\(]?sshd(?:\(\S+\))?[\]\)]?:?|[\[\(]?sshd(?:\(\S+\))?[\]\)]?:?(?:\[\d+\])?:?)\s+)?(?:\[ID \d+ \S+\]\s+)?)(?:(?:error|fatal): (?:PAM: )?)?(?P<content>.+)$
`-
Failregex: 20 total
|- #) [# of hits] regular expression
| 4) [7] ^Failed (?:<F-NOFAIL>publickey</F-NOFAIL>|\S+) for (?P<cond_inv>invalid user )?<F-USER>(?P<cond_user>\S+)|(?(cond_inv)(?:(?! from ).)*?|[^:]+)</F-USER> from <HOST>(?: (?:port \d+|on \S+)){0,2}(?: ssh\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)
| 14) [2] ^<F-NOFAIL>pam_[a-z]+\(sshd:auth\):\s+authentication failure;</F-NOFAIL>(?:\s+(?:(?:logname|e?uid|tty)=\S*)){0,4}\s+ruser=<F-ALT_USER>\S*</F-ALT_USER>\s+rhost=<HOST>(?:\s+user=<F-USER>\S*</F-USER>)?(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
| 15) [1] ^maximum authentication attempts exceeded for <F-USER>.*</F-USER> from <HOST>(?: (?:port \d+|on \S+)){0,2}(?: ssh\d*)?(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*$
| 21) [10] ^<F-MLFFORGET><F-MLFGAINED>Accepted \w+</F-MLFGAINED></F-MLFFORGET> for <F-USER>\S+</F-USER> from <HOST>(?:\s|$)
`-
Ignoreregex: 0 total
Date template hits:
|- [# of hits] date format
| [10544] {^LN-BEG}(?:DAY )?MON Day %k:Minute:Second(?:\.Microseconds)?(?: ExYear)?
`-
Lines: 10544 lines, 12 ignored, 8 matched, 10524 missed
[processed in 2.80 sec]
|- Ignored line(s):
| Nov 1 16:19:05 debian sshd[3492659]: Accepted password for bob from 192.168.178.33 port 56356 ssh2
| Nov 1 16:19:05 debian sshd[3492714]: Accepted password for bob from 192.168.178.33 port 56358 ssh2
| Nov 2 08:20:09 debian sshd[3739170]: Accepted password for bob from 192.168.178.202 port 2791 ssh2
| Nov 2 08:20:10 debian sshd[3739196]: Accepted password for bob from 192.168.178.202 port 2795 ssh2
| Nov 2 10:46:36 debian sshd[3776281]: Accepted password for bob from 192.168.178.202 port 27977 ssh2
| Nov 2 10:46:36 debian sshd[3776291]: Accepted password for bob from 192.168.178.202 port 27980 ssh2
| Nov 3 07:32:23 debian sshd[4072434]: Accepted password for bob from 192.168.178.202 port 17932 ssh2
| Nov 3 07:32:23 debian sshd[4072456]: Accepted password for bob from 192.168.178.202 port 17939 ssh2
| Nov 3 10:18:20 debian sshd[4150841]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.178.202 user=root
| Nov 3 10:49:02 debian sshd[4158907]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.178.202 user=root
| Nov 3 10:49:29 debian sshd[4159096]: Accepted password for bob from 192.168.178.202 port 19008 ssh2
| Nov 3 10:49:29 debian sshd[4159106]: Accepted password for bob from 192.168.178.202 port 19009 ssh2
`-
Missed line(s): too many to print. Use --print-all-missed to print all 10524 lines
Wenn die maximale Anzahl an fehlerhaften Login-Versuchen erreicht wird, kommen die Aktionen zum tragen. Dazu finden sich Standard-Actions in der jail.local, die Einträge in den iptables machen und somit IP-Adressen bannen (sperren) und nach der festgelegten zeit auch wieder entsperren
...
# Default banning action (e.g. iptables, iptables-new,
# iptables-multiport, shorewall, etc) It is used to define
# action_* variables. Can be overridden globally or per
# section within jail.local file
banaction = iptables-multiport
banaction_allports = iptables-allports
...
Dies schaut bei mir z.B. derzeit so aus (Auszug aus der Chain f2b-sshd):
# iptables -L
...
Chain f2b-sshd (1 references)
target prot opt source destination
REJECT all -- 61.177.172.89 anywhere reject-with icmp-port-unreachable
REJECT all -- 181.115.146.34 anywhere reject-with icmp-port-unreachable
REJECT all -- clientanalyticscampaigns.com anywhere reject-with icmp-port-unreachable
REJECT all -- v017051.ppp.asahi-net.or.jp anywhere reject-with icmp-port-unreachable
REJECT all -- 122.194.229.40 anywhere reject-with icmp-port-unreachable
REJECT all -- 112.85.42.28 anywhere reject-with icmp-port-unreachable
REJECT all -- 122.194.229.54 anywhere reject-with icmp-port-unreachable
REJECT all -- 167.71.234.157 anywhere reject-with icmp-port-unreachable
REJECT all -- 177-139-137-190.dsl.telesp.net.br anywhere reject-with icmp-port-unreachable
REJECT all -- 106.75.126.6 anywhere reject-with icmp-port-unreachable
REJECT all -- 20.71.193.60 anywhere reject-with icmp-port-unreachable
REJECT all -- 106.12.80.223 anywhere reject-with icmp-port-unreachable
...
Dies funktioniert in dieser Form für relativ viele Dienste, die direkt auf dem Host laufen und fertig vorkonfiguriert sind. Bei Docker-Containern verhält es sich deutlich anders. Das fängt schon damit an, das die Log-Dateien der einzelnen Dockerinstanzen nicht im Standardverzeichnis auf dem Host liegen, sondern „irgendwo“ im Dockercontainer. Da eine deiner ersten Docker-Instanzen vermutlich eine WordPress-Installation ist, werde ich dir die Konfiguration daran erläutern. Zuerst gilt es also herauszufinden, wo die auszuwertende Logdatei zu finden ist. Dazu suchst du den passenden Containernamen heraus
# docker ps | grep wordpress
24c76f4215ba wordpress:latest "docker-entrypoint.s…" 3 months ago Up 3 weeks 80/tcp
Das wird bei dir ähnlich aussehen, das entscheidende ist gleich am Anfang die Container-ID (die ist bei dir garantiert eine andere). Mit docker logs 24c76f4215ba kannst du dir das Logfile auch vom Host aus ansehen.
Wie kommt fail2ban jetzt aber an das Log-File aus dem Docker-Container? Denn das Logfile liegt ja im Container und fail2ban läuft in unserem Fall auf dem Host. Aber auch hier gibt es einen Kniff. Docker legt nämlich die Konfigurations-Dateien, aber auch das Logfile zu jeder einzelnen Docker-Instanz im Verzeichnis /var/lib/docker/container/<CONTAINER-ID>/ ab (Datei CONTAINER-ID-json.log) ab:
# ls -la /var/lib/docker/containers/24c76f4215ba*/
insgesamt 89924
drwx--x--- 4 root root 4096 15. Jan 08:58 .
drwx--x--- 30 root root 4096 29. Jan 08:04 ..
-rw-r----- 1 root root 92030280 6. Feb 11:59 24c76f4215ba5b2a680e7dcd27704d01db23af931c6a4251c685744022385336-json.log
drwx------ 2 root root 4096 20. Okt 12:33 checkpoints
-rw------- 1 root root 4583 15. Jan 08:58 config.v2.json
-rw-r--r-- 1 root root 1974 15. Jan 08:58 hostconfig.json
-rw-r--r-- 1 root root 13 15. Jan 08:58 hostname
-rw-r--r-- 1 root root 174 15. Jan 08:58 hosts
drwx--x--- 2 root root 4096 20. Okt 12:33 mounts
-rw-r--r-- 1 root root 38 15. Jan 08:58 resolv.conf
-rw-r--r-- 1 root root 71 15. Jan 08:58 resolv.conf.hash
Da wir jetzt den Pfad und den Namen des Logfile (hier unser WordPress-Container) haben, können wir diesen nach ungewöhnlichen Ausdrücken druchsuchen. Mit dem Wissen, das die Administrations-Oberfläche von WordPress standardmäßig über den Link https://URL/wp-login.php aufgerufen wird, ist dies ein erster Ansatz, um nach Anomalien zu suchen.
root@kwellkorn:~# cat /var/lib/docker/containers/24c76f4215ba*/24c76f4215ba*-json.log | grep wp-login.php
...
{"log":"39.116.218.200 - - [04/Feb/2022:12:10:39 +0000] \"POST /wp-login.php HTTP/1.1\" 200 3217 \"https://kwellkorn.de/wp-login.php\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\"\n","stream":"stdout","time":"2022-02-04T12:10:41.193149376Z"}
...
Dies ist natürlich nur ein ganz kleiner Auszug aus meiner Log-Datei, das entscheidende ist die IP (hier 39.116.218.200), denn die ist es in diesem konkreten Beispiel, die gesperrt werden soll. Wenn du dir diese Zeile mal im Vergleich zu weiteren bei dir aufgelisteten näher ansiehst, kannst du mit ein wenig Wissen zum Thema „Regular Expression“ gezielt nach diesen Zeichenketten filtern, da bestimmte Zeichenketten sich pro Zeile immer wiederholen. Und so erstellen wir uns einen neuen Filter:
# nano /etc/fail2ban/filter.d/wplogin.conf
[Definition]
failregex = {"log":"<HOST> -.*POST.*wp-login.php.*
ignoreregex =
Den soeben erstellten Filter testen wir erst einmal, dafür hat fail2ban auch einen passendes Tool mit etlichen Zusatzfeatures parat:
# fail2ban-regex -v /var/lib/docker/containers/24c76f4215ba*/24c76f4215ba*-json.log /etc/fail2ban/filter.d/wplogin.conf
Running tests
=============
Use failregex filter file : wplogin, basedir: /etc/fail2ban
Use log file : /var/lib/docker/containers/24c76f4215ba5b2a680e7dcd27704d01db23af931c6a4251c685744022385336/24c76f4215ba5b2a680e7dcd27704d01db23af931c6a4251c685744022385336-json.log
Use encoding : UTF-8
Results
=======
Failregex: 5732 total
|- #) [# of hits] regular expression
| 1) [5732] {"log":"<HOST> -.*POST.*wp-login.php.*
| 167.71.207.186 Wed Oct 20 15:20:56 2021
| 103.56.149.232 Wed Oct 20 15:29:54 2021
| 35.201.240.201 Wed Oct 20 15:40:04 2021
...
| 222.151.46.103 Fri Feb 04 13:09:50 2022
| 222.151.46.103 Fri Feb 04 13:09:54 2022
| 175.206.76.253 Fri Feb 04 13:09:56 2022
| 211.210.217.92 Fri Feb 04 13:09:59 2022
| 221.150.227.75 Fri Feb 04 13:10:02 2022
| 221.150.227.75 Fri Feb 04 13:10:07 2022
| 191.53.195.227 Fri Feb 04 13:10:10 2022
| 74.120.174.135 Fri Feb 04 13:10:15 2022
| 191.14.179.183 Fri Feb 04 13:10:19 2022
| 191.14.179.183 Fri Feb 04 13:10:23 2022
| 112.137.123.5 Fri Feb 04 13:10:25 2022
| 126.62.86.159 Fri Feb 04 13:10:28 2022
| 77.123.121.197 Fri Feb 04 13:10:31 2022
| 177.190.75.145 Fri Feb 04 13:10:35 2022
| 39.116.218.200 Fri Feb 04 13:10:39 2022
...
Okay, der Filter funktioniert und kann zum suchen nach unerwünschten IP-Adressen genutzt werden. Interessant in meinem Beispiel oben, wieviel gescheiterten Login-Versuche alleine in den 2 Minuten am 04. Februar waren, oder? Ein Grund mehr, dem mal einen Riegel vorzuschieben.
Also erstellen wir uns als nächstes einen neuen Jail und editieren dazu die Datei /etc/fail2ban/jail.local bzw. ergänzen ganz am Ende den Eintrag [wplogin]
#nano /etc/fail2ban/jail.local
...
[wplogin]
enabled = true
port = http,https
filter = wplogin
logpath = /var/lib/docker/containers/24c76f4215ba*/24c76f4215ba*-json.log
banaction = wplogin-action
maxretry = 5
findtime = 120
bantime = 86400
Mit enabled = true wird das jail aktiviert. Wir suchen gezielt nach Anfragen auf Port 80 oder 443 (http(s), mittels des Filters wplogin (den haben wir in der wplogin.conf in einem der letzten Schritte erstellt), in der angegebenen WordPress-Logdatei unseres Containers. Bei einen Treffer wird die Action wplogin-action (auch hier kannst du dir das .conf am Ende von wplogin-action sparen) ausgeführt und es müssen noch 5 gescheiterte Loginversuche in 120 Sekunden als Bedingung erfüllt sein. Wenn das alles zusammenkommt, ist die IP für 86400 Sekunden weg vom Fenster 🙂
Als nächstes erstellst du die Datei /etc/fail2ban/action.d/docker-action.conf und legst Standard-Aktionen (generieren von Chains für unsere WordPress-Instanz innerhalb von iptables) fest. Weiterhin was passieren soll, wenn alle Bedingungen erfüllt wurden und die IP gesperrt werden soll, mit welchen Kommandos soll die Sperre aufgehoben werden:
# nano /etc/fail2ban/action.d/wplogin-action.conf
[Definition]
actionstart = iptables -N f2b-wplogin
iptables -A f2b-wplogin -j RETURN
iptables -I FORWARD -p tcp -m multiport --dports 80 -j f2b-wplogin
actionstop = iptables -D FORWARD -p tcp -m multiport --dports 80 -j f2b-wplogin
iptables -F f2b-wplogin
iptables -X f2b-wplogin
actioncheck = iptables -n -L FORWARD | grep -q 'f2b-wplogin[ \t]'
actionban = iptables -I f2b-wplogin 1 -s <ip> -j DROP
actionunban = iptables -D f2b-wplogin -s <ip> -j DROP
Jetzt muss du den fail2ban-Dienst nur noch neu starten und sowohl sshd als auch die WordPress-Docker-Instanz sind abgesichert.
# systemctl restart fail2ban
# fail2ban-client reload
Das kannst auch auch leicht überprüfen:
# systemctl status fail2ban.service
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; vendor preset: enabled)
Active: active (running) since Sat 2022-02-05 08:28:42 CET; 1 day 6h ago
Docs: man:fail2ban(1)
Process: 980282 ExecStartPre=/bin/mkdir -p /run/fail2ban (code=exited, status=0/SUCCESS)
Main PID: 980283 (fail2ban-server)
Tasks: 7 (limit: 9510)
Memory: 17.1M
CPU: 1min 43.730s
CGroup: /system.slice/fail2ban.service
└─980283 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
# fail2ban-client status
Status
|- Number of jail: 2
`- Jail list: sshd, wplogin
# fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 2871
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 81
|- Total banned: 552
`- Banned IP list: 211.108.51.122 103.39.222.186 180.167.214.190 163.114.131.64 43.155.77.170 5.101.99.198 167.99.96.114 37.186.117.123 221.181.185.94 112.85.42.88 112.85.42.15 206.189.119.230 82.196.5.221 121.4.92.162 219.145.61.20 43.155.62.136 118.34.14.126 43.155.68.187 106.53.91.250 54.39.235.200 147.182.137.182 112.85.42.73 122.194.229.38 106.75.84.194 157.230.151.241 117.50.174.243 43.154.44.40 43.154.85.208 112.85.42.119 202.65.69.193 112.85.42.227 43.135.160.246 143.198.23.4 104.207.132.131 178.128.248.121 89.175.29.126 38.72.132.227 49.235.123.184 195.19.102.173 121.65.121.149 221.181.185.111 2.82.234.17 112.85.42.128 93.39.225.138 43.154.129.214 222.187.238.58 221.181.185.143 112.85.42.89 190.145.12.233 122.194.229.45 117.122.212.78 49.235.92.187 49.234.190.10 179.43.80.6 113.128.122.98 221.122.119.79 159.65.156.27 49.88.112.60 111.230.201.59 49.232.173.143 85.155.150.76 194.163.147.71 78.159.98.209 143.198.167.99 141.136.47.251 167.86.107.85 157.230.105.246 117.111.1.42 122.194.229.40 43.154.57.141 112.85.42.87 150.95.25.221 47.176.104.74 190.247.83.170 221.181.185.151 82.156.12.198 221.181.185.159 218.188.85.193 35.199.73.100 186.4.222.45 139.155.69.95
# fail2ban-client status wplogin
Status for the jail: wplogin
|- Filter
| |- Currently failed: 1
| |- Total failed: 3
| `- File list: /var/lib/docker/containers/24c76f4215ba5b2a680e7dcd27704d01db23af931c6a4251c685744022385336/24c76f4215ba5b2a680e7dcd27704d01db23af931c6a4251c685744022385336-json.log
`- Actions
|- Currently banned: 1
|- Total banned: 42
`- Banned IP list: 202.65.69.193
Wie du siehst, sind jetzt schon 2 Dienste mit fail2ban abgesichert, SSH und WordPress (als Docker-Instanz).
Im Grunde ist es immer wieder dasselbe, ich fasse zusammen:
- Du erstellst dir in der jail.conf einen neuen jail und definierst die Bedingungen, den Filter, die Action und die Timer
- Du suchst das zum Dienst zugehörige Logfile und suchst nach Fehlermeldungen und in diesen Zeilen nach wiederkehrenden Ausdrücken , an Hand du den HOST (also die gesuchte IP-Adresse) herausfiltern kannst
- Du definierst eine regular expression auf Grundlage der in Schritt 2 erlangten Informationen
- Du definierst eine Action, die dir grundsätzlich die notwendigen Chains in iptables anlegt und anschließend, was passieren sollm wenn ein Client gesperrt werden soll oder dessen Sperre wieder aufgehoben wird.
Schauen wir uns noch ein weiteres Beispiel an. Auf meinem selbst betriebenen DNS-Server (siehe dazu den passenden Blog-Beitrag Adguard Home) kommen seit geraumer Zeit täglich mehrere 10000 DNS-Anfragen täglich z.B. für pizzaseo.com. Dabei handelt es sich um gezielte, automatisierte DDoS-DNS-Angriffe auf meinen Server. Da ich meinen DNS-Server allgemein verfügbar halten wollte, bin ich dazu übergegangen, die betroffenen Hosts seperat in Adguard Home zu sperren, irgendwann wurde mir das aber zu aufwändig und ich wollte den Prozess automatisieren. Also habe ich ein entsprechendes Jail in Fail2Ban erstellt und überlasse dieses Kampffeld jetzt auch diesem Service.
Aber schauen wir uns das doch einmal im Detail an. Das passende Logfile von Adguard Home liegt bei mir unter /var/adguard/work/data/querylog.json.
# cat /var/adguardhome/work/data/querylog.json | grep pizzaseo.com
...
{"T":"2022-02-10T17:12:46.444293902Z","QH":"pizzaseo.com","QT":"RRSIG","QC":"IN","CP":"","Answer":"f0+BgwABAAAAAQAACHBpenphc2VvA2NvbQAALgABCHBpenphc2VvA2NvbQAABgABAAAACgBUGWZha2UtZm9yLW5lZ2F0aXZlLWNhY2hpbmcHYWRndWFyZANjb20ACmhvc3RtYXN0ZXIIcGl6emFzZW8DY29tAAABiJQAAAcIAAADhAAJOoAAAVGA","Result":{"IsFiltered":true,"Reason":3,"Rules":[{"Text":"||pizzaseo.com^$important"}]},"IP":"84.63.6.237","Elapsed":24245}
{"T":"2022-02-10T17:12:46.444367573Z","QH":"pizzaseo.com","QT":"RRSIG","QC":"IN","CP":"","Answer":"f0+BgwABAAAAAQAACHBpenphc2VvA2NvbQAALgABCHBpenphc2VvA2NvbQAABgABAAAACgBUGWZha2UtZm9yLW5lZ2F0aXZlLWNhY2hpbmcHYWRndWFyZANjb20ACmhvc3RtYXN0ZXIIcGl6emFzZW8DY29tAAABiJQAAAcIAAADhAAJOoAAAVGA","Result":{"IsFiltered":true,"Reason":3,"Rules":[{"Text":"||pizzaseo.com^$important"}]},"IP":"84.63.6.237","Elapsed":779779}
{"T":"2022-02-10T17:12:46.444372101Z","QH":"pizzaseo.com","QT":"RRSIG","QC":"IN","CP":"","Answer":"f0+BgwABAAAAAQAACHBpenphc2VvA2NvbQAALgABCHBpenphc2VvA2NvbQAABgABAAAACgBUGWZha2UtZm9yLW5lZ2F0aXZlLWNhY2hpbmcHYWRndWFyZANjb20ACmhvc3RtYXN0ZXIIcGl6emFzZW8DY29tAAABiJQAAAcIAAADhAAJOoAAAVGA","Result":{"IsFiltered":true,"Reason":3,"Rules":[{"Text":"||pizzaseo.com^$important"}]},"IP":"84.63.6.237","Elapsed":810637}
{"T":"2022-02-10T17:12:58.980888349Z","QH":"pizzaseo.com","QT":"RRSIG","QC":"IN","CP":"","Answer":"QoGBgwABAAAAAQAACHBpenphc2VvA2NvbQAALgABCHBpenphc2VvA2NvbQAABgABAAAACgBUGWZha2UtZm9yLW5lZ2F0aXZlLWNhY2hpbmcHYWRndWFyZANjb20ACmhvc3RtYXN0ZXIIcGl6emFzZW8DY29tAAABiJQAAAcIAAADhAAJOoAAAVGA","Result":{"IsFiltered":true,"Reason":3,"Rules":[{"Text":"||pizzaseo.com^$important"}]},"IP":"93.218.214.246","Elapsed":129275}
{"T":"2022-02-10T17:12:58.981049294Z","QH":"pizzaseo.com","QT":"RRSIG","QC":"IN","CP":"","Answer":"QoGBgwABAAAAAQAACHBpenphc2VvA2NvbQAALgABCHBpenphc2VvA2NvbQAABgABAAAACgBUGWZha2UtZm9yLW5lZ2F0aXZlLWNhY2hpbmcHYWRndWFyZANjb20ACmhvc3RtYXN0ZXIIcGl6emFzZW8DY29tAAABiJQAAAcIAAADhAAJOoAAAVGA","Result":{"IsFiltered":true,"Reason":3,"Rules":[{"Text":"||pizzaseo.com^$important"}]},"IP":"93.218.214.246","Elapsed":54183}
{"T":"2022-02-10T17:12:58.981137181Z","QH":"pizzaseo.com","QT":"RRSIG","QC":"IN","CP":"","Answer":"QoGBgwABAAAAAQAACHBpenphc2VvA2NvbQAALgABCHBpenphc2VvA2NvbQAABgABAAAACgBUGWZha2UtZm9yLW5lZ2F0aXZlLWNhY2hpbmcHYWRndWFyZANjb20ACmhvc3RtYXN0ZXIIcGl6emFzZW8DY29tAAABiJQAAAcIAAADhAAJOoAAAVGA","Result":{"IsFiltered":true,"Reason":3,"Rules":[{"Text":"||pizzaseo.com^$important"}]},"IP":"93.218.214.246","Elapsed":123243}
...
Was nämlich diese DNS-Anfragen alle gemeinsam haben, ist der Umstand, das diese allesamt für pizzaseo.com gestellt werden. Da das Logfile immer dieselbe Struktur hat, lässt sich mit Regular Expression sehr gut nach den IPs suchen, von denen diese Attacken ausgehen. Die Menge an Suchanfragen nach pizzaseo.com ist nicht zu übersehen, glaubt mir.
Und das, obwohl ich bereits etliche Hosts einzeln über die Client-Konfiguration blockiere und Anfragen auf pizzaseo.com grundsätzlich nicht beantworte. Aber das löst ja nicht das eigentliche Problem, denn die DNS-Anfrage wird ja trotzdem gestellt und belastet den Server. Also bauen wir uns einen Filter (hier kann ich regex101.com empfehlen).
# nano /etc/fail2ban/filter.d/adguardhome.conf
[Definition]
failregex = "pizzaseo.com".*"IP":"<HOST>".*
ignoreregex =
Ich suche in meinem Fall (bei dir kann das Logfile anders aussehen, da in meinem Fall die Domain pizzaseo.com im Adguard-DNS-Filter ohnehin schon geblockt wird und dieser Vorgang auch im Logfile so dokumentiert wird) also nach dem Ausdruck pizzaseo.com, gefolgt von einer Wildcard (.*), gefolgt von dem Ausdruck „IP“:“ und dann der eigentlichen IP (<HOST>). Der Rest der Zeile filter ich wieder mittels einer Wildcard (.*) weg. Was also bleibt ist die IP-Adresse des Angreifers (wer stellt sonst DNS-Anfragen für pizzaseo.com :-)) und das testen wir auch gleich:
# fail2ban-regex -v /var/adguardhome/work/data/querylog.json /etc/fail2ban/filter.d/adguardhome.conf
Running tests
=============
Use failregex filter file : adguardhome, basedir: /etc/fail2ban
Use log file : /var/adguardhome/work/data/querylog.json
Use encoding : UTF-8
Results
=======
Failregex: 890 total
|- #) [# of hits] regular expression
| 1) [890] "pizzaseo.com".*"IP":"<HOST>".*
| 94.134.196.37 Sun Feb 06 23:09:00 2022
| 94.134.196.37 Sun Feb 06 23:09:00 2022
| 94.134.196.37 Sun Feb 06 23:09:00 2022
| 94.134.196.37 Sun Feb 06 23:09:00 2022
| 94.134.196.37 Sun Feb 06 23:09:00 2022
...
| 84.63.6.237 Thu Feb 10 17:13:54 2022
| 84.63.6.237 Thu Feb 10 17:13:54 2022
| 84.63.6.237 Thu Feb 10 17:13:54 2022
| 84.63.6.237 Thu Feb 10 17:13:54 2022
| 84.63.6.237 Thu Feb 10 17:13:54 2022
| 84.63.6.237 Thu Feb 10 17:13:54 2022
| 84.63.6.237 Thu Feb 10 17:13:54 2022
| 84.63.6.237 Thu Feb 10 17:13:54 2022
`-
Ignoreregex: 0 total
Date template hits:
|- [# of hits] date format
| [282004] ExYear(?P<_sep>[-/.])Month(?P=_sep)Day(?:T| ?)24hour:Minute:Second(?:[.,]Microseconds)?(?:\s*Zone offset)?
| [0] {^LN-BEG}ExYear(?P<_sep>[-/.])Month(?P=_sep)Day(?:T| ?)24hour:Minute:Second(?:[.,]Microseconds)?(?:\s*Zone offset)?
| [0] {^LN-BEG}(?:DAY )?MON Day %k:Minute:Second(?:\.Microseconds)?(?: ExYear)?
| [0] {^LN-BEG}(?:DAY )?MON Day ExYear %k:Minute:Second(?:\.Microseconds)?
| [0] {^LN-BEG}Day(?P<_sep>[-/])Month(?P=_sep)(?:ExYear|ExYear2) %k:Minute:Second
| [0] {^LN-BEG}Day(?P<_sep>[-/])MON(?P=_sep)ExYear[ :]?24hour:Minute:Second(?:\.Microseconds)?(?: Zone offset)?
| [0] {^LN-BEG}Month/Day/ExYear:24hour:Minute:Second
| [0] {^LN-BEG}Month-Day-ExYear %k:Minute:Second(?:\.Microseconds)?
| [0] {^LN-BEG}Epoch
| [0] {^LN-BEG}ExYear2ExMonthExDay ?24hour:Minute:Second
| [0] {^LN-BEG}MON Day, ExYear 12hour:Minute:Second AMPM
| [0] {^LN-BEG}ExYearExMonthExDay(?:T| ?)Ex24hourExMinuteExSecond(?:[.,]Microseconds)?(?:\s*Zone offset)?
| [0] {^LN-BEG}(?:Zone name )?(?:DAY )?MON Day %k:Minute:Second(?:\.Microseconds)?(?: ExYear)?
| [0] {^LN-BEG}(?:Zone offset )?(?:DAY )?MON Day %k:Minute:Second(?:\.Microseconds)?(?: ExYear)?
| [0] {^LN-BEG}TAI64N
| [0] (?:DAY )?MON Day %k:Minute:Second(?:\.Microseconds)?(?: ExYear)?
| [0] (?:DAY )?MON Day ExYear %k:Minute:Second(?:\.Microseconds)?
| [0] Day(?P<_sep>[-/])Month(?P=_sep)(?:ExYear|ExYear2) %k:Minute:Second
| [0] Day(?P<_sep>[-/])MON(?P=_sep)ExYear[ :]?24hour:Minute:Second(?:\.Microseconds)?(?: Zone offset)?
| [0] Month/Day/ExYear:24hour:Minute:Second
| [0] Month-Day-ExYear %k:Minute:Second(?:\.Microseconds)?
| [0] Epoch
| [0] {^LN-BEG}24hour:Minute:Second
| [0] ^<Month/Day/ExYear2@24hour:Minute:Second>
| [0] ExYear2ExMonthExDay ?24hour:Minute:Second
| [0] MON Day, ExYear 12hour:Minute:Second AMPM
| [0] ^MON-Day-ExYear2 %k:Minute:Second
| [0] ExYearExMonthExDay(?:T| ?)Ex24hourExMinuteExSecond(?:[.,]Microseconds)?(?:\s*Zone offset)?
| [0] (?:Zone name )?(?:DAY )?MON Day %k:Minute:Second(?:\.Microseconds)?(?: ExYear)?
| [0] (?:Zone offset )?(?:DAY )?MON Day %k:Minute:Second(?:\.Microseconds)?(?: ExYear)?
| [0] TAI64N
`-
Lines: 282004 lines, 0 ignored, 890 matched, 281114 missed
[processed in 8.48 sec]
Missed line(s): too many to print. Use --print-all-missed to print all 281114 lines
Die IP-Adressen oben sind nur ein kleiner Auszug (insgesamt wurden über 28000 Einträge gefunden). Als nächstes konfigurieren wir uns ein Jail. Dazu öffnest du die jail.local und hängst am Ende der Datei folgenden Inhalt an (natürlich ohne die 3 Punkte) und erstellst im Anschluss noch die passende Action
# nano /etc/fail2ban/jail.local
...
[adguardhome]
enabled = true
port = domain,domain-s
filter = adguardhome
logpath = /var/adguardhome/work/data/querylog.json
banaction = adguard-action
maxretry = 15
findtime = 600
bantime = 86400
# nano /etc/fail2ban/action.d/adguard-action.conf
[Definition]
actionstart = iptables -N f2b-adguard
iptables -A f2b-adguard -j RETURN
iptables -I FORWARD -p udp -m multiport --dports 53,853 -j f2b-adguard
actionstop = iptables -D FORWARD -p udp -m multiport --dports 53,853 -j f2b-adguard
iptables -F f2b-adguard
iptables -X f2b-adguard
actioncheck = iptables -n -L FORWARD | grep -q 'f2b-adguard[ \t]'
actionban = iptables -I f2b-adguard 1 -s <ip> -j DROP
actionunban = iptables -D f2b-adguard -s <ip> -j DROP
Wie du siehst, hat das Jail fast dieselbe Konfiguration wie für wplogin (lediglich die Ports, der Filter und der Pfad zur Logdatei sind anders. Anschließend starten wir den Dienst fail2ban neu.
# systemctl restart fail2ban.service
# systemctl status fail2ban.service
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; vendor preset: enabled)
Active: active (running) since Sat 2022-02-12 10:31:38 CET; 2s ago
Docs: man:fail2ban(1)
Process: 864104 ExecStartPre=/bin/mkdir -p /run/fail2ban (code=exited, status=0/SUCCESS)
Main PID: 864105 (fail2ban-server)
Tasks: 9 (limit: 9510)
Memory: 12.8M
CPU: 171ms
CGroup: /system.slice/fail2ban.service
└─864105 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
Ich weiß, alles in allem viel zu tun. Aber du kannst anschließend, also wenn der Kopf nicht mehr raucht, wieder ruhiger schlafen.