workbuddy.xpcool.com/.workbuddy/tmp/server-monitor.sh

140 lines
6.6 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# ============================================================
# 服务器安全与资源监控脚本(轻量版)
# 功能:
# 1. SSH 爆破检测10分钟内失败认证超阈值同一波攻击只提醒一次攻击停止后再犯才重新提醒
# 2. 新来源 IP SSH 登录(仅"没见过的新 IP"首次登录时提醒,避免自己日常登录刷屏)
# 3. fail2ban 新封禁 IP同一 IP 只提醒一次,永久去重)
# 4. 根分区磁盘、内存、系统负载告警
# 5. 未登记的公网监听端口变化告警(同一端口只提醒一次)
# 告警方式:写日志 /var/log/server-monitor.log + 可选 Bark 推送
# Bark 配置:/etc/server-monitor.conf 中设置 BARK_URL 后生效
# 部署:/usr/local/bin/server-monitor.sh配合 cron 每 10 分钟执行
# 去重状态目录:/var/lib/server-monitor/
# - banned-ips.state :已提醒过的封禁 IP只增不减 → 同一 IP 永不再重复提醒)
# - ssh-src-ips.state已提醒过的登录来源 IP仅新 IP 首次登录提醒)
# - ports.state :当前已提醒的未登记端口(端口消失后清除,再出现会再提醒)
# - ssh-brute.state 爆破告警状态attacking=本轮已提醒;攻击停止后复位为 idle
# 之后再出现攻击视为"新一波",重新提醒一次)
# 说明:完整审计数据仍在 /var/log/fail2ban.log 与 journald且已由
# server-security-collector.sh 上报到 admin 后台,去重不影响追溯。
# ============================================================
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 读取可选配置BARK_URL
CONF=/etc/server-monitor.conf
[ -f "$CONF" ] && . "$CONF"
LOG=/var/log/server-monitor.log
NOW=$(date '+%F %T')
ALERT=""
BARK_URL=${BARK_URL:-}
alert() { ALERT="${ALERT}$1
"; }
# 去重状态目录(第 1、2、3、7 段共用)
STATE_DIR=/var/lib/server-monitor
mkdir -p "$STATE_DIR" 2>/dev/null
BAN_STATE=$STATE_DIR/banned-ips.state
SSH_STATE=$STATE_DIR/ssh-src-ips.state
PORT_STATE=$STATE_DIR/ports.state
BRUTE_STATE=$STATE_DIR/ssh-brute.state
touch "$BAN_STATE" "$SSH_STATE" "$PORT_STATE" 2>/dev/null
# ---------- 1) SSH 爆破检测(同一波攻击只提醒一次;攻击停止后再犯才重新提醒) ----------
# 逻辑:进入攻击态时提醒一次并置 attacking攻击停止10分钟内失败数回落时静默复位为
# idle。之后再被攻击即视为"新一波",重新提醒。避免持续攻击每 10 分钟刷屏。
FAILED=$(sudo journalctl _SYSTEMD_UNIT=sshd.service --since "10 minutes ago" 2>/dev/null | grep -ci "Failed password")
if [ "${FAILED:-0}" -ge 10 ]; then
if [ "$(cat "$BRUTE_STATE" 2>/dev/null)" != "attacking" ]; then
# 取失败次数最多的前 3 个来源 IP便于判断攻击来源
BRUTE_TOP=$(sudo journalctl _SYSTEMD_UNIT=sshd.service --since "10 minutes ago" 2>/dev/null \
| grep -i "Failed password" | awk '{for(i=1;i<=NF;i++) if($i=="from") print $(i+1)}' \
| sort | uniq -c | sort -rn | head -3 | awk '{printf "%s(%s次) ", $2, $1}')
alert "SSH爆破风险: 10分钟内 ${FAILED} 次失败认证攻击进行中fail2ban 已自动拦截,无需处理)
主要来源: ${BRUTE_TOP:-未知}"
echo "attacking" > "$BRUTE_STATE"
fi
else
# 攻击已停止 → 静默复位(只写日志,不推送),下次再犯才重新提醒
if [ "$(cat "$BRUTE_STATE" 2>/dev/null)" = "attacking" ]; then
echo "idle" > "$BRUTE_STATE"
echo "$NOW [INFO] SSH 爆破已停止10分钟内失败 ${FAILED:-0} 次),爆破告警已复位,下次再犯将重新提醒" >> "$LOG"
fi
fi
# ---------- 2) 新来源 IP 登录(仅首次出现的 IP 才提醒) ----------
ACCEPTED=$(sudo journalctl _SYSTEMD_UNIT=sshd.service --since "10 minutes ago" 2>/dev/null | grep -iE "Accepted (password|publickey)")
if [ -n "$ACCEPTED" ]; then
NEWIPS=""
for ip in $(printf '%s\n' "$ACCEPTED" | awk '{for(i=1;i<=NF;i++) if($i=="from") print $(i+1)}' | sort -u); do
grep -qxF "$ip" "$SSH_STATE" 2>/dev/null || NEWIPS="${NEWIPS} ${ip}"
done
if [ -n "$NEWIPS" ]; then
alert "新来源IP登录:${NEWIPS}
$(printf '%s\n' "$ACCEPTED" | tail -3)"
for ip in $NEWIPS; do echo "$ip" >> "$SSH_STATE"; done
fi
fi
# ---------- 3) fail2ban 新封禁 IP同一 IP 只提醒一次) ----------
CUR_BANNED=$(sudo fail2ban-client status sshd 2>/dev/null | sed -n 's/.*Banned IP list:[[:space:]]*//p')
NEW_BANNED=""
for ip in $CUR_BANNED; do
grep -qxF "$ip" "$BAN_STATE" 2>/dev/null || NEW_BANNED="${NEW_BANNED} ${ip}"
done
if [ -n "$NEW_BANNED" ]; then
alert "fail2ban 新封禁IP:${NEW_BANNED}(已自动封禁,无需处理)"
for ip in $NEW_BANNED; do echo "$ip" >> "$BAN_STATE"; done
fi
# ---------- 4) 根分区磁盘 ----------
DF=$(df -P / 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%')
[ "${DF:-0}" -ge 85 ] && alert "根分区使用率已达 ${DF}%"
# ---------- 5) 内存可用比 ----------
MEMFREE=$(free -m 2>/dev/null | awk '/Mem:/{print int($7/$2*100)}')
[ "${MEMFREE:-100}" -le 10 ] && alert "内存可用仅剩 ${MEMFREE}%"
# ---------- 6) 系统负载 ----------
LOAD=$(awk '{print int($1)}' /proc/loadavg 2>/dev/null)
[ "${LOAD:-0}" -ge 4 ] && alert "系统负载过高: $LOAD"
# ---------- 7) 公网监听端口漂移检测 ----------
# 基线:当前业务期望对外监听的端口
# SSH(22025) / Nginx(80,443) / Gitea(3000,2222) / Bark(8081, docker映射,防火墙已挡公网)
BASE_PORTS="22025 80 443 3000 2222 8081"
NOW_PORTS=$(ss -tlnH 2>/dev/null | awk '{print $4}' | grep -vE '^127\.' | grep -oE ':[0-9]+$' | tr -d ':' | sort -u)
EXTRA=""
for p in $NOW_PORTS; do
case " $BASE_PORTS " in
*" $p "*) ;;
*) EXTRA="${EXTRA} ${p}" ;;
esac
done
# 同一端口只提醒一次(端口消失后再出现视为新事件)
NEW_EXTRA=""
for p in $EXTRA; do
grep -qxF "$p" "$PORT_STATE" 2>/dev/null || NEW_EXTRA="${NEW_EXTRA} ${p}"
done
if [ -n "$EXTRA" ]; then
printf '%s\n' $EXTRA > "$PORT_STATE"
else
: > "$PORT_STATE"
fi
[ -n "$NEW_EXTRA" ] && alert "发现未登记的公网监听端口:${NEW_EXTRA}(请核查是否为新增服务)"
# ---------- 输出:日志 + Bark 推送 ----------
if [ -n "$ALERT" ]; then
echo -e "$NOW [ALERT] $HOSTNAME
$ALERT" >> "$LOG"
if [ -n "$BARK_URL" ]; then
# Bark 自建服务端(路径式 keyPOST JSON 到 /<device_key>
BODY=$(printf '%s' "$ALERT" | tr '\n' ' ' | tr '"' "'")
curl -s -m 8 -X POST "$BARK_URL" -H "Content-Type: application/json" -d "{\"title\":\"服务器告警\",\"body\":\"$BODY\"}" >/dev/null 2>&1
fi
else
echo "$NOW [OK] 登录正常 负载${LOAD:-?} 根分区${DF:-?}% 内存可用${MEMFREE:-?}%" >> "$LOG"
fi