feat: plug-and-play refactor — docker-npm action, CF support, whitelist live-update

- Replace iptables-allports with docker-npm action (DOCKER-USER + xt_string
  X-Forwarded-For matching + INPUT chain) matching user's working setup
- Add telegram_notif.sh (deployed to /data/action.d/ at first run, user-editable)
- Add cloudflare.conf action; jail.cloudflare.local enabled via CF compose file
- Two compose files: docker-compose.yml (standard) and docker-compose.cloudflare.yml
- entrypoint: modprobe xt_string, DOCKER-USER chain check, CF jail auto-selection,
  telegram_notif.sh deployment to persistent volume on first run
- Fix whitelist live-update: addignoreip/delignoreip called alongside jail.local write
- Hardcode AUTOBAN_THR=75 and DEFAULT_DAYS=3 (remove env vars)
- Include Nginx Proxy Manager in both compose files with shared log bind mount
- Rewrite filters for actual NPM log format ([Client <HOST>] real IP extraction)
- Add DATA_DIR, Telegram, CF API key fields to .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 15:08:06 +00:00
parent dd7f8dd1a2
commit 920b69cfca
14 changed files with 446 additions and 224 deletions

View File

@@ -1,47 +1,46 @@
# ── F2B Control Center — environment configuration ─────────────────────────── # ── F2B Control Center — environment configuration ───────────────────────────
# Copy this file to .env and fill in your values. # cp .env.example .env then fill in your values.
# Only NPM_LOG_DIR is strictly required to get started.
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# ── Required ────────────────────────────────────────────────────────────────── # ── Data directory ────────────────────────────────────────────────────────────
# Host path where NPM data, logs, and certs are stored.
# Path to your Nginx Proxy Manager log directory on the host. # NPM logs will be at: ${DATA_DIR}/npm/logs/proxy-host-*_access.log
# This directory will be mounted read-only inside the container. DATA_DIR=./data
# Common paths:
# /opt/npm/data/logs
# /home/docker/NGINX/data/logs
# /docker/nginx-proxy-manager/data/logs
NPM_LOG_DIR=/opt/npm/data/logs
# ── Dashboard ───────────────────────────────────────────────────────────────── # ── Dashboard ─────────────────────────────────────────────────────────────────
# Port the dashboard listens on (direct host port — network_mode: host)
# Port the dashboard listens on (host port when using network_mode: host)
DASHBOARD_PORT=4000 DASHBOARD_PORT=4000
# ── AbuseIPDB integration (optional but recommended) ───────────────────────── # ── Network ───────────────────────────────────────────────────────────────────
# Enables IP reputation lookups and auto-ban by abuse score. # Comma-separated CIDRs to skip during log scanning and banning.
# Free API keys available at https://www.abuseipdb.com/ # Include your LAN, Docker bridge, and any other trusted networks.
ABUSEIPDB_API_KEY=
# Minimum AbuseIPDB confidence score (0100) to trigger auto-ban
AUTOBAN_THRESHOLD=75
# ── Log scanning ──────────────────────────────────────────────────────────────
# Default lookback window when scanning nginx logs (days)
DEFAULT_LOOKBACK_DAYS=3
# Comma-separated CIDR subnets to skip during log scanning and banning.
# Include your LAN, Docker bridge, and any trusted networks.
SUBNETS_TO_IGNORE=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 SUBNETS_TO_IGNORE=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
# ── Optional integrations ───────────────────────────────────────────────────── # ── AbuseIPDB (optional but recommended) ─────────────────────────────────────
# Enables IP reputation lookups and the AUTO-BAN feature.
# Free API keys: https://www.abuseipdb.com/
ABUSEIPDB_API_KEY=
# Webhook URL: receives a POST request on every manual ban action. # ── Telegram notifications (optional) ────────────────────────────────────────
# Payload: { "action": "ban", "ip": "1.2.3.4", "jail": "manual-bans", "ts": "..." } # Sends a message on ban/unban/start/stop events.
# Examples: Discord webhook, n8n, Slack, custom endpoint # 1. Create a bot via @BotFather → copy the token
# 2. Get your chat ID (send a message to the bot, then:
# curl https://api.telegram.org/bot<TOKEN>/getUpdates)
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
# ── Cloudflare (docker-compose.cloudflare.yml only) ───────────────────────────
# Required when using docker-compose.cloudflare.yml.
# Global API Key from: https://dash.cloudflare.com/profile/api-tokens
CF_EMAIL=
CF_APIKEY=
# ── Webhook (optional) ────────────────────────────────────────────────────────
# POST to this URL on every manual ban from the dashboard.
# Payload: { "action": "ban", "ip": "...", "jail": "manual-bans", "ts": "..." }
WEBHOOK_URL= WEBHOOK_URL=
# Path to a custom script to run after whitelist changes (e.g. Cloudflare sync). # ── Cloudflare whitelist sync (optional) ──────────────────────────────────────
# The script is executed as a background fire-and-forget process. # Path (inside the container) to a script run after any whitelist change.
# Mount your script into the container and set this path.
# CF_SYNC=/usr/local/bin/cloudflare-whitelist-sync.sh # CF_SYNC=/usr/local/bin/cloudflare-whitelist-sync.sh

View File

@@ -43,7 +43,8 @@ COPY supervisor.conf /etc/supervisor/conf.d/f2b-control-center.conf
# ── Startup and health ──────────────────────────────────────────────────────── # ── Startup and health ────────────────────────────────────────────────────────
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh
COPY healthcheck.sh /healthcheck.sh COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /entrypoint.sh /healthcheck.sh RUN chmod +x /entrypoint.sh /healthcheck.sh \
/etc/f2b-defaults/action.d/telegram_notif.sh
# ── Runtime directories ─────────────────────────────────────────────────────── # ── Runtime directories ───────────────────────────────────────────────────────
RUN mkdir -p /data /nginx-logs /var/log /var/run/fail2ban RUN mkdir -p /data /nginx-logs /var/log /var/run/fail2ban

View File

@@ -19,9 +19,9 @@ const CF_SYNC = process.env.CF_SYNC || '/usr/local/bin/cloudflare-wh
const MANUAL_JAIL = process.env.MANUAL_JAIL || 'manual-bans'; const MANUAL_JAIL = process.env.MANUAL_JAIL || 'manual-bans';
const BAN_HIST_FILE = process.env.BAN_HIST_FILE || '/data/ban-history.json'; const BAN_HIST_FILE = process.env.BAN_HIST_FILE || '/data/ban-history.json';
const SUBNETS = (process.env.SUBNETS_TO_IGNORE || '10.0.0.0/8,172.16.0.0/12').split(',').map(s => s.trim()); const SUBNETS = (process.env.SUBNETS_TO_IGNORE || '10.0.0.0/8,172.16.0.0/12').split(',').map(s => s.trim());
const DEFAULT_DAYS = parseInt(process.env.DEFAULT_LOOKBACK_DAYS || '3'); const DEFAULT_DAYS = 3;
const ABUSE_KEY = process.env.ABUSEIPDB_API_KEY; const ABUSE_KEY = process.env.ABUSEIPDB_API_KEY;
const AUTOBAN_THR = parseInt(process.env.AUTOBAN_THRESHOLD || '75'); const AUTOBAN_THR = 75;
// Optional: POST to this URL on every manual ban (Discord, Slack, n8n, etc.) // Optional: POST to this URL on every manual ban (Discord, Slack, n8n, etc.)
const WEBHOOK_URL = process.env.WEBHOOK_URL || ''; const WEBHOOK_URL = process.env.WEBHOOK_URL || '';
@@ -73,11 +73,12 @@ async function addWhitelist(ip, note) {
} }
} }
fs.writeFileSync(JAIL_LOCAL, lines.join('\n')); fs.writeFileSync(JAIL_LOCAL, lines.join('\n'));
// unban from all jails // Live-update running fail2ban (no reload needed)
const jails = await getJails(); const jails = await getJails();
await Promise.all(jails.map(j => await Promise.all(jails.map(j => Promise.all([
run(`fail2ban-client set ${j} unbanip ${ip}`).catch(() => {}) run(`fail2ban-client set ${j} addignoreip ${ip}`).catch(() => {}),
)); run(`fail2ban-client set ${j} unbanip ${ip}`).catch(() => {}),
])));
if (fs.existsSync(CF_SYNC)) exec(`${CF_SYNC} &`); if (fs.existsSync(CF_SYNC)) exec(`${CF_SYNC} &`);
} }
@@ -87,6 +88,11 @@ async function removeWhitelist(ip) {
new RegExp(`\\s*${ip.replace(/\./g, '\\.')}(?:\\s*#[^\\n]*)?`, 'g'), '' new RegExp(`\\s*${ip.replace(/\./g, '\\.')}(?:\\s*#[^\\n]*)?`, 'g'), ''
); );
fs.writeFileSync(JAIL_LOCAL, updated); fs.writeFileSync(JAIL_LOCAL, updated);
// Live-update running fail2ban (no reload needed)
const jails = await getJails();
await Promise.all(jails.map(j =>
run(`fail2ban-client set ${j} delignoreip ${ip}`).catch(() => {})
));
if (fs.existsSync(CF_SYNC)) exec(`${CF_SYNC} &`); if (fs.existsSync(CF_SYNC)) exec(`${CF_SYNC} &`);
} }

View File

@@ -0,0 +1,78 @@
# ── F2B Control Center — Cloudflare stack ────────────────────────────────────
#
# Identical to docker-compose.yml with CF credentials added.
# Bans are enforced at BOTH the iptables level AND the Cloudflare WAF level.
#
# SETUP:
# 1. cp .env.example .env
# 2. Fill in CF_EMAIL and CF_APIKEY in .env
# 3. docker-compose -f docker-compose.cloudflare.yml up -d
#
# On first start the entrypoint detects CF_EMAIL/CF_APIKEY and installs
# jail.cloudflare.local instead of jail.local, enabling the cloudflare
# action for all jails automatically.
#
# IMPORTANT: If you have already started the standard compose and have an
# existing f2b-config volume, delete it first so the CF jail config is
# installed fresh:
# docker-compose down
# docker volume rm f2b-control-center_f2b-config
# docker-compose -f docker-compose.cloudflare.yml up -d
# ─────────────────────────────────────────────────────────────────────────────
version: "3.9"
services:
# ── Nginx Proxy Manager ─────────────────────────────────────────────────────
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ${DATA_DIR:-./data}/npm:/data
- ${DATA_DIR:-./data}/npm/logs:/data/logs
- ${DATA_DIR:-./data}/letsencrypt:/etc/letsencrypt
# ── F2B Control Center ──────────────────────────────────────────────────────
f2b-control-center:
build: .
image: f2b-control-center:latest
container_name: f2b-control-center
restart: unless-stopped
depends_on:
- npm
network_mode: host
environment:
PORT: "${DASHBOARD_PORT:-4000}"
ABUSEIPDB_API_KEY: "${ABUSEIPDB_API_KEY:-}"
SUBNETS_TO_IGNORE: "${SUBNETS_TO_IGNORE:-10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}"
WEBHOOK_URL: "${WEBHOOK_URL:-}"
TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN:-}"
TELEGRAM_CHAT_ID: "${TELEGRAM_CHAT_ID:-}"
# ── Cloudflare credentials ──────────────────────────────────────────────
# Required: your Cloudflare account email
CF_EMAIL: "${CF_EMAIL}"
# Required: your Cloudflare Global API Key
# https://dash.cloudflare.com/profile/api-tokens → "Global API Key"
CF_APIKEY: "${CF_APIKEY}"
# Internal paths
LOG_DIR: "/nginx-logs"
FAIL2BAN_LOG: "/var/log/fail2ban.log"
JAIL_LOCAL: "/etc/fail2ban/jail.local"
MANUAL_JAIL: "manual-bans"
BAN_HIST_FILE: "/data/ban-history.json"
volumes:
- ${DATA_DIR:-./data}/npm/logs:/nginx-logs:ro
- f2b-data:/data
- f2b-config:/etc/fail2ban
volumes:
f2b-data:
f2b-config:

View File

@@ -1,92 +1,69 @@
# ── F2B Control Center — docker-compose ────────────────────────────────────── # ── F2B Control Center — standard stack ──────────────────────────────────────
#
# Includes: Nginx Proxy Manager + Fail2Ban + dashboard
# #
# QUICK START: # QUICK START:
# 1. cp .env.example .env # cp .env.example .env
# 2. Set NPM_LOG_DIR to your Nginx Proxy Manager log path # # edit .env — at minimum review DATA_DIR and SUBNETS_TO_IGNORE
# 3. docker-compose up -d # docker-compose up -d
# #
# NETWORK MODE: # CLOUDFLARE:
# network_mode: host is the recommended default. # To also ban at the CF WAF level, use docker-compose.cloudflare.yml instead.
# This allows fail2ban's iptables rules to block traffic at the host level,
# which is required when Nginx Proxy Manager receives traffic from the host
# network stack (the typical Docker-based NPM setup).
#
# If you only want the dashboard (no active iptables blocking), you can
# switch to bridge networking by commenting out network_mode and
# uncommenting the ports section instead.
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
version: "3.9" version: "3.9"
services: services:
# ── Nginx Proxy Manager ─────────────────────────────────────────────────────
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81" # NPM admin UI (change or restrict in production)
volumes:
- ${DATA_DIR:-./data}/npm:/data
- ${DATA_DIR:-./data}/npm/logs:/data/logs # shared with f2b (see below)
- ${DATA_DIR:-./data}/letsencrypt:/etc/letsencrypt
# ── F2B Control Center ──────────────────────────────────────────────────────
f2b-control-center: f2b-control-center:
build: . build: .
image: f2b-control-center:latest image: f2b-control-center:latest
container_name: f2b-control-center container_name: f2b-control-center
restart: unless-stopped restart: unless-stopped
depends_on:
- npm
# Required for iptables rules to manipulate the host network stack. # Host network mode is required so fail2ban's iptables rules affect the
# network_mode: host makes the container share the host's network namespace, # host network stack — blocking traffic before it reaches NPM containers.
# so fail2ban bans affect traffic arriving at the host.
network_mode: host network_mode: host
# Alternative (bridge mode — dashboard only, no host-level blocking):
# Comment out network_mode above and uncomment these:
# ports:
# - "${DASHBOARD_PORT:-4000}:4000"
# cap_add:
# - NET_ADMIN
# - NET_RAW
environment: environment:
# ── Dashboard ──────────────────────────────────────────────────────── PORT: "${DASHBOARD_PORT:-4000}"
PORT: "${DASHBOARD_PORT:-4000}" ABUSEIPDB_API_KEY: "${ABUSEIPDB_API_KEY:-}"
SUBNETS_TO_IGNORE: "${SUBNETS_TO_IGNORE:-10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}"
# AbuseIPDB integration (optional but recommended) WEBHOOK_URL: "${WEBHOOK_URL:-}"
# Get a free API key at https://www.abuseipdb.com/ TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN:-}"
ABUSEIPDB_API_KEY: "${ABUSEIPDB_API_KEY:-}" TELEGRAM_CHAT_ID: "${TELEGRAM_CHAT_ID:-}"
# Internal paths — only change if you remap volumes
# Auto-ban: AbuseIPDB confidence score threshold (0-100) LOG_DIR: "/nginx-logs"
AUTOBAN_THRESHOLD: "${AUTOBAN_THRESHOLD:-75}" FAIL2BAN_LOG: "/var/log/fail2ban.log"
JAIL_LOCAL: "/etc/fail2ban/jail.local"
# Default lookback window for nginx log scanning (days) MANUAL_JAIL: "manual-bans"
DEFAULT_LOOKBACK_DAYS: "${DEFAULT_LOOKBACK_DAYS:-3}" BAN_HIST_FILE: "/data/ban-history.json"
# Comma-separated CIDR subnets to ignore in scans and bans
SUBNETS_TO_IGNORE: "${SUBNETS_TO_IGNORE:-10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}"
# Optional: POST ban events to this URL (e.g. Discord webhook, n8n, etc.)
WEBHOOK_URL: "${WEBHOOK_URL:-}"
# ── Internal paths — change only if you remap volumes ────────────────
LOG_DIR: "/nginx-logs"
FAIL2BAN_LOG: "/var/log/fail2ban.log"
JAIL_LOCAL: "/etc/fail2ban/jail.local"
MANUAL_JAIL: "manual-bans"
BAN_HIST_FILE: "/data/ban-history.json"
volumes: volumes:
# ── REQUIRED: your Nginx Proxy Manager access log directory ────────── # NPM logs — read-only. Shared with NPM via bind mount above.
# Change NPM_LOG_DIR in .env to match your setup. - ${DATA_DIR:-./data}/npm/logs:/nginx-logs:ro
# Default paths for common NPM Docker setups: # Persistent app state (ban history)
# /opt/npm/data/logs (appdata-style)
# /home/docker/NGINX/data/logs
# /docker/nginx-proxy-manager/data/logs
- "${NPM_LOG_DIR:-/opt/npm/data/logs}:/nginx-logs:ro"
# ── Persistent application data (ban history) ─────────────────────
- f2b-data:/data - f2b-data:/data
# Fail2ban config — persists across image updates
# ── Fail2ban configuration (survives container updates) ───────────
# Edit /etc/fail2ban/jail.local inside the container or mount a local
# directory here to manage config files outside the container.
- f2b-config:/etc/fail2ban - f2b-config:/etc/fail2ban
labels:
com.f2b-control-center.description: "Fail2Ban Control Center for Nginx Proxy Manager"
volumes: volumes:
f2b-data: f2b-data:
driver: local
f2b-config: f2b-config:
driver: local

View File

@@ -6,11 +6,33 @@ set -e
echo "[f2b-cc] Starting F2B Control Center..." echo "[f2b-cc] Starting F2B Control Center..."
# ── Kernel module: xt_string (required for X-Forwarded-For matching) ──────────
if modprobe xt_string 2>/dev/null; then
echo "[f2b-cc] xt_string kernel module loaded OK"
else
echo "[f2b-cc] WARNING: xt_string module unavailable — X-Forwarded-For iptables rules will NOT work"
echo "[f2b-cc] Run 'modprobe xt_string' on the Docker host to fix this."
fi
# ── DOCKER-USER chain (must exist for the ban action to insert rules) ─────────
if iptables -L DOCKER-USER -n > /dev/null 2>&1; then
echo "[f2b-cc] DOCKER-USER iptables chain found OK"
else
echo "[f2b-cc] DOCKER-USER chain missing — creating it"
iptables -N DOCKER-USER 2>/dev/null || true
fi
# ── First-run: install default fail2ban config if none exists ───────────────── # ── First-run: install default fail2ban config if none exists ─────────────────
if [ ! -f /etc/fail2ban/jail.local ]; then if [ ! -f /etc/fail2ban/jail.local ]; then
echo "[f2b-cc] First run — installing default fail2ban configuration..." echo "[f2b-cc] First run — installing default fail2ban configuration..."
cp -r /etc/f2b-defaults/. /etc/fail2ban/ cp -r /etc/f2b-defaults/. /etc/fail2ban/
# Cloudflare credentials present → use the CF-enabled jail config
if [ -n "${CF_EMAIL}" ] && [ -n "${CF_APIKEY}" ]; then
echo "[f2b-cc] CF_EMAIL + CF_APIKEY detected — enabling Cloudflare jail config"
cp /etc/f2b-defaults/jail.cloudflare.local /etc/fail2ban/jail.local
fi
# Apply SUBNETS_TO_IGNORE from environment into jail.local's ignoreip line # Apply SUBNETS_TO_IGNORE from environment into jail.local's ignoreip line
if [ -n "${SUBNETS_TO_IGNORE}" ]; then if [ -n "${SUBNETS_TO_IGNORE}" ]; then
IGNORE_LINE="ignoreip = 127.0.0.1/8 ::1 ${SUBNETS_TO_IGNORE}" IGNORE_LINE="ignoreip = 127.0.0.1/8 ::1 ${SUBNETS_TO_IGNORE}"
@@ -24,6 +46,16 @@ else
echo "[f2b-cc] Using existing fail2ban configuration." echo "[f2b-cc] Using existing fail2ban configuration."
fi fi
# ── Deploy telegram_notif.sh to persistent volume (user-editable) ─────────────
mkdir -p /data/action.d
if [ ! -f /data/action.d/telegram_notif.sh ]; then
echo "[f2b-cc] Deploying telegram_notif.sh to /data/action.d/"
cp /etc/f2b-defaults/action.d/telegram_notif.sh /data/action.d/telegram_notif.sh
chmod +x /data/action.d/telegram_notif.sh
else
echo "[f2b-cc] /data/action.d/telegram_notif.sh already present — skipping copy"
fi
# ── Ensure required directories and files exist ─────────────────────────────── # ── Ensure required directories and files exist ───────────────────────────────
mkdir -p /data /var/log /var/run/fail2ban mkdir -p /data /var/log /var/run/fail2ban
@@ -33,7 +65,7 @@ touch /var/log/fail2ban.log
# Ensure nginx-logs directory exists (warn if empty/unmounted) # Ensure nginx-logs directory exists (warn if empty/unmounted)
if [ ! -d /nginx-logs ] || [ -z "$(ls -A /nginx-logs 2>/dev/null)" ]; then if [ ! -d /nginx-logs ] || [ -z "$(ls -A /nginx-logs 2>/dev/null)" ]; then
echo "[f2b-cc] WARNING: /nginx-logs appears empty or unmounted." echo "[f2b-cc] WARNING: /nginx-logs appears empty or unmounted."
echo "[f2b-cc] Set NPM_LOG_DIR in .env and mount your NPM log directory." echo "[f2b-cc] Set DATA_DIR in .env so NPM logs are bind-mounted here."
echo "[f2b-cc] Log scanning will not return results until logs are available." echo "[f2b-cc] Log scanning will not return results until logs are available."
mkdir -p /nginx-logs mkdir -p /nginx-logs
fi fi

View File

@@ -0,0 +1,44 @@
[Definition]
# ── Cloudflare IP Access Rules action ────────────────────────────────────────
#
# Blocks/unblocks IPs at the Cloudflare account level via the Access Rules API.
# When enabled, a ban will be enforced by Cloudflare before traffic even
# reaches your server — the most effective layer for high-volume attackers.
#
# SETUP:
# 1. Get your Global API Key from:
# https://dash.cloudflare.com/profile/api-tokens
# 2. Set CF_EMAIL and CF_APIKEY in your .env file
# 3. Use docker-compose.cloudflare.yml instead of docker-compose.yml
#
# NOTE: This uses the user-level Access Rules API, which applies the block
# across all zones on your Cloudflare account. For zone-scoped rules,
# replace the URL with:
# https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/firewall/access_rules/rules
# ─────────────────────────────────────────────────────────────────────────────
actionban = curl -s -X POST \
-H "X-Auth-Email: %(cf_email)s" \
-H "X-Auth-Key: %(cf_apikey)s" \
-H "Content-Type: application/json" \
-d "{\"mode\":\"block\",\"configuration\":{\"target\":\"ip\",\"value\":\"<ip>\"},\"notes\":\"f2b-cc: <name>\"}" \
"https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules" \
> /dev/null 2>&1 || true
actionunban = RULE_ID=$(curl -s \
-H "X-Auth-Email: %(cf_email)s" \
-H "X-Auth-Key: %(cf_apikey)s" \
"https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules?configuration_target=ip&configuration_value=<ip>&mode=block&page=1&per_page=1" | \
python3 -c "import sys,json; r=json.load(sys.stdin).get('result',[]); print(r[0]['id'] if r else '')" 2>/dev/null) ; \
[ -n "$RULE_ID" ] && \
curl -s -X DELETE \
-H "X-Auth-Email: %(cf_email)s" \
-H "X-Auth-Key: %(cf_apikey)s" \
"https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules/$RULE_ID" \
> /dev/null 2>&1 || true
[Init]
# Populated from environment via jail.local — do not set here
cf_email =
cf_apikey =

View File

@@ -0,0 +1,21 @@
[Definition]
# ── Lifecycle notifications ───────────────────────────────────────────────────
actionstart = bash /data/action.d/telegram_notif.sh -a start
actionstop = bash /data/action.d/telegram_notif.sh -a stop
# ── Ban ───────────────────────────────────────────────────────────────────────
# 1. DOCKER-USER: drops forwarded packets containing the banned IP in the
# X-Forwarded-For header — catches traffic coming through Cloudflare/CDN
# where the real client IP is forwarded as a header to NPM.
# 2. INPUT: drops direct connections from the banned IP at the host level.
# 3. Telegram notification (silent if TELEGRAM_BOT_TOKEN is unset).
actionban = iptables -I DOCKER-USER -m string --algo bm --string 'X-Forwarded-For: <ip>' -j DROP
iptables -A INPUT -s <ip> -j DROP
bash /data/action.d/telegram_notif.sh -b <ip> -r "<name>"
# ── Unban ─────────────────────────────────────────────────────────────────────
# || true prevents failure if the rule was already removed (e.g. on restart).
actionunban = iptables -D DOCKER-USER -m string --algo bm --string 'X-Forwarded-For: <ip>' -j DROP || true
iptables -D INPUT -s <ip> -j DROP || true
bash /data/action.d/telegram_notif.sh -u <ip>

View File

@@ -0,0 +1,54 @@
#!/bin/bash
# ── Telegram notification hook for fail2ban ───────────────────────────────────
# Called by docker-npm.conf on ban/unban/start/stop events.
# Silently exits if TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID is not set.
#
# Usage:
# telegram_notif.sh -a start|stop
# telegram_notif.sh -b <ip> -r "<reason>"
# telegram_notif.sh -u <ip>
#
# Configure by setting in .env (passed into the container via docker-compose):
# TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
# TELEGRAM_CHAT_ID=-100123456789
# ─────────────────────────────────────────────────────────────────────────────
BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
CHAT_ID="${TELEGRAM_CHAT_ID:-}"
# Exit silently if not configured — allows docker-npm action to work without Telegram
[[ -z "$BOT_TOKEN" || -z "$CHAT_ID" ]] && exit 0
send() {
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${CHAT_ID}" \
--data-urlencode "text=$1" \
--data-urlencode "parse_mode=HTML" \
> /dev/null 2>&1 || true
}
# Parse flags
ACTION="" BAN_IP="" REASON="" UNBAN_IP=""
while getopts ":a:b:r:u:" opt; do
case $opt in
a) ACTION="$OPTARG" ;;
b) BAN_IP="$OPTARG" ;;
r) REASON="$OPTARG" ;;
u) UNBAN_IP="$OPTARG" ;;
esac
done
case "$ACTION" in
start) send "🟢 <b>F2B Control Center started</b>" ;;
stop) send "🔴 <b>F2B Control Center stopped</b>" ;;
esac
if [[ -n "$BAN_IP" ]]; then
MSG="🚫 <b>BANNED</b>: <code>${BAN_IP}</code>"
[[ -n "$REASON" ]] && MSG="${MSG} [${REASON}]"
send "$MSG"
fi
if [[ -n "$UNBAN_IP" ]]; then
send "✅ <b>UNBANNED</b>: <code>${UNBAN_IP}</code>"
fi

View File

@@ -1,28 +1,15 @@
# ── F2B Control Center — badbot filter ───────────────────────────────────────
#
# Blocks known malicious scanners, vulnerability testers, and exploit frameworks
# by their HTTP User-Agent string.
#
# LOG FORMAT:
# Primary pattern matches NPM logs with [Client IP] real-IP field:
# PROXY_IP - - [date] "GET /" 200 146 "-" "UserAgent" [Client REAL_IP]
#
# If your NPM logs have the client IP at line start (standard nginx combined),
# uncomment the alternative failregex lines instead.
# ─────────────────────────────────────────────────────────────────────────────
[Definition] [Definition]
# ── Primary: NPM [Client IP] format ────────────────────────────────────────── # ── NPM access log format ─────────────────────────────────────────────────────
failregex = ^[^ ]+ - -[^\[]*\[[^\]]+\] "[A-Z]+ [^"]*" \d{3} \d+ "[^"]*" "(?:masscan|zgrab|python-requests|Go-http-client/1\.1|[Nn]uclei|sqlmap|dirbuster|gobuster|nikto|nmap|wfuzz|Metasploit|libwww-perl|WPScan|ZmEu|jorgee|NetcraftSurveyAgent|Expanse|Shodan|censys|BinaryEdge|internet-measurement|SemrushBot|AhrefsBot)[^"]*" \[Client <HOST>\] # PROXY_IP - - [DD/Mon/YYYY:HH:MM:SS +0000] "METHOD PATH HTTP/VER" STATUS BYTES "REFERER" "UA" [Client REAL_IP]
#
# <HOST> is placed at the [Client REAL_IP] position — this is the IP that gets
# banned, which is the real client IP forwarded by Cloudflare/CDN via X-Forwarded-For.
#
# Test against your logs:
# fail2ban-regex /nginx-logs/proxy-host-1_access.log /etc/fail2ban/filter.d/badbot.conf
# ─────────────────────────────────────────────────────────────────────────────
# ── Alternative: standard nginx combined format (IP at line start) ──────────── failregex = ^\S+ - - \[[^\]]+\] "\S+ [^"]*" \d{3} \d+ "[^"]*" "(?i:masscan|zgrab|python-requests|go-http-client/1\.1|nuclei|sqlmap|dirbuster|gobuster|nikto|wfuzz|metasploit|libwww-perl|wpscan|nmap|zmeu|jorgee|shodan\.com|censys|binaryedge|internet-measurement|netcraft|strikeready|dataforseo|semrushbot|ahrefsbot|mj12bot|dotbot)[^"]*" \[Client <HOST>\]
# Uncomment and comment out the Primary line above to use this instead.
# failregex = ^<HOST> - -[^\[]*\[[^\]]+\] "[A-Z]+ [^"]*" \d{3} \d+ "[^"]*" "(?:masscan|zgrab|python-requests|Go-http-client/1\.1|[Nn]uclei|sqlmap|dirbuster|gobuster|nikto|nmap|wfuzz|Metasploit|libwww-perl|WPScan|ZmEu|jorgee|NetcraftSurveyAgent|Expanse|Shodan|censys|BinaryEdge|internet-measurement)[^"]*"
ignoreregex = ignoreregex =
# ── Notes ────────────────────────────────────────────────────────────────────
# Add more UA patterns to the failregex alternation as needed.
# Test your filter with:
# fail2ban-regex /nginx-logs/proxy-host-1_access.log /etc/fail2ban/filter.d/badbot.conf

View File

@@ -1,28 +1,14 @@
# ── F2B Control Center — http-errors filter ──────────────────────────────────
#
# Bans IPs generating a high volume of HTTP 4xx/5xx error responses.
# Works well for catching brute-force login attempts, path traversal scans,
# and generally misbehaving clients.
#
# Tune `maxretry` and `findtime` in jail.local to adjust sensitivity.
# Default: 15 errors in 5 minutes.
#
# LOG FORMAT:
# Primary pattern matches NPM logs with [Client IP] real-IP field.
# See badbot.conf for details on switching to standard nginx format.
# ─────────────────────────────────────────────────────────────────────────────
[Definition] [Definition]
# ── Primary: NPM [Client IP] format ────────────────────────────────────────── # ── NPM access log format ─────────────────────────────────────────────────────
# Matches any 4xx or 5xx response (excluding 200 implicitly by the status code). # Bans IPs generating excessive 4xx/5xx errors.
failregex = ^[^ ]+ - -[^\[]*\[[^\]]+\] "[A-Z]+ [^"]*" [45]\d\d \d+ "[^"]*" "[^"]*" \[Client <HOST>\] # Default jail: 15 errors in 5 minutes (tunable in jail.local).
#
# PROXY_IP - - [date] "METHOD PATH HTTP/VER" STATUS BYTES "REFERER" "UA" [Client REAL_IP]
# ─────────────────────────────────────────────────────────────────────────────
# ── Alternative: standard nginx combined format ─────────────────────────────── failregex = ^\S+ - - \[[^\]]+\] "\S+ [^"]*" [45]\d\d \d+ "[^"]*" "[^"]*" \[Client <HOST>\]
# failregex = ^<HOST> - -[^\[]*\[[^\]]+\] "[A-Z]+ [^"]*" [45]\d\d
# Ignore common false-positive 4xx codes: # Exclude very common benign 404s to reduce noise.
# 404 — very common for missing favicons, /robots.txt, etc. Remove from # Remove these if you want to count ALL error responses.
# ignoreregex below if you DO want to count 404s (recommended for ignoreregex = ^\S+ - - \[[^\]]+\] "\S+ /(?:favicon\.ico|robots\.txt|sitemap\.xml|apple-touch-icon[^"]*|\.well-known/[^"]*)[^"]*" 404 \d+ "[^"]*" "[^"]*" \[Client <HOST>\]
# aggressive probe detection when combined with high maxretry).
ignoreregex = ^[^ ]+ - -[^\[]*\[[^\]]+\] "[A-Z]+ /(?:favicon\.ico|robots\.txt|apple-touch-icon[^"]*) HTTP[^"]*" 404 .* \[Client <HOST>\]

View File

@@ -1,29 +1,12 @@
# ── F2B Control Center — npm-probe filter ────────────────────────────────────
#
# Bans IPs probing for well-known vulnerable paths.
# These requests almost always indicate automated exploit scanning — a single
# hit warrants a long ban (configured to 48h / maxretry 3 in jail.local).
#
# Covered categories:
# - PHP-based CMS admin paths (WordPress, Joomla, etc.)
# - Common config/credential file leaks (.env, .git, etc.)
# - Java frameworks (actuator, Spring Boot, Struts)
# - Web shells and common RCE payloads
# - Device/router admin interfaces (HNAP, boaform)
# - PHPMyAdmin, Adminer, database tools
# - Path traversal attempts (../)
#
# LOG FORMAT:
# Primary pattern matches NPM logs with [Client IP] real-IP field.
# See badbot.conf for details on switching to standard nginx format.
# ─────────────────────────────────────────────────────────────────────────────
[Definition] [Definition]
# ── Primary: NPM [Client IP] format ────────────────────────────────────────── # ── NPM access log format ─────────────────────────────────────────────────────
failregex = ^[^ ]+ - -[^\[]*\[[^\]]+\] "(?:GET|POST|HEAD|OPTIONS) (?:/.env(?:\.[^"]*)?|/.git(?:/[^"]*)?|/wp-login\.php|/wp-admin(?:/[^"]*)?|/xmlrpc\.php|/phpmyadmin(?:/[^"]*)?|/pma(?:/[^"]*)?|/adminer(?:\.php)?|/admin\.php|/config\.php|/setup\.php|/install\.php|/actuator(?:/[^"]*)?|/console|/manager/html|/invoker/JMXInvokerServlet|/solr(?:/[^"]*)?|/geoserver(?:/[^"]*)?|/boaform(?:/[^"]*)?|/HNAP1(?:/[^"]*)?|/cgi-bin/[^"]*|/shell\.php|/cmd\.php|/eval-stdin\.php|/[^"]*\.\./[^"]*) HTTP[^"]*" \d{3} .* \[Client <HOST>\] # Bans IPs probing for well-known vulnerable paths.
# Default jail: 3 hits in 30 minutes → 48h ban (very aggressive, intentionally).
#
# PROXY_IP - - [date] "METHOD PATH HTTP/VER" STATUS BYTES "REFERER" "UA" [Client REAL_IP]
# ─────────────────────────────────────────────────────────────────────────────
# ── Alternative: standard nginx combined format ─────────────────────────────── failregex = ^\S+ - - \[[^\]]+\] "[A-Z]+ /(?:\.env[^"]*|\.git[^"]*|wp-login\.php[^"]*|wp-admin[^"]*|xmlrpc\.php[^"]*|phpmyadmin[^"]*|pma/[^"]*|adminer[^"]*|admin\.php[^"]*|config\.php[^"]*|setup\.php[^"]*|install\.php[^"]*|actuator[^"]*|console[^"]*|manager/html[^"]*|invoker/[^"]*|solr/[^"]*|geoserver/[^"]*|boaform/[^"]*|HNAP1[^"]*|cgi-bin/[^"]*|shell\.php[^"]*|cmd\.php[^"]*|eval-stdin\.php[^"]*)[^"]*" \d{3} \d+ "[^"]*" "[^"]*" \[Client <HOST>\]
# failregex = ^<HOST> - -[^\[]*\[[^\]]+\] "(?:GET|POST|HEAD) (?:/.env|/.git|/wp-login\.php|/wp-admin|/xmlrpc\.php|/phpmyadmin|/pma|/adminer|/admin\.php|/actuator|/console|/boaform|/HNAP1|/cgi-bin/|/shell\.php) HTTP"
ignoreregex = ignoreregex =

View File

@@ -0,0 +1,78 @@
# ── F2B Control Center — jail configuration (Cloudflare) ─────────────────────
#
# Used when CF_EMAIL and CF_APIKEY are set (docker-compose.cloudflare.yml).
# Identical to jail.local but adds the cloudflare action to every jail so
# bans are enforced at both the iptables and Cloudflare WAF levels.
#
# CF credentials are read from environment variables — no credentials are
# stored in this file.
# ─────────────────────────────────────────────────────────────────────────────
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
# Populated by entrypoint from SUBNETS_TO_IGNORE env var on first run.
# Updated live by the dashboard — do not edit by hand.
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
# Cloudflare credentials — injected from environment at runtime.
# Set CF_EMAIL and CF_APIKEY in your .env file.
cf_email = %(ENV[CF_EMAIL])s
cf_apikey = %(ENV[CF_APIKEY])s
# ── NPM: Bad Bots ─────────────────────────────────────────────────────────────
[badbot]
enabled = true
filter = badbot
logpath = /nginx-logs/proxy-host-*_access.log
bantime = 24h
findtime = 10m
maxretry = 3
action = docker-npm
cloudflare[cf_email="%(cf_email)s", cf_apikey="%(cf_apikey)s"]
# ── NPM: HTTP Error Spamming ──────────────────────────────────────────────────
[http-errors]
enabled = true
filter = http-errors
logpath = /nginx-logs/proxy-host-*_access.log
bantime = 1h
findtime = 5m
maxretry = 15
action = docker-npm
cloudflare[cf_email="%(cf_email)s", cf_apikey="%(cf_apikey)s"]
# ── NPM: Exploit Probing ──────────────────────────────────────────────────────
[npm-probe]
enabled = true
filter = npm-probe
logpath = /nginx-logs/proxy-host-*_access.log
bantime = 48h
findtime = 30m
maxretry = 3
action = docker-npm
cloudflare[cf_email="%(cf_email)s", cf_apikey="%(cf_apikey)s"]
# ── Manual Bans ───────────────────────────────────────────────────────────────
[manual-bans]
enabled = true
filter = manual-bans
logpath = /dev/null
bantime = -1
findtime = 1d
maxretry = 1
action = docker-npm
cloudflare[cf_email="%(cf_email)s", cf_apikey="%(cf_apikey)s"]
# ── Recidive — repeat offenders ───────────────────────────────────────────────
[recidive]
enabled = false
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 7d
findtime = 1d
maxretry = 3
action = docker-npm
cloudflare[cf_email="%(cf_email)s", cf_apikey="%(cf_apikey)s"]

View File

@@ -1,46 +1,27 @@
# ── F2B Control Center — fail2ban jail configuration ───────────────────────── # ── F2B Control Center — jail configuration (standard) ───────────────────────
# #
# This file is written to /etc/fail2ban/jail.local on first container start. # Installed to /etc/fail2ban/jail.local on first container start.
# After first run it is persisted in the f2b-config Docker volume so your # Persisted in the f2b-config Docker volume — survives image updates.
# customisations survive image updates.
# #
# IMPORTANT: The dashboard manages the `ignoreip` line automatically. # WHITELIST: managed by the dashboard. The ignoreip line below is written by
# Do not edit it by hand unless you are not using the dashboard. # the entrypoint (from SUBNETS_TO_IGNORE) and updated live by the dashboard
# without restarting fail2ban.
# #
# LOG FORMAT NOTE: # CLOUDFLARE: to also block IPs at the CF level, use docker-compose.cloudflare.yml
# The default filters expect Nginx Proxy Manager access logs that include # instead of docker-compose.yml. It installs jail.cloudflare.local which
# the real client IP in a [Client X.X.X.X] field — the format produced # adds the cloudflare action to every jail automatically.
# when NPM forwards through Cloudflare or another proxy.
#
# If your NPM logs have the client IP at the start of each line (standard
# nginx combined format), edit the failregex lines in each filter to use:
# failregex = ^<HOST> - - \[...
# See fail2ban/filter.d/*.conf for both patterns (commented examples included).
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
[DEFAULT] [DEFAULT]
# Default ban duration (supports suffixes: s, m, h, d)
bantime = 1h bantime = 1h
# Time window in which maxretry violations trigger a ban
findtime = 10m findtime = 10m
# Number of violations before banning
maxretry = 5 maxretry = 5
# IPs and subnets to never ban. # Populated by entrypoint from SUBNETS_TO_IGNORE env var on first run.
# The dashboard appends whitelisted IPs here — do not remove this line. # Updated live by the dashboard — do not edit by hand.
# The entrypoint script expands SUBNETS_TO_IGNORE from your .env on first run.
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
# Default ban action: iptables-allports blocks all ports for banned IPs.
# Requires NET_ADMIN capability (granted in docker-compose.yml) and
# network_mode: host to affect host-level traffic.
banaction = iptables-allports
# ── NPM: Bad Bots ───────────────────────────────────────────────────────────── # ── NPM: Bad Bots ─────────────────────────────────────────────────────────────
# Blocks known malicious scanners and exploit frameworks by User-Agent.
# Stricter settings: low maxretry, long ban time.
[badbot] [badbot]
enabled = true enabled = true
filter = badbot filter = badbot
@@ -48,11 +29,9 @@ logpath = /nginx-logs/proxy-host-*_access.log
bantime = 24h bantime = 24h
findtime = 10m findtime = 10m
maxretry = 3 maxretry = 3
action = %(action_)s action = docker-npm
# ── NPM: HTTP Error Spamming ────────────────────────────────────────────────── # ── NPM: HTTP Error Spamming ──────────────────────────────────────────────────
# Bans IPs that generate a high volume of 4xx/5xx errors.
# Useful for catching brute-force attempts, broken crawlers, and probes.
[http-errors] [http-errors]
enabled = true enabled = true
filter = http-errors filter = http-errors
@@ -60,11 +39,9 @@ logpath = /nginx-logs/proxy-host-*_access.log
bantime = 1h bantime = 1h
findtime = 5m findtime = 5m
maxretry = 15 maxretry = 15
action = %(action_)s action = docker-npm
# ── NPM: Exploit Probing ────────────────────────────────────────────────────── # ── NPM: Exploit Probing ──────────────────────────────────────────────────────
# Bans IPs requesting well-known vulnerable paths (.env, wp-admin, etc.).
# These are almost always malicious — short maxretry, long ban.
[npm-probe] [npm-probe]
enabled = true enabled = true
filter = npm-probe filter = npm-probe
@@ -72,11 +49,10 @@ logpath = /nginx-logs/proxy-host-*_access.log
bantime = 48h bantime = 48h
findtime = 30m findtime = 30m
maxretry = 3 maxretry = 3
action = %(action_)s action = docker-npm
# ── Manual Bans ─────────────────────────────────────────────────────────────── # ── Manual Bans ───────────────────────────────────────────────────────────────
# Permanent bans added via the dashboard or `fail2ban-client set manual-bans banip`. # Populated via dashboard or: fail2ban-client set manual-bans banip <IP>
# No automatic log-based detection — only manual entries via the dashboard.
[manual-bans] [manual-bans]
enabled = true enabled = true
filter = manual-bans filter = manual-bans
@@ -84,11 +60,11 @@ logpath = /dev/null
bantime = -1 bantime = -1
findtime = 1d findtime = 1d
maxretry = 1 maxretry = 1
action = %(action_)s action = docker-npm
# ── Recidive ────────────────────────────────────────────────────────────────── # ── Recidive — repeat offenders ───────────────────────────────────────────────
# Escalated bans for repeat offenders across any jail. # Escalates bans to 7d for IPs that get banned 3+ times within a day.
# Disabled by default — enable if you want long-term blocks for persistent attackers. # Enable once your other jails have been running for a while.
[recidive] [recidive]
enabled = false enabled = false
filter = recidive filter = recidive
@@ -96,4 +72,4 @@ logpath = /var/log/fail2ban.log
bantime = 7d bantime = 7d
findtime = 1d findtime = 1d
maxretry = 3 maxretry = 3
action = %(action_)s action = docker-npm