首页 / 博客 / 本文

日志驱动的安全运营:从 nginx access.log 到自建访问审计与攻击意图识别

🛡️ 安全架构📅 2026-09-14⏱ 18 分钟

《从零加固一台 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、journalctlSSH 爆破、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
别只看 PV。「今天 PV 涨了 3 倍」既可能是被推荐了,也可能是被 CC 了。区分方法很简单:看 UV 与 PV 的比值。真实增长通常 UV 同步上升;攻击则表现为 PV 暴涨而 UV 几乎不动(一台机器刷了上千次),并且往往伴随 UA 重复、Referer 为空、响应字节极小。

四、攻击意图识别:把特征写成规则

攻击是有「形状」的。下面是几类最常见的行为特征,以及能直接落到匹配规则里的判据:

1. 目录扫描 / 漏洞探测

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. 认证爆破

顺带指出一个常被忽略的真实风险:后台管理入口暴露在公网本身就是最大的攻击面。低于 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 访问了 1000 次」这句话本身没有意义,除非你知道昨天同一时段所有 IP 的中位数是 8 次。工程上的做法:按小时统计各维度的分位数(P50/P95/P99)作为基线,再用「超过 P99 的 N 倍」作为告警条件。这样阈值会随业务自然增长自动调整,不需要人工反复改配置。

五、把识别结果接进处置闭环

只记录不处置等于没做。最轻量的自动化处置是 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]

三个实践要点:

六、自建审计看板:一条轻量数据管线

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)
);

几个决定成败的工程细节:

关于隐私与合规的一条底线。源 IP、User-Agent 组合起来属于可识别到个人的信息。自建审计要做到三件事:目的限定(只用于安全与运营分析,不挪作他用)、保留期限(到期删除,不要无限期囤积)、访问最小化(看板不公开、导出留痕)。个人站虽小,习惯要正——「先囤着以后可能有用」是数据泄露最常见的起点。

七、一页纸的落地顺序

阶段动作验收标准
① 可用改 JSON log_format、配 logrotate + USR1日志能结构化解析、轮转后仍在写入
② 能看跑聚合脚本,出 TOP IP / TOP 路径 / 状态码分布能一眼判断今天是否异常
③ 能判加敏感路径字典与注入特征匹配,打上 category攻击请求被正确分类
④ 能处置fail2ban 联动,先只记录后开启封禁误封为零,扫描源被自动拦截
⑤ 能回看看板 + 汇总表 + 鉴权 + 保留策略可查询、可回溯、数据有边界
总结。安全运营的本质是把「不可见的访问」变成「可解释的数据」:先让日志结构化(否则一切分析都是正则苦工),再把数据压成四个维度的指标(流量、来源、路径、结果),然后用基线和特征把访问分成「正常 / 可疑」两类,最后把分类结果接回处置动作。这条路不需要昂贵的平台,一台普通服务器加上两张表就能跑起来——关键在于你愿不愿意每天花两分钟看一眼那几个数字。
#安全运营#日志审计#Nginx#fail2ban#运维
广告