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:
commit
d7dcd9d6b1
29 files changed
+1163
No files matched your search
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# npm-scanner – watch the NPM log directories for new hosts
|
||||
# https://git.retronerd.at/schiwagoa/npm-scanner
|
||||
#
|
||||
# fail2ban resolves logpath globs only when a jail starts. A proxy host created later
|
||||
# in NPM would therefore not be watched. This script reloads the jails as soon as a
|
||||
# log file appears that no jail watches yet. Bans survive the reload.
|
||||
#
|
||||
# npm-newlog-watch.sh <dir> [<dir> …]
|
||||
# NPM_SCANNER_JAILS="npm-scanner npm-wplogin" jails that read NPM logs
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
JAILS=${NPM_SCANNER_JAILS:-npm-scanner}
|
||||
SETTLE=${NEWLOG_SETTLE_SECONDS:-5}
|
||||
|
||||
log() { echo "[npm-newlog] $*"; }
|
||||
|
||||
[[ $# -gt 0 ]] || { log "no directories given"; exit 1; }
|
||||
|
||||
# Wait until the fail2ban server answers (max. 5 minutes).
|
||||
for _ in $(seq 1 300); do
|
||||
fail2ban-client ping >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
if ! fail2ban-client ping >/dev/null 2>&1; then
|
||||
log "fail2ban does not answer – watcher stopped"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Same mode the crazymax entrypoint chose (nft or legacy) is active by now.
|
||||
chain=$(grep -m1 -oE 'chain=[A-Za-z0-9_-]+' /etc/fail2ban/jail.local | cut -d= -f2)
|
||||
if [[ -n "$chain" ]] && ! iptables -n -L "$chain" >/dev/null 2>&1; then
|
||||
log "WARNING: iptables chain $chain does not exist – bans will fail. Is Docker running on the host?"
|
||||
fi
|
||||
|
||||
watched() {
|
||||
local file=$1 jail
|
||||
for jail in $JAILS; do
|
||||
fail2ban-client get "$jail" logpath 2>/dev/null | grep -qF -- "$file" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
declare -A tried=()
|
||||
|
||||
log "watching $* (jails: $JAILS)"
|
||||
inotifywait -m -q -e create -e moved_to --format '%w%f' "$@" | while read -r file; do
|
||||
case "${file##*/}" in
|
||||
proxy-host-*_access.log|default-host_access.log|fallback_access.log|fallback_http_access.log) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
[[ -n "${tried[$file]:-}" ]] && continue
|
||||
|
||||
# Give NPM time to finish writing the config, and debounce log rotation.
|
||||
sleep "$SETTLE"
|
||||
[[ -f "$file" ]] || continue
|
||||
watched "$file" && continue
|
||||
|
||||
tried[$file]=1
|
||||
for jail in $JAILS; do
|
||||
# npm-wplogin only reads proxy-host logs.
|
||||
[[ "$jail" == npm-wplogin && "${file##*/}" != proxy-host-* ]] && continue
|
||||
if fail2ban-client reload "$jail" >/dev/null 2>&1; then
|
||||
log "new log $file – $jail reloaded"
|
||||
else
|
||||
log "WARNING: reload of $jail failed for $file"
|
||||
fi
|
||||
done
|
||||
done
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env bash
|
||||
# npm-scanner – container entrypoint
|
||||
# https://git.retronerd.at/schiwagoa/npm-scanner
|
||||
#
|
||||
# Builds /etc/fail2ban/jail.local from the environment, starts the log watcher
|
||||
# and hands over to the crazymax entrypoint (/entrypoint.sh), which starts fail2ban.
|
||||
#
|
||||
# npm-scanner-init.sh fail2ban-server … normal start (CMD of the image)
|
||||
# npm-scanner-init.sh check write config, run `fail2ban-client -t`, print jail.local, exit
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TPL_DIR=/etc/npm-scanner/jail.tpl
|
||||
JAIL_LOCAL=/etc/fail2ban/jail.local
|
||||
LOG1=/logs/npm1
|
||||
LOG2=/logs/npm2
|
||||
|
||||
log() { echo "[npm-scanner] $*"; }
|
||||
die() { echo "[npm-scanner] ERROR: $*" >&2; exit 1; }
|
||||
|
||||
# --- Settings --------------------------------------------------------------
|
||||
|
||||
WHITELIST=${WHITELIST:-}
|
||||
F2B_CHAIN=${F2B_CHAIN:-DOCKER-USER}
|
||||
|
||||
SCANNER_MAXRETRY=${SCANNER_MAXRETRY:-3}
|
||||
SCANNER_FINDTIME=${SCANNER_FINDTIME:-1h}
|
||||
SCANNER_BANTIME=${SCANNER_BANTIME:-24h}
|
||||
SCANNER_MAXTIME=${SCANNER_MAXTIME:-4w}
|
||||
|
||||
ENABLE_WPLOGIN=${ENABLE_WPLOGIN:-false}
|
||||
WPLOGIN_MAXRETRY=${WPLOGIN_MAXRETRY:-5}
|
||||
WPLOGIN_FINDTIME=${WPLOGIN_FINDTIME:-10m}
|
||||
WPLOGIN_BANTIME=${WPLOGIN_BANTIME:-24h}
|
||||
WPLOGIN_MAXTIME=${WPLOGIN_MAXTIME:-4w}
|
||||
|
||||
ENABLE_RECIDIVE=${ENABLE_RECIDIVE:-false}
|
||||
RECIDIVE_MAXRETRY=${RECIDIVE_MAXRETRY:-3}
|
||||
RECIDIVE_FINDTIME=${RECIDIVE_FINDTIME:-1w}
|
||||
RECIDIVE_BANTIME=${RECIDIVE_BANTIME:-4w}
|
||||
|
||||
ENABLE_NEWLOG_WATCH=${ENABLE_NEWLOG_WATCH:-true}
|
||||
|
||||
is_true() { [[ "${1,,}" =~ ^(1|true|yes|on)$ ]]; }
|
||||
|
||||
check_int() { [[ "$2" =~ ^[1-9][0-9]*$ ]] || die "$1 must be a positive number (got '$2')"; }
|
||||
check_time() { [[ "$2" =~ ^[1-9][0-9]*[smhdw]?$ ]] || die "$1 must be a duration like 10m, 24h, 4w (got '$2')"; }
|
||||
|
||||
check_int SCANNER_MAXRETRY "$SCANNER_MAXRETRY"
|
||||
check_time SCANNER_FINDTIME "$SCANNER_FINDTIME"
|
||||
check_time SCANNER_BANTIME "$SCANNER_BANTIME"
|
||||
check_time SCANNER_MAXTIME "$SCANNER_MAXTIME"
|
||||
check_int WPLOGIN_MAXRETRY "$WPLOGIN_MAXRETRY"
|
||||
check_time WPLOGIN_FINDTIME "$WPLOGIN_FINDTIME"
|
||||
check_time WPLOGIN_BANTIME "$WPLOGIN_BANTIME"
|
||||
check_time WPLOGIN_MAXTIME "$WPLOGIN_MAXTIME"
|
||||
check_int RECIDIVE_MAXRETRY "$RECIDIVE_MAXRETRY"
|
||||
check_time RECIDIVE_FINDTIME "$RECIDIVE_FINDTIME"
|
||||
check_time RECIDIVE_BANTIME "$RECIDIVE_BANTIME"
|
||||
|
||||
[[ "$F2B_CHAIN" =~ ^[A-Za-z0-9_-]{1,28}$ ]] || die "F2B_CHAIN contains invalid characters (got '$F2B_CHAIN')"
|
||||
|
||||
# Only IPs, networks and host names – no characters that could break jail.local.
|
||||
for entry in $WHITELIST; do
|
||||
[[ "$entry" =~ ^[0-9A-Za-z.:/_-]+$ ]] || die "invalid WHITELIST entry '$entry'"
|
||||
done
|
||||
[[ -n "$WHITELIST" ]] || log "WARNING: WHITELIST is empty – your own IP can be banned."
|
||||
|
||||
# --- Log directories -------------------------------------------------------
|
||||
|
||||
[[ -d "$LOG1" ]] || die "$LOG1 is missing – check NPM_LOG_DIR in .env"
|
||||
|
||||
# NPM_LOG_DIR2 defaults to NPM_LOG_DIR. Watching the same directory twice
|
||||
# would count every line twice, so the second mount is only used if it differs.
|
||||
LOG_DIRS=("$LOG1")
|
||||
if [[ -d "$LOG2" ]] && [[ "$(stat -L -c '%d:%i' "$LOG1")" != "$(stat -L -c '%d:%i' "$LOG2")" ]]; then
|
||||
LOG_DIRS+=("$LOG2")
|
||||
fi
|
||||
log "log directories: ${LOG_DIRS[*]}"
|
||||
|
||||
if ! ls "$LOG1"/proxy-host-*_access.log >/dev/null 2>&1 \
|
||||
&& { [[ ${#LOG_DIRS[@]} -lt 2 ]] || ! ls "$LOG2"/proxy-host-*_access.log >/dev/null 2>&1; }; then
|
||||
log "WARNING: no proxy-host-*_access.log found yet – is NPM_LOG_DIR correct?"
|
||||
fi
|
||||
|
||||
# One logpath entry per line, continuation lines indented for fail2ban.
|
||||
join_paths() {
|
||||
local out="" p
|
||||
for p in "$@"; do
|
||||
if [[ -z "$out" ]]; then out="$p"; else out+=$'\n'" $p"; fi
|
||||
done
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
proxy_paths=() scanner_paths=()
|
||||
for d in "${LOG_DIRS[@]}"; do
|
||||
proxy_paths+=("$d/proxy-host-*_access.log")
|
||||
done
|
||||
scanner_paths=("${proxy_paths[@]}")
|
||||
for d in "${LOG_DIRS[@]}"; do
|
||||
scanner_paths+=("$d/default-host_access.log" "$d/fallback_access.log" "$d/fallback_http_access.log")
|
||||
done
|
||||
LOGPATH_PROXY=$(join_paths "${proxy_paths[@]}")
|
||||
LOGPATH_SCANNER=$(join_paths "${scanner_paths[@]}")
|
||||
|
||||
# recidive reads the fail2ban log, so it needs a file instead of STDOUT.
|
||||
# A tail process keeps `docker logs` working.
|
||||
F2B_LOGFILE=/data/log/fail2ban.log
|
||||
if is_true "$ENABLE_RECIDIVE"; then
|
||||
mkdir -p /data/log
|
||||
touch "$F2B_LOGFILE"
|
||||
export F2B_LOG_TARGET="$F2B_LOGFILE"
|
||||
fi
|
||||
|
||||
# --- jail.local ------------------------------------------------------------
|
||||
|
||||
render() {
|
||||
local tpl
|
||||
tpl=$(<"$1")
|
||||
tpl=${tpl//@WHITELIST@/$WHITELIST}
|
||||
tpl=${tpl//@F2B_CHAIN@/$F2B_CHAIN}
|
||||
tpl=${tpl//@F2B_LOGFILE@/$F2B_LOGFILE}
|
||||
tpl=${tpl//@LOGPATH_PROXY@/$LOGPATH_PROXY}
|
||||
tpl=${tpl//@LOGPATH_SCANNER@/$LOGPATH_SCANNER}
|
||||
tpl=${tpl//@SCANNER_MAXRETRY@/$SCANNER_MAXRETRY}
|
||||
tpl=${tpl//@SCANNER_FINDTIME@/$SCANNER_FINDTIME}
|
||||
tpl=${tpl//@SCANNER_BANTIME@/$SCANNER_BANTIME}
|
||||
tpl=${tpl//@SCANNER_MAXTIME@/$SCANNER_MAXTIME}
|
||||
tpl=${tpl//@WPLOGIN_MAXRETRY@/$WPLOGIN_MAXRETRY}
|
||||
tpl=${tpl//@WPLOGIN_FINDTIME@/$WPLOGIN_FINDTIME}
|
||||
tpl=${tpl//@WPLOGIN_BANTIME@/$WPLOGIN_BANTIME}
|
||||
tpl=${tpl//@WPLOGIN_MAXTIME@/$WPLOGIN_MAXTIME}
|
||||
tpl=${tpl//@RECIDIVE_MAXRETRY@/$RECIDIVE_MAXRETRY}
|
||||
tpl=${tpl//@RECIDIVE_FINDTIME@/$RECIDIVE_FINDTIME}
|
||||
tpl=${tpl//@RECIDIVE_BANTIME@/$RECIDIVE_BANTIME}
|
||||
printf '%s\n' "$tpl"
|
||||
}
|
||||
|
||||
{
|
||||
render "$TPL_DIR/00-defaults.tpl"
|
||||
render "$TPL_DIR/10-npm-scanner.tpl"
|
||||
is_true "$ENABLE_WPLOGIN" && render "$TPL_DIR/11-npm-wplogin.tpl"
|
||||
is_true "$ENABLE_RECIDIVE" && render "$TPL_DIR/20-recidive.tpl"
|
||||
true
|
||||
} > "$JAIL_LOCAL"
|
||||
|
||||
jails="npm-scanner"
|
||||
is_true "$ENABLE_WPLOGIN" && jails+=" npm-wplogin"
|
||||
is_true "$ENABLE_RECIDIVE" && jails+=" recidive"
|
||||
log "jails: $jails"
|
||||
|
||||
# --- Start -----------------------------------------------------------------
|
||||
|
||||
if [[ "${1:-}" == "check" ]]; then
|
||||
# Same preparation as the crazymax entrypoint, without starting the server.
|
||||
mkdir -p /data/db /data/action.d /data/filter.d /data/jail.d
|
||||
ln -sfn /data/jail.d /etc/fail2ban/jail.d
|
||||
cat "$JAIL_LOCAL"
|
||||
exec fail2ban-client -t
|
||||
fi
|
||||
|
||||
if is_true "$ENABLE_RECIDIVE"; then
|
||||
tail -F -n 0 "$F2B_LOGFILE" 2>/dev/null &
|
||||
fi
|
||||
|
||||
if is_true "$ENABLE_NEWLOG_WATCH"; then
|
||||
NPM_SCANNER_JAILS="${jails/ recidive/}" /usr/local/bin/npm-newlog-watch.sh "${LOG_DIRS[@]}" &
|
||||
fi
|
||||
|
||||
exec /entrypoint.sh "$@"
|
||||
Reference in new issue
Block a user