Add scripts/ directory with install.sh + all client scripts
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
[Definition]
|
||||
|
||||
# Path to staging directory
|
||||
stagingdir = /var/lib/shared-bans-staging
|
||||
|
||||
# Path to the actual ban-staging script (installed separately)
|
||||
banscript = /usr/local/bin/shared-bans-stage-ban.sh
|
||||
|
||||
actionban = <banscript> "<ip>" "<name>" "<matches>" "<stagingdir>"
|
||||
|
||||
actionunban =
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
# Install shared-bans client on a server.
|
||||
# Run as root on the target server.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="${1:-}"
|
||||
if [ -z "$REPO_URL" ]; then
|
||||
echo "Usage: $0 <repo-url-with-token>"
|
||||
echo "Example: $0 'http://shared-bans-reader:TOKEN@100.81.59.99:3030/sami7777/shared-bans.git'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install scripts
|
||||
install -m 0755 scripts/shared-bans-apply.sh /usr/local/bin/shared-bans-apply.sh
|
||||
install -m 0755 scripts/shared-bans-update-static.sh /usr/local/bin/shared-bans-update-static.sh
|
||||
install -m 0755 scripts/shared-bans-stage-ban.sh /usr/local/bin/shared-bans-stage-ban.sh
|
||||
install -m 0644 scripts/fail2ban-action-shared-bans.conf /etc/fail2ban/action.d/shared-bans.conf
|
||||
|
||||
# Config
|
||||
cat > /etc/shared-bans.conf <<EOF
|
||||
SHARED_BANS_REPO_URL="$REPO_URL"
|
||||
EOF
|
||||
chmod 0600 /etc/shared-bans.conf
|
||||
|
||||
# Systemd units
|
||||
cat > /etc/systemd/system/shared-bans-apply.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Apply shared IP ban list from git repo to local ipset
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/shared-bans-apply.sh
|
||||
EOF
|
||||
|
||||
cat > /etc/systemd/system/shared-bans-apply.timer <<'EOF'
|
||||
[Unit]
|
||||
Description=Apply shared IP ban list from git repo to local ipset
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=5min
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
cat > /etc/systemd/system/shared-bans-update-static.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Update shared IP ban list static sources from URLs
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/shared-bans-update-static.sh
|
||||
EOF
|
||||
|
||||
cat > /etc/systemd/system/shared-bans-update-static.timer <<'EOF'
|
||||
[Unit]
|
||||
Description=Nightly update of shared IP ban list static sources
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:30:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now shared-bans-apply.timer shared-bans-update-static.timer
|
||||
|
||||
# fail2ban jail update
|
||||
mkdir -p /var/lib/shared-bans-staging
|
||||
|
||||
if command -v nft >/dev/null 2>&1 && systemctl is-active nftables >/dev/null 2>&1; then
|
||||
FW_ACTION='nftables-multiport[name=sshd, port="22", protocol=tcp]'
|
||||
else
|
||||
FW_ACTION='iptables-multiport[name=sshd, port="22", protocol=tcp]'
|
||||
fi
|
||||
|
||||
cat > /etc/fail2ban/jail.local <<EOF
|
||||
[DEFAULT]
|
||||
bantime = 86400
|
||||
findtime = 600
|
||||
maxretry = 3
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = 22
|
||||
filter = sshd
|
||||
logpath = /var/log/auth.log
|
||||
maxretry = 3
|
||||
action = $FW_ACTION
|
||||
shared-bans[name=sshd]
|
||||
EOF
|
||||
|
||||
systemctl restart fail2ban
|
||||
|
||||
echo "Installed. Run /usr/local/bin/shared-bans-apply.sh to verify."
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
# Shared ban list applier — pulls git repo, applies to ipset on this server.
|
||||
# Runs every 5 min via systemd timer.
|
||||
set -uo pipefail
|
||||
|
||||
REPO_DIR="/var/lib/shared-bans"
|
||||
LOG="/var/log/shared-bans-apply.log"
|
||||
IPSET_NAME="shared_bans"
|
||||
|
||||
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG"; }
|
||||
|
||||
CONF="/etc/shared-bans.conf"
|
||||
if [ -f "$CONF" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$CONF"
|
||||
fi
|
||||
|
||||
REPO_URL="${SHARED_BANS_REPO_URL:-}"
|
||||
if [ -z "$REPO_URL" ]; then
|
||||
log "ERROR: SHARED_BANS_REPO_URL not set in $CONF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$REPO_DIR")"
|
||||
log "=== apply tick ==="
|
||||
|
||||
# Clone or pull
|
||||
if [ ! -d "$REPO_DIR/.git" ]; then
|
||||
log "Cloning $REPO_URL"
|
||||
if ! git clone --depth 1 "$REPO_URL" "$REPO_DIR" 2>>"$LOG"; then
|
||||
log "ERROR: clone failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
cd "$REPO_DIR" || exit 1
|
||||
if ! git pull --ff-only 2>>"$LOG"; then
|
||||
log "WARN: pull failed (using cached repo)"
|
||||
fi
|
||||
fi
|
||||
|
||||
cd "$REPO_DIR" || exit 1
|
||||
|
||||
# Build merged list (static + dynamic, deduped, sorted)
|
||||
MERGED="$REPO_DIR/merged/bans.txt"
|
||||
mkdir -p "$(dirname "$MERGED")"
|
||||
{
|
||||
cat "$REPO_DIR/static/bans.txt" 2>/dev/null
|
||||
echo
|
||||
cat "$REPO_DIR/dynamic/bans.txt" 2>/dev/null
|
||||
} | grep -vE '^\s*(#|$)' | grep -E '^[0-9]' | awk '!seen[$0]++' | sort -V > "$MERGED"
|
||||
|
||||
COUNT=$(wc -l < "$MERGED")
|
||||
log "Merged list: $COUNT unique entries"
|
||||
|
||||
if [ "$COUNT" -lt 50 ]; then
|
||||
log "ERROR: suspiciously few entries ($COUNT), refusing to apply"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create main set if missing
|
||||
if ! /sbin/ipset list "$IPSET_NAME" >/dev/null 2>&1; then
|
||||
/sbin/ipset create "$IPSET_NAME" hash:net family inet hashsize 16384 maxelem 500000 2>>"$LOG"
|
||||
log "Created ipset $IPSET_NAME"
|
||||
fi
|
||||
|
||||
# Build new set with unique name (PID-based)
|
||||
TMP_SET="${IPSET_NAME}_new_$$"
|
||||
/sbin/ipset destroy "$TMP_SET" 2>/dev/null
|
||||
/sbin/ipset create "$TMP_SET" hash:net family inet hashsize 16384 maxelem 500000 2>>"$LOG"
|
||||
|
||||
# Build ipset restore-format file pointing at TMP_SET (NOT the live one)
|
||||
TMP_RESTORE="$(mktemp)"
|
||||
sed "s|^|add ${TMP_SET} |" "$MERGED" > "$TMP_RESTORE"
|
||||
|
||||
# Restore into temp set
|
||||
if ! /sbin/ipset restore -file "$TMP_RESTORE" 2>>"$LOG"; then
|
||||
log "ERROR: ipset restore failed (stderr above)"
|
||||
/sbin/ipset destroy "$TMP_SET" 2>/dev/null
|
||||
rm -f "$TMP_RESTORE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PRE_SWAP=$(/sbin/ipset list "$TMP_SET" 2>/dev/null | grep -cE '^[0-9]')
|
||||
log "Restored $PRE_SWAP entries into $TMP_SET"
|
||||
|
||||
# Atomic swap so packets are never matched against an empty set
|
||||
/sbin/ipset swap "$TMP_SET" "$IPSET_NAME" 2>>"$LOG" || {
|
||||
log "ERROR: ipset swap failed"
|
||||
/sbin/ipset destroy "$TMP_SET" 2>/dev/null
|
||||
rm -f "$TMP_RESTORE"
|
||||
exit 1
|
||||
}
|
||||
/sbin/ipset destroy "$TMP_SET" 2>/dev/null
|
||||
rm -f "$TMP_RESTORE"
|
||||
|
||||
# Ensure iptables rule
|
||||
if ! /sbin/iptables -C INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null; then
|
||||
/sbin/iptables -I INPUT 1 -m set --match-set "$IPSET_NAME" src -j DROP 2>>"$LOG" && \
|
||||
log "Inserted iptables rule for $IPSET_NAME" || \
|
||||
log "WARN: failed to insert iptables rule"
|
||||
fi
|
||||
|
||||
# Log rotation
|
||||
if [ "$(wc -l < "$LOG")" -gt 5000 ]; then
|
||||
tail -n 3000 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
|
||||
fi
|
||||
|
||||
APPLIED=$(/sbin/ipset list "$IPSET_NAME" 2>/dev/null | grep -cE '^[0-9]')
|
||||
log "Applied: $APPLIED entries in $IPSET_NAME"
|
||||
exit 0
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Stage a fail2ban ban for later promotion to the shared-bans repo.
|
||||
# Args: <ip> <jail-name> <match-string> <staging-dir>
|
||||
set -uo pipefail
|
||||
|
||||
IP="$1"
|
||||
JAIL="$2"
|
||||
MATCH="$3"
|
||||
STAGING="$4"
|
||||
|
||||
mkdir -p "$STAGING"
|
||||
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
{
|
||||
echo "# fail2ban ban at $ts jail=$JAIL match=$MATCH"
|
||||
echo "$IP"
|
||||
} >> "$STAGING/bans.txt"
|
||||
chmod 0644 "$STAGING/bans.txt"
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
# Shared ban list updater — pulls URLs from sources.json and refreshes static/bans.txt.
|
||||
# Runs nightly via systemd timer on the master (DNS3).
|
||||
set -uo pipefail
|
||||
|
||||
REPO_DIR="/var/lib/shared-bans"
|
||||
SOURCES_JSON="$REPO_DIR/sources.json"
|
||||
STATIC_OUT="$REPO_DIR/static/bans.txt"
|
||||
LOG="/var/log/shared-bans-update.log"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG"; }
|
||||
|
||||
if [ ! -d "$REPO_DIR/.git" ]; then
|
||||
log "ERROR: $REPO_DIR not a git repo, run shared-bans-apply.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$REPO_DIR" || exit 1
|
||||
|
||||
# Refresh git index (in case dynamic bans were pushed from elsewhere)
|
||||
git pull --ff-only >>"$LOG" 2>&1 || log "WARN: git pull failed, using local"
|
||||
|
||||
# Parse sources.json with python (jq may not be installed everywhere)
|
||||
python3 << PYEOF > "$WORK/sources.txt"
|
||||
import json, sys
|
||||
try:
|
||||
with open("$SOURCES_JSON") as f:
|
||||
data = json.load(f)
|
||||
for src in data.get("sources", []):
|
||||
print(f"# source: {src['name']} ({src.get('comment','')})")
|
||||
print(f"# url: {src['url']}")
|
||||
print(f"URL\t{src['url']}")
|
||||
except Exception as e:
|
||||
print(f"# ERROR parsing sources.json: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
|
||||
if [ ! -s "$WORK/sources.txt" ]; then
|
||||
log "ERROR: sources.txt empty (python parse failed?)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fetch each URL, dedupe
|
||||
> "$WORK/all.txt"
|
||||
|
||||
while IFS=$'\t' read -r tag url; do
|
||||
[ "$tag" != "URL" ] && continue
|
||||
[ -z "$url" ] && continue
|
||||
echo "# fetching $url ..." >> "$WORK/all.txt"
|
||||
if curl -fsS --max-time 60 "$url" 2>>"$LOG" >> "$WORK/all.txt"; then
|
||||
log " fetched $url"
|
||||
else
|
||||
log " WARN: failed to fetch $url"
|
||||
fi
|
||||
done < "$WORK/sources.txt"
|
||||
|
||||
# Clean: strip comments/blank/whitespace, keep only IP/CIDR lines
|
||||
grep -E '^[0-9]' "$WORK/all.txt" | tr -d ' \t' | sort -u > "$WORK/clean.txt"
|
||||
|
||||
COUNT=$(wc -l < "$WORK/clean.txt")
|
||||
log "After dedupe: $COUNT unique CIDRs/IPs"
|
||||
|
||||
if [ "$COUNT" -lt 50 ]; then
|
||||
log "ERROR: suspiciously few entries ($COUNT), refusing to commit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Write new static file
|
||||
{
|
||||
echo "# Auto-generated from sources.json — do not edit by hand."
|
||||
echo "# To add new block sources, edit sources.json instead."
|
||||
echo "# Last regenerated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "# Total unique entries: $COUNT"
|
||||
echo
|
||||
cat "$WORK/clean.txt"
|
||||
} > "$STATIC_OUT.new"
|
||||
|
||||
# Diff against current
|
||||
if diff -q "$STATIC_OUT" "$STATIC_OUT.new" >/dev/null 2>&1; then
|
||||
log "No changes since last update"
|
||||
rm -f "$STATIC_OUT.new"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mv "$STATIC_OUT.new" "$STATIC_OUT"
|
||||
log "Wrote $STATIC_OUT"
|
||||
|
||||
# Commit + push
|
||||
git add "$STATIC_OUT"
|
||||
if git diff --cached --quiet; then
|
||||
log "Nothing to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "Update static ban list ($(date -u +%Y-%m-%d)) [auto]" >>"$LOG" 2>&1 || {
|
||||
log "ERROR: git commit failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if git push origin main >>"$LOG" 2>&1; then
|
||||
log "Pushed to origin"
|
||||
else
|
||||
log "ERROR: git push failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Log rotation
|
||||
if [ "$(wc -l < "$LOG")" -gt 5000 ]; then
|
||||
tail -n 3000 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user