Nginx 5xx错误与SSH暴力破解实时监控实战:2024年Linux日志告警精选方案
在Linux运维实践中,两类风险尤为棘手:一是Nginx持续产生5xx错误而团队未能及时感知;二是服务器SSH端口遭受不明IP的持续暴力破解尝试。
面对这类问题,部署一套完整的监控体系(如ELK或Prometheus)是标准思路。然而,对于资源有限的中小规模业务,这类方案的部署与维护成本可能过高。
事实上,利用Linux系统内置的工具链——tail、awk和curl——我们完全可以构建一套稳定、实时且成本可控的日志告警方案。
但需注意,网络上许多现成的“一键脚本”存在设计缺陷,直接用于生产环境可能导致问题。典型缺陷包括:
- 管道与子Shell导致的计数失效:在管道中进行变量计数,会因子Shell作用域隔离而无法正确累加。
- 伪“时间窗口”:许多脚本采用每分钟整点清零的计数方式,而非真正的滑动时间窗口,易造成误报或漏报。
- grep缓冲延迟:使用
grep时若未关闭输出缓冲,日志事件无法被实时处理,导致告警延迟。 - 日志轮转后监控中断:简单的
tail -f命令在日志文件被轮转(rotate)后可能丢失跟踪。
一、核心设计原则:规避常见陷阱
构建可靠的轻量级告警方案,需遵循以下核心设计原则以规避上述问题。
1. 单进程状态管理
所有核心统计逻辑(如计数、时间戳记录)应在单个awk进程内完成。此举旨在避免在Shell管道中使用多个命令时,因变量作用域隔离而导致状态不一致。简言之,将数据与处理逻辑置于同一上下文中。
2. 实现真正的滑动时间窗口
告警逻辑应基于“过去1分钟内,若事件发生超过N次则触发”。关键在于“过去1分钟”是一个动态滑动的窗口,而非固定分钟间隔的计数器清零。实现上,可借助awk的systime()函数获取时间戳,并持续清理超过60秒的旧事件记录,以此模拟滑动窗口。
3. 避免依赖脆弱的日志格式
脚本应保持健壮性。针对Nginx访问日志,默认采用标准日志格式,仅需按位置获取$1(客户端IP)和$9(HTTP状态码)即可,避免使用复杂且易因日志格式微调而失效的正则表达式。
4. 最小化外部依赖
方案核心仅依赖tail、awk和curl,这些工具在主流Linux发行版中普遍存在,确保了方案的可移植性。
5. 安全优先策略
脚本中避免使用eval等高危操作;支持IP白名单机制,过滤可信来源的告警;在启动时对配置的Webhook地址进行基础校验;对关键目录设置严格的权限控制。
二、可投入生产的部署脚本
将以下脚本保存为 /usr/local/bin/log-alert.sh,并赋予执行权限。
#!/bin/bash
# ================= 配置项 =================
NGINX_LOG="/var/log/nginx/access.log"
SSH_MODE="file" # file 或 journald
SSH_LOG="/var/log/secure"
WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的key"
NGINX_THRESHOLD=10
SSH_THRESHOLD=5
SILENT=300
STATE_DIR="/dev/shm/log_alert"
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
WHITELIST=("127.0.0.1" "10.0.0.0/8" "192.168.0.0/16")
# ================= 工具检查 =================
for cmd in tail awk curl; do
command -v $cmd >/dev/null || { echo "缺少依赖: $cmd"; exit 1; }
done
# ================= webhook 校验 =================
resp=$(curl -s --max-time 5 "$WEBHOOK")
echo "$resp" | grep -q '"errcode":0' || {
echo "Webhook 无效或不可用"
exit 1
}
# ================= 告警函数 =================
send_alert() {
local title="$1"
local content="$2"
curl -s -o /dev/null -X POST "$WEBHOOK" \
-H "Content-Type: application/json" \
-d "{
\"msgtype\": \"text\",
\"text\": {\"content\": \"${title}\n${content}\"}
}" &
}
# ================= 白名单判断 =================
is_white() {
local ip="$1"
[[ "$ip" == "127.0.0.1" ]] && return 0
for w in "${WHITELIST[@]}"; do
[[ "$ip" == "$w" ]] && return 0
done
return 1
}
# ================= NGINX 监控 =================
tail -F "$NGINX_LOG" | awk -v t="$NGINX_THRESHOLD" -v s="$SILENT" '
{
ip=$1
code=$9
now=systime()
if (code !~ /^5[0-9][0-9]$/) next
key=ip
events[key][++cnt[key]]=now
# 清理旧数据
for (i in events[key]) {
if (now - events[key][i] > 60) delete events[key][i]
}
n=0
for (i in events[key]) n++
if (n >= t && now - last[key] > s) {
last[key]=now
print "NGINX|"ip"|"n
fflush()
}
}' | while IFS='|' read -r type ip count; do
is_white "$ip" && continue
send_alert "Nginx 5xx告警" "IP: $ip\n1分钟: $count 次"
done &
# ================= SSH 监控 =================
if [[ "$SSH_MODE" == "journald" ]]; then
SRC="journalctl -f _COMM=sshd"
else
SRC="tail -F $SSH_LOG"
fi
bash -c "$SRC" | grep "Failed password" | awk -v t="$SSH_THRESHOLD" -v s="$SILENT" '
{
for(i=1;i<=NF;i++){
if($i=="from"){ ip=$(i+1); break }
}
if(ip=="") next
now=systime()
key=ip
events[key][++cnt[key]]=now
for(i in events[key]){
if(now - events[key][i] > 60) delete events[key][i]
}
n=0
for(i in events[key]) n++
if(n >= t && now - last[key] > s){
last[key]=now
print "SSH|"ip"|"n
fflush()
}
}' | while IFS='|' read -r type ip count; do
is_white "$ip" && continue
send_alert "SSH暴力破解告警" "IP: $ip\n失败次数: $count"
done &
wait -n
exit 1
三、配置为Systemd服务
为确保脚本在后台稳定运行并具备开机自启、故障重启能力,建议将其配置为Systemd服务。创建文件 /etc/systemd/system/log-alert.service:
[Unit]
Description=Log Alert Service
After=network.target
[Service]
ExecStart=/bin/bash /usr/local/bin/log-alert.sh
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal
NoNewPrivileges=yes
ProtectSystem=full
[Install]
WantedBy=multi-user.target
执行以下命令启用服务:
sudo systemctl daemon-reload
sudo systemctl enable --now log-alert.service
四、关键实施要点说明
1. 关于“近似滑动窗口”
本脚本实现的并非精确到毫秒的时间序列数据库级滑动窗口,而是通过每秒清理超时事件进行近似模拟。对于运维告警场景——核心目标是快速发现异常趋势而非精确审计——此精度已完全满足需求。
2. 状态存储位置
脚本运行时状态(如上次告警时间戳)存储于/dev/shm目录,该目录基于内存文件系统,读写速度快。需注意,服务器重启后状态数据会丢失,此属预期行为,不影响告警功能本身。
3. 白名单功能
脚本提供了基础的IP白名单匹配功能。请注意,其中CIDR格式(如10.0.0.0/8)的匹配在示例中仅为字符串匹配,未实现完整的网络地址计算。在生产环境中,若需严格的CIDR支持,可引入ipcalc工具或进行相应逻辑扩展。
4. 日志格式依赖性
脚本默认假设Nginx日志为标准格式。若您的日志格式经过自定义调整,务必修改awk命令中获取IP($1)和状态码($9)的字段位置,否则监控将失效。
