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:
67
.env.example
67
.env.example
@@ -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 (0–100) 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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} &`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
78
docker-compose.cloudflare.yml
Normal file
78
docker-compose.cloudflare.yml
Normal 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:
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
44
fail2ban/action.d/cloudflare.conf
Normal file
44
fail2ban/action.d/cloudflare.conf
Normal 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 =
|
||||||
21
fail2ban/action.d/docker-npm.conf
Normal file
21
fail2ban/action.d/docker-npm.conf
Normal 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>
|
||||||
54
fail2ban/action.d/telegram_notif.sh
Normal file
54
fail2ban/action.d/telegram_notif.sh
Normal 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
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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>\]
|
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
78
fail2ban/jail.cloudflare.local
Normal file
78
fail2ban/jail.cloudflare.local
Normal 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"]
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user