Initial version: npm-scanner image, filters, entrypoint, watcher, testbed

- Image based on crazymax/fail2ban:1.1.1 with inotify-tools
- Filters npm-scanner and npm-wplogin, tested with fail2ban-regex in the build
- jail.local generated from .env, optional npm-wplogin and recidive
- Watcher reloads the jails when NPM creates a new proxy host log
- check command, docs, testbed for LXC

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schiwagoaandClaude Opus 5.5 committed 2026-10-25 20:41:08 +01:00
commit d7dcd9d6b1
29 files changed
+1163

No files matched your search

@@ -0,0 +1,22 @@
# npm-scanner – fail2ban filter for Nginx Proxy Manager: scanners and probing
# https://git.retronerd.at/schiwagoa/npm-scanner
#
# Rule 1 (proxy-host-*_access.log, NPM "proxy" format):
# only 4xx answers to typical scanner paths
# [25/Oct/2026:12:13:14 +0000] - 404 404 - GET https example.com "/.env" [Client 198.51.100.7] ...
# Rule 2 (default-host / fallback log, combined format): status 444 or 400
# (access by IP or unknown host name, broken requests)
# Rule 3 (default-host / fallback log): scanner paths regardless of status (often 301 there)
#
# Deliberately NOT counted: plain 404 (Nextcloud/Matrix clients produce many legitimately)
# and redirects on the default host without a scanner path (search engine crawlers).
[Definition]
_paths = (?:/\.env|/\.git/|/\.svn/|/\.aws/|/\.docker/|/\.ssh/|/\.s3cfg|/\.npmrc|/\.DS_Store|/\.vscode/|phpinfo|/info\.php|/_ignition/|/_profiler/|/wp-content/debug\.log|/phpmyadmin|/pma/|/cgi-bin/|/server-status|/actuator/|/vendor/phpunit|/boaform|/HNAP1|/secrets\.json|/credentials\.json|/docker-compose\.ya?ml|/\.gitlab-ci\.yml|/shell\.php|/eval-stdin\.php|\.(?:sql|bak|old|swp)(?:\.gz|\.zip)?(?=["?]))
failregex = - (?:\d{3}|-) 4\d\d - [A-Z]+ https? \S+ "[^"]*%(_paths)s[^"]*" \[Client <HOST>\]
^<HOST> - \S+ .*?"[^"]*" (?:444|400)
^<HOST> - \S+ .*?"[A-Z]+ [^"]*%(_paths)s[^"]*" \d{3}
ignoreregex =
@@ -0,0 +1,11 @@
# npm-wplogin – fail2ban filter for Nginx Proxy Manager: WordPress login brute force
# https://git.retronerd.at/schiwagoa/npm-scanner
#
# Counts POST requests to wp-login.php and xmlrpc.php on every host behind this NPM.
# [25/Oct/2026:12:13:14 +0000] - 200 200 - POST https blog.example.com "/wp-login.php" [Client 198.51.100.7] ...
[Definition]
failregex = - (?:\d{3}|-) (?:200|302|403|404) - POST https? \S+ "/(?:wp-login\.php|xmlrpc\.php)[^"]*" \[Client <HOST>\]
ignoreregex =
@@ -0,0 +1,6 @@
# Generated by npm-scanner at container start – do not edit.
# Own additions and overrides: ./data/jail.d/*.local (read after this file).
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 @WHITELIST@
backend = pyinotify
@@ -0,0 +1,13 @@
# Scanners and probing. Ban in @F2B_CHAIN@ on all ports
# (Docker routes published ports past INPUT).
[npm-scanner]
enabled = true
filter = npm-scanner
logpath = @LOGPATH_SCANNER@
maxretry = @SCANNER_MAXRETRY@
findtime = @SCANNER_FINDTIME@
bantime = @SCANNER_BANTIME@
bantime.increment = true
bantime.maxtime = @SCANNER_MAXTIME@
action = iptables-allports[name=npm-scan, chain=@F2B_CHAIN@, protocol=all]
@@ -0,0 +1,12 @@
# WordPress login brute force (POST wp-login.php / xmlrpc.php).
[npm-wplogin]
enabled = true
filter = npm-wplogin
logpath = @LOGPATH_PROXY@
maxretry = @WPLOGIN_MAXRETRY@
findtime = @WPLOGIN_FINDTIME@
bantime = @WPLOGIN_BANTIME@
bantime.increment = true
bantime.maxtime = @WPLOGIN_MAXTIME@
action = iptables-allports[name=npm-wp, chain=@F2B_CHAIN@, protocol=all]
@@ -0,0 +1,10 @@
# Repeat offenders across all jails: blocked in INPUT and @F2B_CHAIN@.
[recidive]
enabled = true
logpath = @F2B_LOGFILE@
findtime = @RECIDIVE_FINDTIME@
maxretry = @RECIDIVE_MAXRETRY@
bantime = @RECIDIVE_BANTIME@
action = iptables-allports[name=recidive-input, chain=INPUT, protocol=all]
iptables-allports[name=recidive-docker, chain=@F2B_CHAIN@, protocol=all]