CentOS服务器只要开着SSH端口,日志里就少不了各种扫描和暴力破解尝试。与其每天手动翻日志封IP,不如让系统自己完成识别和封禁。下面直接给出一个可落地的恶意IP自动封禁方案,包含日志分析逻辑、脚本代码、定时任务配置以及误封处理。

恶意IP的主要判断依据

在CentOS上,绝大多数暴力破解和扫描行为都会在安全日志中留下记录。最核心的文件是/var/log/secure,只要出现“Failed password”就代表一次SSH登录失败,出现“Invalid user”说明尝试了不存在的用户名,出现“Connection closed by authenticating user”往往意味着扫描器在试探后主动断开。一个IP在短时间内反复触发这些记录,基本可以判定为恶意来源。

除了SSH,如果服务器还运行着FTP、邮件服务或Web管理后台,也需要关注对应的日志。但为了控制封禁范围,建议第一版脚本只针对SSH暴力破解,后续再按需扩展。

封禁工具选择与安装

CentOS下最稳定的封禁工具是firewalld和ipset。firewalld负责实际的网络层拦截,ipset用来存放恶意IP集合,避免防火墙规则数量膨胀导致性能下降。如果系统是最小化安装,先确认这两个组件是否存在:

rpm -q firewalld ipset

如果提示未安装,执行:

yum install -y firewalld ipset
systemctl enable --now firewalld

ipset的优势在于可以创建一个名为blacklist的集合,所有恶意IP都添加进去,防火墙只需要一条规则引用这个集合。即使封禁几千个IP,匹配效率也远高于几千条独立规则。

创建ipset黑名单集合

先创建一个hash:ip类型的集合,超时时间设置为0表示永久存在,除非手动删除:

ipset create blacklist hash:ip timeout 0 -exist

然后将这个集合接入firewalld。firewalld支持直接使用ipset作为富规则的源地址:

firewall-cmd --permanent --add-rich-rule='rule source ipset=blacklist drop'
firewall-cmd --reload

这条规则的意思是:凡是源IP在blacklist集合中的连接请求,直接丢弃。drop比reject更隐蔽,攻击方不会立刻得到响应,能增加其扫描成本。

核心封禁脚本编写

脚本的逻辑是:读取/var/log/secure中最近一段时间的失败记录,统计每个IP的出现次数,超过阈值就加入ipset黑名单。这里给出完整脚本,保存为/usr/local/bin/auto_ban_ip.sh:

#!/bin/bash
# CentOS恶意IP自动封禁脚本
# 适用于CentOS 7/8/9,依赖firewalld和ipset

LOG_FILE="/var/log/secure"
THRESHOLD=5
TIME_WINDOW_MINUTES=10
IPSET_NAME="blacklist"
LOCK_FILE="/tmp/auto_ban_ip.lock"

# 防止脚本并发执行
if [ -f "$LOCK_FILE" ]; then
    exit 0
fi
touch "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT

# 检查ipset集合是否存在,不存在则创建
if ! ipset list -n | grep -qw "$IPSET_NAME"; then
    ipset create "$IPSET_NAME" hash:ip timeout 0 -exist
fi

# 获取当前时间和时间窗口起始时间
NOW=$(date +%s)
START_TIME=$((NOW - TIME_WINDOW_MINUTES * 60))

# 提取日志中最近时间窗口内的失败记录
# 使用awk解析时间戳,只处理时间窗口内的日志行
awk -v start="$START_TIME" '
{
    # 解析syslog时间格式,例如:Jan 15 10:30:45
    month_str=$1
    day=$2
    time_str=$3
    # 将月份缩写转换为数字
    month_num = (index("JanFebMarAprMayJunJulAugSepOctNovDec", month_str) + 2) / 3
    # 获取当前年份
    "date +%Y" | getline year
    # 构造完整时间字符串
    datetime_str = year " " month_num " " day " " time_str
    "date -d \"" datetime_str "\" +%s" | getline epoch
    close("date -d \"" datetime_str "\" +%s")
    if (epoch >= start) {
        print $0
    }
}' "$LOG_FILE" | \
grep -E "Failed password|Invalid user|Connection closed by authenticating user" | \
awk '{for(i=1;i<=NF;i++){if($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/){print $i; break}}}' | \
sort | uniq -c | sort -rn | \
while read count ip; do
    if [ "$count" -ge "$THRESHOLD" ]; then
        # 检查IP是否已在黑名单中
        if ! ipset test "$IPSET_NAME" "$ip" 2>/dev/null; then
            # 排除内网IP和本机IP,避免误封
            if ! echo "$ip" | grep -qE "^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|127\.)"; then
                ipset add "$IPSET_NAME" "$ip" -exist
                echo "$(date '+%Y-%m-%d %H:%M:%S') 封禁恶意IP: $ip (失败次数: $count)" >> /var/log/auto_ban.log
            fi
        fi
    fi
done

# 清理超过30天未活动的IP,防止黑名单无限膨胀
ipset list "$IPSET_NAME" | grep -E "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | while read ip; do
    # 检查该IP在最近30天内是否有新的失败记录
    recent_fail=$(grep "$ip" "$LOG_FILE" | tail -1)
    if [ -z "$recent_fail" ]; then
        ipset del "$IPSET_NAME" "$ip" 2>/dev/null
    fi
done

exit 0

这个脚本有几个关键设计。第一,使用时间窗口而不是扫描整个日志文件,避免每次运行都重复处理大量历史数据。第二,通过awk解析syslog时间戳,确保只统计最近10分钟内的失败记录。第三,封禁前排除内网地址段,防止因为内部测试或NAT环境误封正常设备。第四,所有封禁操作都记录到/var/log/auto_ban.log,方便事后审计。

脚本执行权限与手动测试

给脚本添加执行权限:

chmod +x /usr/local/bin/auto_ban_ip.sh

在配置定时任务之前,先手动执行一次,确认没有语法错误:

/usr/local/bin/auto_ban_ip.sh

如果没有任何输出,说明脚本运行正常。可以查看封禁日志确认是否产生了记录:

cat /var/log/auto_ban.log

要验证封禁是否生效,可以从另一台机器连续输入错误密码尝试SSH登录,然后检查ipset集合:

ipset list blacklist

如果看到测试机的IP出现在列表中,说明整个链路已经打通。

配置定时任务自动执行

恶意IP的扫描频率很高,建议每5分钟运行一次脚本。使用crontab配置:

crontab -e

添加以下内容:

*/5 * * * * /usr/local/bin/auto_ban_ip.sh >/dev/null 2>&1

5分钟的间隔足够及时响应暴力破解,又不会给系统带来明显的CPU负担。如果服务器攻击压力特别大,可以缩短到2分钟,但不建议低于1分钟,因为日志解析本身也需要消耗资源。

扩展封禁范围到其他服务

如果服务器还运行着vsftpd或Postfix,这些服务的失败登录也会写入/var/log/secure或/var/log/maillog。可以修改脚本中的grep匹配模式,加入对应的失败关键词。例如FTP的“530 Login incorrect”、邮件服务的“SASL LOGIN authentication failed”。但要注意,不同服务的日志格式不同,IP提取逻辑可能需要调整。

更稳妥的做法是使用fail2ban。fail2ban本身就是为多服务日志分析设计的,支持SSH、FTP、Apache、Nginx等多种过滤器。但fail2ban在CentOS上需要EPEL仓库,而且配置相对复杂。对于只需要SSH防护的场景,上面这个自定义脚本足够轻量和可控。

误封处理与白名单机制

自动封禁最大的风险是误封。比如公司出口IP因为多人同时输错密码被判定为恶意,或者运维人员自己手滑连续输错。脚本中已经排除了内网地址段,但如果运维人员从外部访问,仍然可能被误封。

处理误封的方法是手动从ipset中删除:

ipset del blacklist 203.0.113.50

要彻底避免某个IP被封,可以建立一个白名单ipset集合,并在脚本中加入白名单检查。创建白名单集合:

ipset create whitelist hash:ip timeout 0 -exist

将信任的IP加入白名单:

ipset add whitelist 203.0.113.50 -exist

然后在脚本封禁逻辑之前增加白名单判断:

if ipset test whitelist "$ip" 2>/dev/null; then
    continue
fi

这样即使白名单IP出现了大量失败记录,也不会被自动封禁。

日志轮转与脚本兼容性

CentOS默认使用logrotate管理日志,/var/log/secure可能会被轮转为secure.1、secure.2等文件。脚本只读取当前的/var/log/secure,如果日志刚被轮转,可能会漏掉轮转前几分钟的记录。对于5分钟执行一次的频率来说,这个影响很小。但如果需要更精确,可以在脚本中同时检查轮转后的日志文件,比如/var/log/secure.1。

另外,CentOS 8和CentOS 9的日志时间戳格式与CentOS 7一致,都是syslog标准格式,脚本中的awk解析逻辑可以通用。如果系统配置了systemd-journald而不是传统的rsyslog,/var/log/secure可能不存在,需要改用journalctl命令提取日志。不过CentOS默认安装仍然保留rsyslog,所以直接读文件即可。

性能影响与资源消耗

这个脚本每次运行只处理最近10分钟的日志,日志量通常很小,awk和grep的处理速度在毫秒级别。ipset的查询和添加操作也是内存级操作,即使黑名单中有几千个IP,也不会对网络性能产生可感知的影响。firewalld使用ipset匹配时,底层是哈希表查找,时间复杂度接近O(1),比线性匹配iptables规则高效得多。

如果服务器每天遭受大量攻击,黑名单可能快速增长。脚本中已经加入了清理逻辑,但清理条件比较简单,只删除最近30天内没有失败记录的IP。对于长期不活跃的攻击源,这个策略有效。如果发现黑名单膨胀过快,可以手动清空重建:

ipset flush blacklist

方案总结与使用建议

这套方案的核心价值在于把被动查看日志变成主动自动响应。脚本部署完成后,暴力破解尝试会在5分钟内被自动拦截,攻击者无法持续尝试密码组合。配合SSH密钥登录、修改默认端口、禁用root远程登录等措施,可以大幅降低服务器被攻破的风险。

对于生产环境,建议先在测试机上验证脚本逻辑,确认无误后再部署到正式服务器。同时保留/var/log/auto_ban.log的定期检查习惯,观察封禁行为是否符合预期。如果发现某个正常用户被误封,及时加入白名单并优化阈值。阈值设置得越低越敏感,误封概率也越高;阈值设置得越高越宽松,但响应速度会变慢。根据实际日志中的失败频率,5次/10分钟是一个比较均衡的起点。