日志驱动的安全运营:从 nginx access.log 到自建访问审计与攻击意图识别
《从零加固一台 Linux 服务器》解决的是「门锁好」——SSH 密钥化、防火墙、fail2ban、内核参数,把可以被直接利用的面压到最小。但门锁好了之后还有一个问题没人回答:昨天有谁来过门口、试了哪几把钥匙、失败了几次?这就是日志与审计要解决的事。没有审计,你只能在被入侵之后做事后取证;有了审计,你才有机会在「试探阶段」就把对手拦下来。
/.env、/wp-admin、/phpmyadmin,用你的域名做代理攻击的跳板,或者单纯刷流量。更要紧的是,access.log 是唯一能回答「攻击从哪来、打的是什么、有没有成功」的数据源。它几乎是零成本获得的攻击情报。一、先分清日志的四个层次
| 层次 | 典型文件 | 能回答的问题 |
|---|---|---|
| Web 访问日志 | /var/log/nginx/access.log | 谁访问了什么、结果如何、多慢 |
| Web 错误日志 | /var/log/nginx/error.log | 上游失败、权限问题、配置告警 |
| 系统认证日志 | /var/log/auth.log、journalctl | SSH 爆破、sudo 提权、账号异常 |
| 应用日志 | 由业务自行落盘 | 业务级异常、慢查询、内部错误 |
四层是互补关系,不能只看一层。举一个非常典型的组合场景:扫描器先爆破 /login(在看 access.log 里的 401 洪峰),拿到 shell 之后建立 SSH 反向连接(这时要去 auth.log 里找异常登录来源)。只盯 Web 层,会漏掉后面半段。
二、access.log:先把格式改成「可分析」的
Nginx 默认的 combined 格式是这样的:
127.0.0.1 - - [14/Sep/2026:09:12:03 +0800] "GET /tools.html HTTP/1.1" 200 31945 "-" "Mozilla/5.0 ..."
七个字段依次是:远端地址、远端用户、认证用户、时间、请求行、状态码、响应字节、Referer、User-Agent。它能用,但有两个致命缺陷:一是默认格式里没有耗时,二是本机前面还有代理时 $remote_addr 拿到的是代理 IP。
做审计的第一件事,就是把格式改成结构化 JSON——解析不用正则、不怕字段里出现空格和引号:
log_format audit escape=json '{'
'"ts":"$time_iso8601",'
'"ip":"$remote_addr",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"bytes":$body_bytes_sent,'
'"rt":$request_time,'
'"urt":"$upstream_response_time",'
'"ref":"$http_referer",'
'"ua":"$http_user_agent",'
'"host":"$host",'
'"xff":"$http_x_forwarded_for"'
'}';
access_log /var/log/nginx/audit.access.log audit;
request_time 是总耗时(从收到请求首字节到发完响应),upstream_response_time 是后端耗时;两者相减就是 Nginx 自身与网络的开销——这是区分「应用慢」还是「网关慢」的唯一依据。② escape=json 会自动转义引号与反斜杠,不加它,一个带引号的 UA 就能把你的日志解析全打乱,甚至被用来做日志注入伪造记录。③ 代理场景下要记 $http_x_forwarded_for,配合前面讲过的 real_ip 才能拿到真实来源。日志轮转:别让审计日志吃掉磁盘
/var/log/nginx/*.log {
daily
rotate 30 # 保留 30 天,兼顾排查与合规
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
endscript
}
kill -USR1 是 Nginx 约定的「重新打开日志文件」信号——轮转后必须发这个信号,否则 Nginx 会一直往已被改名/删除的旧文件句柄里写,新日志始终是空的。这是运维里最经典的「日志怎么不更新了」的根因。
三、从原始日志到四个必看指标
审计不是「把日志存起来」,而是把日志压成能一眼判断局势的数字。四个维度足够覆盖 90% 的日常判断:
| 维度 | 看什么 | 异常信号 |
|---|---|---|
| 流量概览 | PV / UV、按小时分布 | 某小时突增数倍;深夜出现高峰 |
| 来源排行 | TOP IP、TOP 国家/地区 | 单一 IP 占比过高(>30%) |
| 路径排行 | TOP URI | 出现 /.env、/wp-login.php 等不存在也不该被访问的路径 |
| 结果与耗时 | 状态码分布、P50/P95/P99 耗时 | 404/403 占比骤升;P95 明显抬升 |
用命令行就能快速聚合,不需要等平台搭好:
# TOP 10 来源 IP(audit 日志取第 2 个字段)
awk -F'"' '{print $4}' /var/log/nginx/audit.access.log | sort | uniq -c | sort -rn | head -10
# TOP 10 被访问路径
awk -F'"' '{print $6}' /var/log/nginx/audit.access.log | sort | uniq -c | sort -rn | head -10
# 状态码分布
awk -F'"' '{print $8}' /var/log/nginx/audit.access.log | awk '{print $1}' \
| sort | uniq -c | sort -rn
# 404 最多的 IP(扫描器的典型画像)
grep '" 404 ' /var/log/nginx/audit.access.log \
| awk -F'"' '{print $4}' | sort | uniq -c | sort -rn | head -10
# 最慢的 20 个请求
sort -t'"' -k10 -k10 -rn /var/log/nginx/audit.access.log | head -20
四、攻击意图识别:把特征写成规则
攻击是有「形状」的。下面是几类最常见的行为特征,以及能直接落到匹配规则里的判据:
1. 目录扫描 / 漏洞探测
- 特征:同一 IP 在短时间内产生大量 404;请求路径集中在不存在也不该存在的文件上。
- 高频探测路径字典:
/.env、/.git/config、/wp-admin、/wp-login.php、/phpmyadmin、/adminer.php、/backup.zip、/config.php.bak、/.ssh/id_rsa、/actuator/env、/druid/index.html。 - 判据:单 IP 5 分钟内 404 次数 > 50;或命中敏感路径字典任意一条即记录。
2. 注入与穿越尝试
| 类型 | URL 中的典型片段 |
|---|---|
| 路径穿越 | ../、%2e%2e%2f、..%5c、/etc/passwd |
| SQL 注入 | ' or 1=1、union select、sleep(、information_schema |
| 命令注入 | ;cat 、|whoami、$(、`id` |
| XSS 试探 | <script>、onerror=、javascript: |
| 模板/表达式注入 | ${、{{、#{ |
这类请求只要出现一次就值得记一条——正常用户不会在 URL 里写 union select。注意匹配时要先做 URL 解码,否则 %2e%2e%2f 这类编码写法会绕过规则。
3. 认证爆破
- 特征:同一 IP 对
/login、/api/login等路径短时间内大量 POST,状态码集中在 401/403,且 UA 恒定、无 Referer。 - 判据:单 IP 1 分钟内 401 次数 > 10;或同一 IP 尝试不同用户名(这需要应用侧日志配合)。
顺带指出一个常被忽略的真实风险:后台管理入口暴露在公网本身就是最大的攻击面。低于 30 秒的加固手段是——把管理端口只监听内网,或直接在前面加一层 allow / deny:
# 管理后台只允许指定来源访问
location ^~ /admin/ {
allow 203.0.113.10; # 你的固定出口 IP
deny all;
proxy_pass http://127.0.0.1:8080;
}
4. CC 与资源消耗型攻击
特征与扫描器刚好相反:路径是正常的(甚至是首页或搜索接口)、状态码是 200,但频率异常且响应字节小。识别要点:
- 单 IP 请求速率远超人类操作(人点击间隔以秒计,脚本以毫秒计)。
- UA 异常集中:大量请求共享同一个 UA,或使用已知爬虫/工具的默认 UA。
- 打的是贵接口:搜索、导出、生成类接口被反复调用——这类请求单价高,几十 QPS 就能把后端压满。
五、把识别结果接进处置闭环
只记录不处置等于没做。最轻量的自动化处置是 fail2ban——它的模型非常清晰:filter(正则识别) + jail(阈值与动作) + action(封禁方式)。
# /etc/fail2ban/filter.d/nginx-scan.conf
[Definition]
failregex = ^.*"ip":"<HOST>".*"status":40[34].*$
ignoreregex =
# /etc/fail2ban/jail.d/nginx-scan.conf
[nginx-scan]
enabled = true
port = http,https
filter = nginx-scan
logpath = /var/log/nginx/audit.access.log
maxretry = 60
findtime = 300 # 5 分钟窗口
bantime = 3600 # 封 1 小时
action = iptables-multiport[name=scan, port="http,https", protocol=tcp]
三个实践要点:
- 阈值宁宽勿严。误封真实用户的代价远高于多放几个扫描请求。上线前先只开
action = %(action_)s之外的「只记录」模式跑几天,观察会命中哪些 IP,再决定阈值。 - 一定要配
ignoreip,把本机、内网网段、你的固定出口 IP 排除,否则最容易的结果是把自己锁在门外。 - 封禁要有期限与留痕。
bantime不要设成永久;用fail2ban-client status nginx-scan和日志保留每一次封禁记录,便于回溯误判。
六、自建审计看板:一条轻量数据管线
ELK、Loki 这类方案功能完整,但对一台 2C4G 的个人服务器来说,光 Elasticsearch 的常驻内存就够呛。单机场景更合适的是「流式解析 + 关系库聚合」的思路,链路只有四段:
| 阶段 | 做什么 | 单机实现 |
|---|---|---|
| 采集 | 跟踪日志文件新增行(要处理轮转) | 定时 tail / 文件偏移量记录,或 filebeat |
| 解析 | JSON 直接反序列化,提取字段 | 一行 JSON 解析即可,无需正则 |
| 存储 | 明细入库 + 预聚合 | SQLite 起步(单写入者、零运维),量大再换 PostgreSQL |
| 聚合与展示 | 按小时/天滚动汇总,生成榜单 | 定时任务写入汇总表,Web 层只读汇总表 |
表结构可以非常朴素,两张表就够:
-- 明细表:按天分区/分表,便于批量删除过期数据
CREATE TABLE access_log (
id INTEGER PRIMARY KEY,
ts TEXT NOT NULL, -- ISO8601
ip TEXT NOT NULL,
method TEXT,
uri TEXT,
status INTEGER,
bytes INTEGER,
rt REAL, -- 总耗时
ua TEXT,
ref TEXT,
category TEXT -- normal / scan / inject / brute / cc
);
CREATE INDEX idx_ts ON access_log(ts);
CREATE INDEX idx_ip ON access_log(ip);
-- 汇总表:定时任务产出,前端只读它,避免每次查询都扫明细
CREATE TABLE access_daily (
day TEXT,
ip TEXT,
hits INTEGER,
bad_hits INTEGER, -- 4xx/5xx
categories TEXT, -- 该 IP 命中的攻击类型
PRIMARY KEY (day, ip)
);
几个决定成败的工程细节:
- 记录文件偏移量。解析进程要把「读到第几字节」持久化,重启后继续读,而不是从头再灌一遍。
- SQLite 用 WAL 模式(
PRAGMA journal_mode=WAL),让写入与查询不互相阻塞。 - 批量事务写入。逐行提交在日志量大时会把磁盘 IO 打满,按 500~1000 行一批提交。
- 过期数据定期清理(保留 30~90 天),并用
VACUUM或分区删除控制体积。 - 看板接口必须鉴权。审计数据里全是 IP 与 UA,本身就是敏感信息;一旦看板裸奔,等于把攻击日志公开——这本身就是一次信息泄露。
七、一页纸的落地顺序
| 阶段 | 动作 | 验收标准 |
|---|---|---|
| ① 可用 | 改 JSON log_format、配 logrotate + USR1 | 日志能结构化解析、轮转后仍在写入 |
| ② 能看 | 跑聚合脚本,出 TOP IP / TOP 路径 / 状态码分布 | 能一眼判断今天是否异常 |
| ③ 能判 | 加敏感路径字典与注入特征匹配,打上 category | 攻击请求被正确分类 |
| ④ 能处置 | fail2ban 联动,先只记录后开启封禁 | 误封为零,扫描源被自动拦截 |
| ⑤ 能回看 | 看板 + 汇总表 + 鉴权 + 保留策略 | 可查询、可回溯、数据有边界 |