Nginx日志安全分析实战:用nginx_log_check发现异常访问
很多做运维的朋友应该都碰到过类似的情况业务流量看着挺正常凌晨两三点后端负载却莫名其妙飙高赶紧登上服务器一看Nginx 的 access.log 里塞满了 404、畸形 URL 和各种来路不明的扫描请求。排查一顿之后发现没有实际业务损失但吓一跳是免不了的。这类问题光靠人眼盯日志是盯不过来的这也是我花业余时间写了一个叫 nginx_log_check 的小工具的原因。这篇文章就来聊聊这个项目是怎么从需求变成可落地脚本的以及我在做 Nginx 日志安全分析时踩过的坑和沉淀下来的经验。如果你正在维护 Nginx 服务或者刚接触日志分析、想知道怎么从日志里快速发现异常访问这篇内容应该对你有用。1. 为什么要用 Nginx 日志做安全分析很多人的第一反应是安全分析不是应该买设备、上防护系统吗自己看日志能看出什么理解这个问题得先明白日志本身的价值。1.1 日志是服务器的“行车记录仪”Nginx 的 access.log 和 error.log 会记录下每一条请求的来源 IP、请求时间、请求方法、URI、状态码、响应大小、User-Agent 等信息。说得直白一点这就是服务器的“行车记录仪”——谁来过、什么时候来的、干了什么、结果是成功还是失败全部都有痕迹。大多数攻击行为在真正发生之前都逃不过踩点、扫描、试探这几个阶段。攻击者也好扫描工具也好通常会先发大量请求来探测服务器上有没有常见后台路径、备份文件、敏感接口。这些探测请求最终都会落到 Nginx 日志里。所以不去追查日志其实等于把犯罪现场的录像带丢在一边不看。error.log 的价值同样不可小觑。当有异常请求触发 Nginx 报错时比如访问不存在 server_name 的请求、SSL 握手异常、上游连接超时error.log 里会留下记录。这些信息有时候比 access.log 更能反映问题因为畸形请求往往是攻击者在找非标准入口。1.2 日志分析到底能发现什么基于我自己的维护经验日志分析至少能发现下面几类典型问题目录扫描和暴力破解。攻击者会逐个尝试常见的路径比如 /admin、/api/v1、/.git、/wp-login.php或者反复尝试登录接口试图撞出弱口令。异常状态码分布。正常情况下一个健康站点的 404 比例是比较低的。如果某个时间段 404 突然猛增大概率是有人在遍历路径或者扫后台。爬虫和采集流量。搜索引擎的爬虫会留存正常运行轨迹但很多非知名爬虫、SEO 采集工具的访问频率极高还会抓走页面里的内容。日志里可以看 UA 特征和访问习惯来区分。超大请求体和畸形请求。有些请求会带着很长的参数、奇怪的字符可能是注入尝试也可能是边界测试。这种请求在日志里会表现出 URI 超长、参数结构异常等特点。单 IP 高并发非正常访问。某个 IP 在极短时间内发起大量请求远超人工访问速度基本可以判定是脚本行为。如果靠人工分析这些日志一天的访问量不大还能扛一旦业务有流量基础日志文件按 GB 增长可读性就趋近于零。nginx_log_check 的设计目标就是把“读原始日志”变成“读检查结果”把安全分析从人肉模式变成半自动化模式。2. 工具设计与方案选型在动手写工具之前我其实先回答了自己三个问题边界在哪里、用什么语言、要做成什么样。2.1 先想清楚要解决哪几个问题日志安全分析工具的核心任务不是“找一次问题”而是“持续发现问题”。所以第一件事是明确边界我不希望它做一个流量实时拦截工具那样会把问题复杂化。nginx_log_check 的定义是分析工具它的职责是定时扫描 Nginx 日志输出可疑访问清单和统计报告再由运维人员决定是否告警、封禁或追查。这个边界非常重要。一旦工具背上“实时阻断”“自动封禁”等功能对自身稳定性、误判率和响应速度的要求会呈指数级上升。比如误封一个正常用户比漏掉一次扫描更可怕。先定位成分析工具跑稳了再加后续动作这种渐进式思路在真实运维环境里更稳妥。确定边界之后我把需求整理成了清单能解析默认格式的 Nginx access.log 和 error.log。能按 IP、URI、状态码、User-Agent 做聚合统计。能过滤出目录扫描、异常状态码、爬虫特征等安全事件。输出结果要可读、可存档最好能顺手发个通知。对机器性能影响要小不能因为跑个日志分析就把业务拖垮。2.2 技术栈选型为什么从 Shell 起步很多人看到日志分析第一反应是上 Python毕竟 Python 处理数据方便。但我最终选择的是“ Bash awk grep 做快速过滤Python 做复杂规则匹配 ”的组合方案。为什么这么选日志分析最多的时候面对的是几个 GB 甚至更大的文件如果全量读入 Python 慢慢解析第一是慢第二是内存压力大。而 awk 和 grep 是天生干这活儿的它们做流式处理边读边匹配不用把整个文件加载进内存性能优势在文本日志场景下非常明显。而且Nginx 日志格式相对标准配合 awk 可以快速完成字段切分。安全检测中很多规则其实不需要复杂的逻辑比如“找出状态码为 404 的请求 Top IP”这类统计awk 几行代码就能完成。只有碰到模板匹配、多条件联合判断时Python 脚本才会更顺手。所以最终的架构是外层用 Shell 脚本做文件定位、格式判断、结果归档中间用 awk 做高频但简单的统计最后把命中多条件的可疑记录交给 Python 模式匹配。整套逻辑下来一台低配云主机跑千万级日志量也不会卡顿。2.3 nginx_log_check 的模块结构nginx_log_check 的代码结构不复杂但对于这种小工具来说已经很够用。整体分为四块入口脚本。负责读取配置文件、锁定当天日志文件、调用后续模块处理完输出汇总信息。字段解析模块。因为日志格式可能因业务调整而变化所以单独做成一个模块保证后续规则和字段解析解耦。规则引擎。这是一个纯规则配置目录每条规则有自己的匹配条件和优先级。规则之间互不干扰方便增删。输出模块。负责把结果整理成文本报告、JSON 文件并按配置决定是否发送告警通知。这样的模块划分最大的好处是“加规则不用动主程序”。实际使用中你经常会遇到新的异常流量特征如果每次都要改主程序工具很快就脆弱得没法维护。规则独立之后加一条规则等于加一行配置实用性大幅提升。3. 核心检测逻辑与实操要点工具真正有价值的地方在于检测逻辑。这部分我挑几个最核心的点展开说。3.1 日志字段拆解做日志分析的第一步肯定是看懂日志字段。Nginx 默认的 access.log 格式长这样127.0.0.1 - - [10/Aug/2024:06:00:01 0800] GET /index.html HTTP/1.1 200 1024 - Mozilla/5.0 ...这条日志从左到右分别是客户端 IP、身份标识一般恒为 -、认证用户名一般恒为 -、时间戳、请求行方法、URI、协议、状态码、响应字节数、Referer、User-Agent。对安全分析来说其中最重要的字段是 IP、状态码、请求 URI 和 User-Agent。IP 负责定位来源状态码负责暴露结果URI 负责揭示意图User-Agent 则经常能透露出对方使用的工具类型。有的团队习惯把日志格式改成 JSON 格式比如加一个log_format json配置。这么做对日志分析有巨大帮助因为字段语义清晰解析不容易出错。如果你还处在可以调整日志格式的阶段建议优先考虑 JSON 格式输出后续用 jq 或者 Python 解析都会舒服很多。3.2 状态码异常检测逻辑nginx_log_check 里的第一个核心检测逻辑是状态码聚合。逻辑很朴素把一段时间内的日志按状态码归类统计数量和占比如果某个状态码的占比超过阈值就重点看对应请求。这里有个经验不要一看到 404 就紧张要结合基线和上下文。比如某个站点的正常 404 比例可能是 1%突然某天变成 15%那大概率是扫描。但如果你的站点本来就有很多前端图片请求历史遗留路径404 比例常年偏高那就是另一回事。工具里我设置了两类阈值。第一类是绝对阈值单个 IP 在一小时内产生的 404 数量超过 50第二类是比例阈值404 请求占总请求量的比例超过 20%。两个阈值取“或”的关系只要命中一个就会进一步检查请求的具体 URI。awk {split($0, arr, \); if (arr[3] ~ /^ 404/) {print $1}} access.log | sort | uniq -c | sort -nr | head -20这条命令虽然简单但在实际验证阶段非常好用。它快速列出 404 状态码对应的 IP 排行如果排在最前面的 IP 还带着一堆陌生 URI基本可以直接列入可疑名单。3.3 请求路径与 UA 特征识别路径特征是识别恶意请求最直接的信号。正常的业务请求总是集中在你暴露出来的那几个 API 和页面上而扫描行为的特点是“广撒网”——尝试各种常见的后台路径、搜索引擎敏感路径、备份文件路径。在 nginx_log_check 里我把路径规则分成两类。第一类是敏感路径直接命中比如 /.git/HEAD、/.env、/wp-login.php、/actuator/env、/phpMyAdmin/ 之类的路径一旦出现基本就是扫描器在试探直接标记为高可疑。第二类是路径模式匹配比如连续访问大量不存在的路径、URI 里带上奇怪的编码字符、请求的参数名异常长等。User-Agent 也是重要线索。很多扫描工具会带固定的 UA 特征比如某些开源扫描器的 UA 里会带上工具名称和版本号一眼就能认出来。但也要注意很多恶意流量会伪造浏览器 UA比如把自己的 UA 改成和 Chrome 一样再配合真实的浏览器 headless 行为这时候只看 UA 就不够了。我的处理方式是双通道UA 命中明确的黑名单特征时直接告警UA 很普通但路径和状态码都异常时进入二次判断逻辑。也就是说单靠 UA 不看路径会漏报单靠路径不看 UA 会误报两个维度交叉验证结果才靠谱。3.4 请求频率与并发行为分析访问频率是另一个有效指标。统计单位时间窗口内单 IP 的请求数量发现远超正常人类操作速度的访问行为这个行为本身就值得关注。正常用户通过浏览器访问一个网页时状态通常是“访问一个 URL、加载页面、思考几秒、再访问下一个”。而扫描器的行为模式是“毫秒级连续请求、并发通路、路径规律递增”。这两种行为特征在日志的时间戳和请求序列上非常明显。我在工具里设定了一个简化版的频率检测器把日志按“IP 分钟”分组统计每分钟请求量。如果单 IP 每分钟请求量超过 200同时来源 IP 没有任何浏览器的完整请求链也就是缺少静态资源的二次请求就极高概率是脚本行为。awk {split($4, time, :); keytime[2]:time[3]_$1; count[key]} END {for (k in count) if (count[k] 200) print count[k], k} access.log | sort -nr | head这个脚本核心就是按分钟切割时间字段再用 IP 做聚合。看起来简单但正是这种基础数据的支撑才能帮助我发现凌晨那波爬虫流量来自哪几个 IP、集中在哪个时间段。4. 完整实操过程与代码实现讲了原理来说说怎么落地。整个 nginx_log_check 的部署和使用流程我拆成五步来介绍了。4.1 准备环境与确认日志位置第一步确认 Nginx 日志的位置和格式。不同版本的 Nginx、不同发行版日志路径有差异。先执行命令nginx -V 21 | grep -o \-\-conf-path\S*或者更直接一点查看 Nginx 配置文件里的access_log指令grep -r access_log /etc/nginx/我建议把日志路径写进 nginx_log_check 的配置文件中不要写在代码里。因为日志可能做轮转切割路径会变如果直接写死切到新文件之后工具就失明了。配置文件的方式更灵活。同时注意 Nginx 日志的权限。如果 nginx_log_check 以普通用户身份运行要确认该用户对日志目录有读取权限。很多日志目录默认是 nginx 用户所有直接用 root 跑 Cron 虽然能读但不安全。我的做法是建一个专用的日志分析用户把它加入日志文件组的读权限集合里。4.2 编写配置与自定义规则nginx_log_check 的规则放在一个独立的配置文件中我使用的配置格式是简单的文本表格每一行代表一个规则字段依次是优先级、动作、匹配字段、匹配模式、说明。这样设计的目的是方便在服务器上用 grep、vim 快速编辑不用引入复杂的 DSL 或数据库。实际配置内容大致是这样的# 优先级0 高1 中2 低 # 动作alert 记录并告警report 仅记录 0 alert uri ^/\.git 0 alert uri ^/\.env 0 alert uri \?(.*)(script|union|select|exec) 1 alert status 404 count_ip_more_than 50 1 report ua (sqlmap|nmap|nikto|masscan)这里要注意一点正则规则要写得尽可能窄。我当时就吃过亏一开始想“多匹配总比漏匹配好”写了几条宽泛正则结果把好几个正常的公司内网请求给误报了。后来总结经验正则宁可少匹配两成也不能误匹配一成。命中可疑规则之后还可以通过人工复核来弥补。4.3 核心分析脚本实现核心分析脚本的逻辑可以分成三步读取日志文件、逐行解析、按规则判定。下面是一个简化版的 Python 核心循环用于展示整体思路import re import json RULES [] def load_rules(rule_file): with open(rule_file) as f: for line in f: if line.startswith(#) or not line.strip(): continue parts line.strip().split() RULES.append({ priority: int(parts[0]), action: parts[1], field: parts[2], pattern: parts[3] }) def parse_log_line(line): # 简化版 access.log 解析 pattern r(\S) \S \S \[([^\]])\] ([A-Z]) (\S) ([^]) (\d{3}) (\d) ([^]*) ([^]*) m re.match(pattern, line) if not m: return None ip, timestamp, method, uri, protocol, status, size, referer, ua m.groups() return { ip: ip, time: timestamp, method: method, uri: uri, status: int(status), size: int(size), referer: referer, ua: ua } def check_rules(entry): result [] for rule in RULES: if rule[field] uri and re.search(rule[pattern], entry[uri], re.I): result.append(rule) elif rule[field] ua and re.search(rule[pattern], entry[ua], re.I): result.append(rule) elif rule[field] status and int(rule[pattern]) entry[status]: result.append(rule) return result这段代码不是我生产环境的完整版但核心思路是对的每条日志解析成结构化对象再用规则循环匹配。生产环境里我会做两处优化一是用编译过的正则对象缓存二是按规则分组高优先级规则先跑低优先级规则能跳过就跳过降低 CPU 开销。4.4 聚合统计与报告输出判定完单条日志之后还需要做聚合。因为人工看几百条可疑请求依然很费眼把同一 IP、同一行为聚合起来看效率会高很多。聚合的维度主要包括同一 IP 触发的可疑规则集合。同一 IP 在单位时间内的请求总数。命中某条规则的全部 URI 列表去重。异常状态码的分布。报告输出我用两种形式。第一种是人类可读的文本按天归档到 reports 目录下文件名格式是check_report_20240810.txt方便以后回溯。第二种是 JSON 格式方便后续接其他系统。比如我所在团队后来接了企业微信机器人就是通过这个 JSON 报告再往外推非常灵活。报告里还应该包含一个“无害原因”的备注字段。比如某些规则虽然命中了但同时命中多个可信标记比如 UA 是搜索引擎官方爬虫、Referer 来自本公司域名那么可以自动降低告警级别。这个机制能大大减少无效告警。4.5 定时调度与告警接入定时调度我直接用系统的 cron。推荐在凌晨和上午各跑一次因为这两天时段正好能覆盖前一夜的攻击高峰和上午的异常流量。示例 crontab30 2 * * * /usr/local/bin/nginx_log_check --config /etc/nginx_log_check.conf --report 0 9 * * * /usr/local/bin/nginx_log_check --config /etc/nginx_log_check.conf --alert告警这块nginx_log_check 本身不做太重的通信功能而是通过“外部命令钩子”的方式实现。配置里可以指定告警时执行的脚本路径比如调用 curl 往企业微信机器人的接口 POST 一段 JSON或者调用sendmail发一封邮件。这样做的好处是解耦不管你后面换成钉钉、短信还是自研监控平台都不用改工具本体。5. 常见问题与排错技巧任何工具都会碰到问题nginx_log_check 从小到大也迭代了好几轮。这里把我在实际使用中最常遇到的四类问题以及解决办法整理出来。5.1 日志格式不一致怎么办最头疼的问题就是日志格式不一致。比如用了 CDN 之后日志里的真实客户端 IP 变成了 CDN 节点 IP需要去加X-Forwarded-For头又比如某些业务单独配置了 JSON 格式日志字段和默认格式完全不一样。解决办法是在日志解析模块一开始不要写死正则。我先做一个自动探测读取日志的前 50 行判断格式类型是默认 combined 格式还是 JSON 格式还是带额外字段的自定义格式。探测结果缓存在临时文件中避免每次重复判断。如果碰到 CDN 场景我会在配置里加一个“真实 IP 提取开关”。开启之后解析器会优先从X-Forwarded-For字段里取最左侧的 IP因为那是用户的真实来源。这个开关视业务情况决定不要默认开启否则遇到某些客户端伪造头部时IP 统计会彻底失真。5.2 规则误报多怎么调误报是规则工具的常态不用慌。调规则不是一蹴而就的事我的流程是每天看一次告警报告把每一条告警里确认无害的请求记下来攒几天之后总结出共性的特征再针对这些特征添加白名单规则。比如我一开始有一条规则是“URI 中带 admin 就算可疑”结果内部系统有很多合法请求包含 /admin/ 路径误报特别多。后来我把规则改细/admin/后面如果再跟index.php或login才算可疑再配合 IP 白名单误报率立刻降下来了。还有一个重要技巧规则要和业务特点匹配。如果你的业务本身就是一个管理后台系统路径里带 admin 是常态那这条规则就该直接删掉。规则不是越严越好而是越贴合业务越好。5.3 大日志量下分析性能不足日志文件几 GB 的时候Python 逐行解析仍然偏慢。我在这里做了三层优化第一层先用 grep 排除掉明显正常的大流量静态资源请求比如图片、CSS、JS安全分析和这些静态请求关系不大过滤掉之后数据量能减小一半以上。第二层对剩下的日志用 awk 做快速字段切分把不可能触发规则的日志直接丢弃。第三层只有命中初筛的记录才交给 Python 做深规则匹配。这三层做完整个分析时间能压缩到原来的百分之二三十。有一个经验参数单文件超过 500 MB 时不要用 Python 的 readlines 一次性读取而是用for line in file逐行迭代这样内存占用会平稳很多。5.4 封禁 IP 怎么和防火墙联动日志分析本质上只能发现问题真正解决问题还需要防火墙联动操作。nginx_log_check 不直接执行封禁命令而是生成一个可疑 IP 清单文件再由外部脚本去读取并调用系统防火墙命令封禁。联动脚本的逻辑也很简单# 从报告 json 中提取嫌疑 ip jq -r .suspicious_ip_list[] report.json | while read ip; do iptables -A INPUT -s $ip -j DROP done不过封禁之前一定要确认几点这个 IP 是否出现在 CDN 回源列表里、是否属于内部监控系统、是否可能是某一栋办公楼里多个员工的出口 IP。如果存在这些可能就该走人工复核流程而不是脚本自动封禁。自动封禁做得太激进最终伤害的是正常用户。6. 迭代路上的几个体会如果让我把 nginx_log_check 的使用经验浓缩成几句话大概就是日志分析工具最厉害的不是规则多而是维护者的判断力和持续迭代的耐力。刚上线那会儿规则少、误报多我几乎每一天都要改配置甚至一度怀疑“日志分析是不是只是自娱自乐”。但坚持跑了一个多月之后报警和日志数量都稳定了误报几乎绝迹后来再出异常流量报告出来之后基本一眼就能定位是谁在扫、扫什么、要不要处理。另外一个小技巧是哪怕工具已经稳定运行也不要让它彻底“无人值守”。建议每隔半个月左右手动打开一次完整报告肉眼扫一遍——机器只能发现你提前写好的规则而安全分析里最致命的问题往往来自“从没见过的流量模式”。这种模式机器识别不了只有人能看到。nginx_log_check 解决的是“守住基本盘、解放人力”但最终的判断力始终在人这边。