
1. 项目概述从“爱扫站”到主动防御最近在整理服务器日志时发现了一个有趣又让人头疼的现象总有一些IP地址像不知疲倦的“清洁工”一样24小时不间断地扫描我的网站。它们尝试各种默认后台路径、探测已知漏洞、甚至暴力破解登录入口。虽然大部分现代Web应用框架和服务器都有基础的安全防护但放任这些“爱扫站”的IP持续骚扰不仅浪费服务器资源、产生大量无效日志更关键的是它们暴露了潜在的攻击意图。今天要聊的就是如何从被动记录转为主动出击为这些不请自来的“访客”建立一份专属的“黑名单”。这个“IP黑名单”项目本质上是一个基于行为的动态防火墙规则集。它不是一个简单的静态列表而是一套从日志分析、威胁判定到自动封禁的完整流程。对于任何拥有线上服务无论是个人博客、企业官网还是API服务的运维人员或开发者来说手动处理攻击日志是低效且不可持续的。通过自动化脚本我们可以将那些在短时间内进行高频次恶意扫描、尝试非法访问的IP地址自动识别出来并实时将其加入系统的防火墙如iptables、firewalld或Web服务器如Nginx、Apache的拒绝访问列表中从而实现服务器层面的主动防御。适合阅读这篇内容的朋友包括个人站长、中小型企业运维、对服务器安全有初步了解的开发者以及任何希望提升自己服务安全水位又不想依赖昂贵商业WAFWeb应用防火墙的实践者。整个过程将围绕Linux服务器环境展开用到的工具都是开源且常见的核心思想是“用自动化对抗自动化攻击”。2. 核心思路与方案选型为什么是“日志分析动态封禁”面对海量的服务器访问日志手动筛选恶意IP无异于大海捞针。因此我们的核心思路是通过程序自动分析特定时间段内的访问日志根据预设的规则如单位时间内访问特定敏感路径的次数、返回404状态码的频率等识别出可疑IP然后调用系统命令将其封禁。这个方案有几个关键优势。首先成本极低完全利用现有服务器和开源工具无需额外硬件或软件投入。其次响应迅速可以实现近实时的封禁攻击者刚扫描几分钟就可能被踢出局。再者高度可定制你可以根据自己服务的具体特点定义什么样的行为算是“恶意扫描”。例如对于后台登录页面/wp-admin或/admin的频繁访问显然比访问首页更值得警惕。在技术选型上我们主要需要解决两个问题日志分析引擎和封禁执行器。对于日志分析awk、grep、sed这些Linux文本处理“三剑客”是轻量级任务的绝佳选择。它们速度快几乎在所有Linux发行版上都预装非常适合处理按行存储的Nginx或Apache日志。如果规则更复杂或者需要连接数据库进行历史记录查询那么用Python配合re正则表达式模块会是更灵活强大的选择。本项目我们将以Python为例因为它可读性更好便于后续添加更复杂的逻辑。对于封禁执行器主流选择有两个系统防火墙如iptables传统或firewalldCentOS/RHEL 8 Fedora。直接在网络层丢弃该IP的所有数据包效果最彻底对Web服务器软件透明。Web服务器层封禁如在Nginx配置文件的server块内使用deny指令或在Apache中使用Require not ip。这仅在应用层生效效率略低于防火墙但配置更简单且不影响服务器上其他服务。注意直接操作iptables需要root权限且规则配置不当可能导致自己无法远程连接服务器。务必在操作前确保有通过控制台如云服务商的VNC登录服务器的备用方案。综合考虑我们将采用“Python分析Nginx日志 调用iptables封禁”的组合。这套方案通用性强封禁力度大是生产环境常见的实践。整个系统的运行将由crontab定时任务驱动例如每5分钟执行一次分析脚本实现准实时防护。3. 实战环境准备与日志格式解析在开始编写脚本之前我们需要确保环境就绪并深刻理解要分析的“原材料”——服务器访问日志的格式。3.1 环境与权限检查首先确认你的服务器环境。通过ssh登录后可以快速检查# 查看系统版本和内核 cat /etc/os-release uname -a # 检查Python3是否安装 python3 --version # 或 python --version # 检查iptables是否可用 which iptables sudo iptables -L -n | head -20 # 查看现有规则需要sudo权限确保你使用的账号有执行sudo iptables命令的权限。通常需要将用户加入sudoers组或者为特定的iptables命令配置免密码sudo。出于安全我们更建议后者。可以使用visudo命令编辑/etc/sudoers文件添加一行请将your_username替换为实际用户名your_username ALL(ALL) NOPASSWD: /sbin/iptables这样脚本中调用sudo iptables时就不需要交互式输入密码了。3.2 理解Nginx日志格式Nginx的访问日志通常位于/var/log/nginx/access.log也可能在/etc/nginx/nginx.conf或站点配置中指定。日志的默认格式是“combined”格式一条记录看起来像这样123.45.67.89 - - [10/May/2024:15:32:01 0800] GET /wp-login.php HTTP/1.1 404 162 - Mozilla/5.0 (compatible; SomeBot/1.0)我们需要拆解每个字段的含义以便用程序提取关键信息123.45.67.89: 客户端的IP地址。这是我们最关心的字段。[10/May/2024:15:32:01 0800]: 访问的时间戳。GET /wp-login.php HTTP/1.1: 请求方法、请求的URI资源路径和HTTP协议版本。/wp-login.php就是一个典型的扫描目标。404: HTTP状态码。404表示未找到大量404请求往往意味着扫描器在盲猜路径。162: 返回给客户端的数据包大小字节。-:Referer请求头信息。Mozilla/5.0 (compatible; SomeBot/1.0):User-Agent字符串。很多扫描器会在此暴露自己比如包含scan、bot、python-requests等关键词但更狡猾的攻击者会伪装成普通浏览器。我们的分析逻辑将主要围绕IP地址、请求URI和状态码这三个核心字段展开。例如我们可以定义一个规则“在过去5分钟内来自同一个IP地址对/admin、/wp-admin、/phpmyadmin等敏感路径的访问请求超过10次且其中404状态码占比超过70%则判定该IP为扫描器加入黑名单。”4. 核心脚本编写从日志分析到自动封禁接下来我们将一步步构建这个自动化的“IP黑名单”系统。脚本分为三个主要部分日志读取与解析、恶意IP判定、执行封禁操作。4.1 日志读取与时间窗口筛选首先我们需要读取最近一段时间内的日志。直接分析整个日志文件是不现实的因为文件可能很大。我们利用日志的时间戳只分析最近N分钟的数据。#!/usr/bin/env python3 # -*- coding: utf-8 -*- # 文件名: ip_blacklist.py import re import sys from datetime import datetime, timedelta import subprocess import logging # 配置日志方便调试和记录封禁操作 logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(/var/log/ip_blacklist.log), logging.StreamHandler(sys.stdout) ] ) logger logging.getLogger(__name__) # 配置文件区域根据实际情况修改 NGINX_LOG_PATH /var/log/nginx/access.log # 分析最近多少分钟内的日志 TIME_WINDOW_MINUTES 5 # 封禁时长单位秒86400秒24小时 BAN_DURATION 86400 # 敏感路径列表可根据自己站点情况增删 SENSITIVE_PATHS [ /wp-admin, /wp-login.php, /admin, /administrator, /phpmyadmin, /mysql, /db, /config, /.env, /api/login, /user/login, /backup, /shell ] # 判定为恶意的阈值在时间窗口内访问敏感路径的总次数 THRESHOLD_ACCESS_COUNT 15 # 另一个判定维度在时间窗口内404状态码的请求次数 THRESHOLD_404_COUNT 10 def parse_nginx_log_line(line): 解析单条Nginx combined格式日志。 返回一个字典包含ip, time, method, uri, status等字段。 如果解析失败返回None。 # 使用正则表达式匹配combined格式 # 这个正则比简单的split更健壮能处理User-Agent中包含空格等复杂情况 pattern r(?Pip\S) \S \S \[(?Ptime[^\]])\] (?Pmethod\S) (?Puri\S) \S (?Pstatus\d) (?Psize\S) [^]* (?Pua[^]*) match re.match(pattern, line) if match: return match.groupdict() else: # 如果正则匹配失败可以尝试简单的空格分割可靠性较低 parts line.split() if len(parts) 10: return { ip: parts[0], time: parts[3].strip([), uri: parts[6], status: parts[8] } return None这段代码定义了日志解析函数。我们使用正则表达式来精确匹配日志格式并提供了一个备用的简单分割方法。SENSITIVE_PATHS列表需要你根据自己网站的实际结构进行定制把那些不希望被公开访问或经常被扫描的管理后台、配置文件和API端点加进去。4.2 恶意IP判定逻辑实现有了解析函数我们需要遍历日志文件筛选出时间窗口内的记录并按IP进行聚合分析。def analyze_logs(): 分析日志返回判定为恶意的IP列表。 malicious_ips [] ip_access_info {} # 结构{ip: {sensitive_count: 0, 404_count: 0, total: 0}} # 计算时间窗口的起始时间点 time_window_start datetime.now() - timedelta(minutesTIME_WINDOW_MINUTES) try: with open(NGINX_LOG_PATH, r, encodingutf-8, errorsignore) as f: for line in f: data parse_nginx_log_line(line) if not data: continue # 解析日志时间并判断是否在时间窗口内 # Nginx日志时间格式: 10/May/2024:15:32:01 0800 try: log_time_str data[time].split()[0] # 取日期时间部分忽略时区 # 注意strptime的格式符必须与日志完全匹配 log_time datetime.strptime(log_time_str, %d/%b/%Y:%H:%M:%S) except ValueError as e: logger.warning(f解析时间失败: {data[time]}, 错误: {e}) continue if log_time time_window_start: # 如果日志时间早于时间窗口起点由于日志是按时间追加的可以提前结束如果日志是顺序的 # 但为了严谨我们继续读取因为日志文件可能被轮转或不是严格按时间排序 continue ip data[ip] uri data[uri] status data[status] # 初始化该IP的记录 if ip not in ip_access_info: ip_access_info[ip] {sensitive_count: 0, 404_count: 0, total: 0} ip_access_info[ip][total] 1 # 检查是否访问了敏感路径 for sensitive_path in SENSITIVE_PATHS: if sensitive_path in uri: ip_access_info[ip][sensitive_count] 1 break # 只要匹配一个敏感路径就计数一次 # 检查是否是404状态 if status.startswith(404): ip_access_info[ip][404_count] 1 except FileNotFoundError: logger.error(f日志文件不存在: {NGINX_LOG_PATH}) return [] except Exception as e: logger.error(f读取日志文件时发生未知错误: {e}) return [] # 根据阈值判定恶意IP for ip, info in ip_access_info.items(): # 规则1访问敏感路径次数过多 if info[sensitive_count] THRESHOLD_ACCESS_COUNT: malicious_ips.append(ip) logger.info(fIP {ip} 被判定为恶意敏感路径访问 {info[sensitive_count]} 次) continue # 符合一条规则就加入避免重复 # 规则2产生大量404错误可能是在扫描不存在的路径 if info[404_count] THRESHOLD_404_COUNT: malicious_ips.append(ip) logger.info(fIP {ip} 被判定为恶意404错误 {info[404_count]} 次) logger.info(f分析完成共发现 {len(malicious_ips)} 个可疑IP。) return malicious_ips判定逻辑采用了两种并行规则满足任一即触发。这比单一规则更有效因为有的扫描器会小心翼翼地避开已知敏感路径但依然会产生大量404而有的则直接对管理后台进行高频撞击。阈值THRESHOLD_ACCESS_COUNT和THRESHOLD_404_COUNT需要根据你站点的实际流量进行调整。对于一个日PV几千的小站5分钟内出现15次敏感访问已经非常可疑而对于一个高流量站点这个阈值可能需要调高。4.3 封禁执行与黑名单管理识别出恶意IP后我们需要通过iptables将其封禁。同时为了避免重复封禁和便于管理我们最好将黑名单持久化存储。def ban_ip_with_iptables(ip_list): 使用iptables封禁指定的IP列表。 采用在INPUT链头部插入DROP规则的方式。 if not ip_list: return # 读取已封禁的IP列表避免重复操作 banned_ips set() try: # 检查iptables规则获取已存在的封禁IP # 这里假设我们使用一个特定的链或标记来管理为了简单我们检查INPUT链中已有的DROP规则 # 注意这个方法在规则很多时可能效率不高生产环境建议使用独立的iptables链管理 result subprocess.run( [sudo, iptables, -L, INPUT, -n, --line-numbers], capture_outputTrue, textTrue, checkFalse ) for line in result.stdout.split(\n): if DROP in line and 0.0.0.0/0 not in line: # 简单提取IP实际规则可能更复杂这里仅为示例 parts line.split() for part in parts: # 一个简单的IPv4地址匹配 if re.match(r^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$, part): banned_ips.add(part) break except Exception as e: logger.error(f读取现有iptables规则失败: {e}) for ip in ip_list: if ip in banned_ips: logger.info(fIP {ip} 已在黑名单中跳过。) continue # 执行封禁命令 # 在INPUT链的头部插入一条规则丢弃来自该IP的所有数据包 cmd [sudo, iptables, -I, INPUT, 1, -s, ip, -j, DROP] try: subprocess.run(cmd, checkTrue) logger.warning(f已封禁IP: {ip}) # 可选添加日志标记方便后续审计 # subprocess.run([sudo, iptables, -I, INPUT, 2, -s, ip, -j, LOG, --log-prefix, fBLACKLIST_DROP: ]) except subprocess.CalledProcessError as e: logger.error(f封禁IP {ip} 失败: {e}) except Exception as e: logger.error(f执行封禁命令时发生未知错误: {e}) def main(): logger.info( IP黑名单脚本开始执行 ) malicious_ips analyze_logs() if malicious_ips: ban_ip_with_iptables(malicious_ips) else: logger.info(未发现需要封禁的IP。) logger.info( IP黑名单脚本执行结束 \n) if __name__ __main__: main()封禁函数ban_ip_with_iptables做了几件重要的事先尝试读取现有的iptables规则构建一个已封禁IP的集合避免重复添加规则导致iptables规则链冗长。使用iptables -I INPUT 1 -s IP -j DROP命令。-I INPUT 1表示在INPUT链的第1条位置插入规则确保它优先于其他可能允许的规则。-j DROP直接丢弃数据包不回应任何信息让扫描器感觉像是遇到了一个“黑洞”。添加了详细的日志记录所有封禁操作都会记录到/var/log/ip_blacklist.log文件和标准输出中便于事后审计和排查问题。重要提示直接在INPUT链操作存在一定风险。更规范的做法是创建一个自定义的链比如BLACKLIST将所有封禁规则放在那里然后在INPUT链的头部跳转到这个自定义链。这样管理起来更清晰也便于批量清空或修改黑名单规则。考虑到本文的入门导向我们使用了直接操作INPUT链的简单方法但在生产环境中建议采用自定义链的方式。5. 部署、调度与进阶优化脚本写好了但它不会自己运行。我们需要将其部署到服务器并设置定时任务。5.1 脚本部署与权限设置保存脚本将上面的完整代码保存为/usr/local/bin/ip_blacklist.py。赋予执行权限sudo chmod x /usr/local/bin/ip_blacklist.py测试脚本可以先手动运行一次检查是否有语法错误以及日志输出是否正常。sudo python3 /usr/local/bin/ip_blacklist.py查看/var/log/ip_blacklist.log和终端输出确认脚本能正确读取日志并执行分析。5.2 使用Crontab设置定时任务我们希望脚本每5分钟自动执行一次实现近实时防护。编辑当前用户的crontabcrontab -e在文件末尾添加一行*/5 * * * * /usr/bin/python3 /usr/local/bin/ip_blacklist.py /dev/null 21这行配置表示每5分钟执行一次脚本并将所有标准输出和错误输出重定向到/dev/null丢弃因为我们已经在脚本内部将重要信息记录到独立的日志文件了。注意确保python3的路径正确可以使用which python3命令查看。使用/dev/null重定向是为了避免cron的邮件通知但调试阶段可以去掉 /dev/null 21以便在/var/mail/$USER中查看任何错误输出。5.3 进阶优化与功能扩展基础版本已经能工作但一个健壮的生产级系统还需要考虑更多1. 封禁自动过期与释放我们目前是永久封禁直到手动移除。更合理的做法是设置封禁时长。这可以通过两种方式实现方式A使用iptables的-m comment模块和定时清理脚本。封禁时添加一个带有时间戳的注释sudo iptables -I INPUT 1 -s 123.45.67.89 -m comment --comment ban_$(date %s) -j DROP然后另一个定时脚本比如每天凌晨运行检查所有带comment的规则如果时间戳超过24小时或你设定的BAN_DURATION就删除该规则。方式B在应用层管理。将封禁的IP和封禁时间存入一个小型数据库如SQLite或文件。每次分析脚本运行时先检查“黑名单数据库”释放那些已过期的IP即调用iptables -D删除对应规则然后再分析新的日志。这种方式更灵活可以轻松实现不同IP不同封禁时长的策略。2. 误封处理与白名单机制自动封禁难免有误伤比如自己频繁测试后台或者某个搜索引擎的合法爬虫如Googlebot触发了规则。因此必须建立白名单机制。静态白名单在脚本开头定义一个WHITELIST_IPS列表包含你自己的办公IP、家庭IP、云服务商监控IP等。在判定为恶意IP后执行封禁前先检查该IP是否在白名单内。动态豁免对于某些虽然触发规则但可能是合法的IP例如你忘记密码导致登录失败多次可以设计一个“申诉”或“验证”环节。例如不直接DROP而是先将其重定向到一个验证页面使用iptables的REDIRECT或Web应用层实现通过验证则加入临时白名单一段时间。3. 性能优化与日志轮转分析增量日志每次都从头读取整个日志文件是低效的。可以记录上次分析到的日志文件位置行号或时间戳下次从该位置开始读取。或者更简单的方法是利用logrotate机制分析完成后在日志中做一个标记或者直接分析access.log而历史归档的access.log.1.gz等文件则不再分析。使用更高效的数据结构如果站点流量巨大ip_access_info字典可能会变得很大。可以考虑使用collections.defaultdict或对IP进行聚合分析时只保留最近时间窗口内活跃的IP定期清理旧数据。4. 报警与通知集成单纯的封禁还不够知道“谁”在攻击也很重要。可以将封禁信息通过其他渠道通知你发送邮件使用smtplib库当封禁了新的IP时发送一封邮件到你的管理邮箱包含IP、封禁时间、触发原因敏感访问/404过多。集成即时通讯工具通过调用Webhook将报警信息发送到Slack、钉钉、企业微信或飞书群中实现实时感知。6. 常见问题排查与操作心得在实际部署和运行过程中你可能会遇到以下问题。这里记录了我踩过的一些坑和对应的解决方案。6.1 权限问题导致封禁失败问题描述脚本执行时报错sudo: no tty present and no askpass program specified或直接Permission denied。排查与解决检查sudoers配置确保已按照3.1节所述为运行脚本的用户配置了无需密码执行/sbin/iptables的权限。使用sudo visudo检查配置是否正确注意语法。检查命令路径脚本中使用的命令路径必须是绝对路径。iptables通常位于/sbin/或/usr/sbin/。可以使用which iptables确认。以root用户运行cron最直接但安全性稍低的方法是将定时任务添加到root用户的crontab中sudo crontab -e这样就不需要配置sudo免密了。6.2 误封自己或重要服务IP问题描述脚本运行后发现自己无法SSH连接服务器或者网站监控告警。紧急处理通过控制台登录立即通过云服务商提供的VNC或串口控制台登录服务器。查看并删除iptables规则# 列出INPUT链规则及行号 sudo iptables -L INPUT -n --line-numbers # 找到封禁自己IP的那条规则记下行号假设是第3行 sudo iptables -D INPUT 3 # 删除第3行规则分析原因检查白名单立刻将自己的IP加入脚本的WHITELIST_IPS。调整阈值可能是THRESHOLD_ACCESS_COUNT或THRESHOLD_404_COUNT设置过低导致正常的管理操作也被判定为攻击。根据日志流量适当调高阈值。细化敏感路径检查SENSITIVE_PATHS列表是否包含了你自己也会频繁访问的合法API路径将其从列表中移除或添加更精确的匹配规则如使用正则表达式只匹配/admin/目录下的特定危险文件。6.3 脚本执行但无任何封禁记录问题描述/var/log/ip_blacklist.log中只有开始和结束的日志没有发现恶意IP的记录。排查步骤检查日志路径确认NGINX_LOG_PATH变量指向了正确的、正在被写入的访问日志文件。有时Nginx配置了多个日志文件或者使用了日志轮转。检查时间窗口手动查看日志文件末尾确认最近几分钟确实有访问记录。可以临时将TIME_WINDOW_MINUTES调大如30分钟再次运行脚本测试。检查判定阈值阈值可能设置过高。可以临时将THRESHOLD_ACCESS_COUNT和THRESHOLD_404_COUNT调低到1或2然后运行脚本看是否能触发封禁。同时在脚本中打印出ip_access_info字典的内容查看每个IP的统计计数是否如预期。检查日志格式我们的解析函数针对的是Nginx的combined格式。如果你的Nginx使用了自定义的log_format那么正则表达式可能无法正确匹配。需要根据实际的日志格式调整parse_nginx_log_line函数中的正则表达式。6.4 iptables规则过多导致管理混乱问题描述运行一段时间后sudo iptables -L -n输出非常长难以管理。优化建议 采用之前提到的自定义链方案。以下是设置步骤# 1. 创建一个名为 BLACKLIST 的自定义链 sudo iptables -N BLACKLIST # 2. 将自定义链的规则默认动作设为DROP可选也可以在最后加一条规则 # sudo iptables -A BLACKLIST -j DROP # 3. 在INPUT链的头部引用这个自定义链 sudo iptables -I INPUT 1 -j BLACKLIST然后修改脚本中的封禁命令将规则添加到BLACKLIST链cmd [sudo, iptables, -A, BLACKLIST, -s, ip, -j, DROP]这样所有黑名单规则都集中在BLACKLIST链里查看和管理如清空所有黑名单sudo iptables -F BLACKLIST都非常方便。6.5 个人实操心得阈值设置是一门艺术不是科学初始阈值不要设得太激进。可以先设置一个较高的值比如敏感访问30次404错误50次让脚本跑几天观察日志和封禁记录。然后根据/var/log/ip_blacklist.log中记录的“嫌疑IP”的访问模式逐步调低阈值到一个合理的水平。对于个人小站5分钟内10-20次敏感访问已经非常可疑。日志是你的眼睛定期比如每周查看/var/log/ip_blacklist.log不仅看封禁了谁更要看那些“差点”被封禁的IP访问次数接近阈值。这能帮你发现新的攻击模式从而更新你的SENSITIVE_PATHS列表或判定规则。组合拳效果更佳这个IP黑名单脚本是服务器安全的一环但不应是唯一一环。务必确保你的Web应用程序本身是安全的及时更新、使用强密码、关闭不必要的服务并考虑启用Nginx的limit_req模块来限制请求频率以及使用fail2ban这类更成熟的工具来防护SSH等服务的暴力破解。多层次的防御才能构成有效的安全体系。保持敬畏谨慎操作防火墙规则是服务器的大门卫士错误的规则可能导致服务完全不可用。任何对生产环境的修改尤其是防火墙规则都建议先在测试环境验证。修改脚本或规则后第一次运行可以加上--dry-run模拟运行参数只打印将要执行的操作而不实际执行确认无误后再去掉该参数。