#!/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 "$@"