Add scripts/ directory with install.sh + all client scripts

This commit is contained in:
sami
2026-06-30 14:06:03 -07:00
parent 5f620a8d8e
commit 657bf3d798
5 changed files with 350 additions and 0 deletions
+11
View File
@@ -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 =
+98
View File
@@ -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."
+110
View File
@@ -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
+17
View File
@@ -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"
+114
View File
@@ -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