Files
npm-scanner/rootfs/usr/local/bin/npm-scanner-init.sh
T
schiwagoaandClaude Opus 5.5 d7dcd9d6b1 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>
2026-10-25 20:41:08 +01:00

171 lines
6.0 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "$@"