为什么"事后看日志"往往已经太晚
个人站长的服务器被改了一行配置、被塞了一个后门 PHP 文件、/etc/passwd 里莫名多出一个 UID 0 的账号,这些事情在发生的那一刻,通常不会有任何告警。等你发现首页被跳转、Google 搜索结果出现奇怪的标题时,距离入侵已经过去了几天甚至几周。这时候再翻 /var/log/nginx/access.log,往往只能看到一堆已经被清理过的痕迹。
问题的根源在于:绝大多数站长只部署了"网络层"的防护(防火墙、fail2ban、Nginx 限流),却完全忽略了"文件层"的监控。网络层只能挡住"从外往里打"的行为,而攻击者一旦通过某个弱口令、某个过期的插件拿到了 shell,接下来做的事情全都是在文件系统里:改文件、加文件、改权限、改定时任务。这些动作在 Linux 上有一个专门的子系统可以完整记录——auditd。
这篇文章不讲泛泛的"安全加固清单",而是聚焦一个具体场景:用 auditd 监控几个关键文件,让任何对它们的改动都留下不可抵赖的记录。内容包含内核审计的基本原理、audit 规则的写法、日志怎么读、以及最容易被忽略的"规则在重启后失效"这个坑。
auditd 是什么,它和普通日志有什么本质区别
Linux 内核从 2.6 开始内置了 Audit 子系统。它工作在系统调用层:当进程执行 open、write、unlink、execve、chmod 这类系统调用时,如果命中了预先下发的审计规则,内核就会在动作发生的同一时刻生成一条审计记录。
这一点非常关键。普通应用日志是"程序自己想记什么就记什么",攻击者控制了程序就可以不记;而审计记录由内核产生,用户态程序无法阻止、无法伪造、也无法删除已经写出去的部分(除非拿到 root 后去改 audit 规则,但那本身也会留下一条规则变更记录)。
| 对比项 | 普通日志 (access.log / auth.log) | auditd 审计日志 |
|---|---|---|
| 产生位置 | 用户态进程 | 内核 Audit 子系统 |
| 记录粒度 | 应用语义(HTTP 请求、登录成功) | 系统调用(谁、在什么时间、对哪个文件、做了什么) |
| 能否被绕过 | 换一个入口就不记了 | 只要有权限检查就必然过审计点 |
| 典型用途 | 排障、流量分析 | 入侵取证、合规、敏感文件改动追责 |
换句话说,如果你想知道"除了我之外,还有谁动过我的网站目录",只有 auditd 能回答。Nginx 访问日志只能告诉你有人请求了某个 URL,它无法告诉你有人在本地 shell 里 echo '<?php eval($_POST[1]);?>' > shell.php。
安装与确认内核支持
Debian / Ubuntu 上安装:
apt update
apt install -y auditd audispd-pluginsCentOS / Rocky 上:
yum install -y audit
systemctl enable --now auditd装完先确认三件事,很多"规则不生效"的问题其实卡在这一步:
# 1. 内核是否带 audit 支持
grep AUDIT /boot/config-$(uname -r)
# 应看到 CONFIG_AUDIT=y
# 2. 服务状态
systemctl status auditd --no-pager
# 3. 能否与内核通信(关键!)
auditctl -sauditctl -s 的输出里如果 enabled 是 0,说明审计在内核里被关闭了,此时你写再多规则都不会产生日志。如果 enabled 是 2(immutable,不可变模式),说明规则被锁定,只能重启才能改。lost 字段表示因缓冲区满而丢弃的审计事件数,这个值如果持续增长,说明你的规则太宽泛、事件量太大,需要考虑收窄或调大 backlog_limit。
内核启动参数:先确认审计没被静默关闭
有些 VPS 镜像或安全加固脚本会默认加上 audit=0 或者直接不编译 audit,导致服务看着是 active,实际一条记录都没有。检查方式:
cat /proc/cmdline | tr ' ' '\n' | grep -i audit常见的有害参数是 audit=0(内核关闭审计)和 audit_backlog_limit=(过小)。要让它开机就启用,编辑 /etc/default/grub:
GRUB_CMDLINE_LINUX_DEFAULT="quiet audit=1 audit_backlog_limit=8192"然后 update-grub && reboot。注意:修改这个参数必须重启,不要在没重启的情况下反复调规则然后怀疑人生。笔者就曾因为在某个云厂商的救援模式下没带 audit=1,白白排查了两小时。
规则文件:/etc/audit/rules.d/ 才是正道
很多人习惯用命令行 auditctl -w ... 临时加规则,测试没问题就以为搞定了。这是最大的坑:auditctl 加的规则存在内存里,重启即失效。而且如果 /etc/audit/audit.rules 里没有对应条目,重启后还会被"清空重载"逻辑覆盖。
正确做法是把规则写进 /etc/audit/rules.d/ 目录下任意 .rules 文件:
# /etc/audit/rules.d/zz1984-website.rules
# 1. 监控关键配置文件
-w /etc/passwd -p wa -k identity_change
-w /etc/shadow -p wa -k identity_change
-w /etc/group -p wa -k identity_change
-w /etc/sudoers -p wa -k sudo_policy
-w /etc/sudoers.d/ -p wa -k sudo_policy
# 2. 监控 SSH 配置
-w /etc/ssh/sshd_config -p wa -k ssh_config
-w /etc/ssh/sshd_config.d/ -p wa -k ssh_config
# 3. 监控网站目录(重点!)
-w /www/wwwroot/ -p wa -k webroot_change
-w /etc/nginx/nginx.conf -p wa -k nginx_conf
-w /etc/nginx/conf.d/ -p wa -k nginx_conf
-w /etc/nginx/sites-enabled/ -p wa -k nginx_conf
# 4. 监控定时任务(这是最常被忽略的持久化入口)
-w /etc/crontab -p wa -k cron_change
-w /etc/cron.d/ -p wa -k cron_change
-w /var/spool/cron/ -p wa -k cron_change
-w /etc/cron.daily/ -p wa -k cron_change
# 5. 监控授权公钥(攻击者最爱加 authorized_keys)
-w /root/.ssh/ -p wa -k ssh_key
-w /home/ -p wa -k ssh_key
# 6. 监控内核模块加载,防 rootkit
-w /sbin/insmod -p x -k modules
-w /sbin/modprobe -p x -k modules
-a always,exit -F arch=b64 -S init_module -S delete_module -k modules
# 7. 监控执行文件被删除(攻击者清理工具的常见动作)
-a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F auid>=1000 -F auid!=-1 -k delete
# 8. 让规则不可被随意删除(放在最后)
-e 2逐条解释关键点:
-w 路径 -p wa -k 标签:-w表示 watch 一个路径,-p指定监听的操作类型。权限字母含义:r=读、w=写、x=执行、a=属性变更(chmod/chown/setxattr)。wa是写 + 属性变更,这是监控文件被篡改最常用的组合。如果写成-p r,你的日志会被"每次读取"刷爆。-k 标签:后面的 key,用于在日志里过滤。不起名字的话,几万条日志里你根本找不到要的那条。-F auid>=1000 -F auid!=-1:只记录"真实登录用户"发起的操作,过滤掉系统服务和内核线程产生的噪音。auid是登录时分配的审计 ID,即使这个用户后面su成了 root,auid依然保留原始登录用户,这正是审计能追责的根本原因。-e 2:把审计设为不可变模式。设置之后,任何进程(包括 root)都无法再删除或修改规则,想改只能重启。这是防止攻击者"打完就关审计"的关键一招。注意它必须放在规则文件最后一行。
路径加不加斜杠,结果完全不同
这是 audit 规则里最隐蔽的一个语义陷阱:
-w /www/wwwroot(不加尾部斜杠):只监控/www/wwwroot这个路径本身。如果它是目录,那么只记录对这个目录节点属性(比如目录被 chmod、被重命名、被删除)的操作,目录里文件内容的修改不会被记录。-w /www/wwwroot/(加尾部斜杠):监控这个目录及其所有子项。但要注意,它仍然是"树形递归"的:新创建的文件也会自动纳入监控。
所以监控目录必须加斜杠。这个细节我在第一次部署时踩过——规则写了 -w /www/wwwroot,监控了两周一条记录没有,还以为是服务的问题,实际上规则完全没写错,只是漏了个 /。
加载规则并验证
# 加载规则(-R 读取文件方式,会校验语法)
augenrules --load
# 查看当前内核里的规则
auditctl -l
# 确认 immutability 状态
auditctl -s | grep enabled验证规则真的生效,最简单的办法是自己制造一次改动:
touch /www/wwwroot/test_audit_probe.php
rm -f /www/wwwroot/test_audit_probe.php
# 立刻查看审计日志
ausearch -k webroot_change --start recent如果能看到 NAME、SYSCALL、PATH 三段记录,说明链路是通的。如果 ausearch 提示 "no matches",按下面顺序排查:
auditctl -s的enabled是否为 0 → 内核未启用审计,检查 grub 参数。auditctl -l里那条规则是否还在 → 被augenrules --load覆盖或没写进 rules.d。- 路径斜杠、权限字母是否正确。
/var/log/audit/audit.log是否因磁盘满被轮转丢弃(见下一节)。
读懂一条审计记录
auditd 的日志格式对新手很不友好,一条文件写操作通常会拆成好几行,用同一个 msg=audit(时间戳:序号) 关联。看一个真实例子:
type=SYSCALL msg=audit(1758800000.123:4567): arch=c000003e syscall=257
success=yes exit=3 a0=ffffff9c a1=7ffe1234 a2=241
ppid=1234 pid=5678 auid=1000 uid=0 gid=0
euid=0 comm="bash" exe="/usr/bin/bash" key="webroot_change"
type=PATH msg=audit(1758800000.123:4567): item=1 name="/www/wwwroot/shell.php"
inode=789012 dev=fd:00 mode=0100644 ouid=0 ogid=0
type=CWD msg=audit(1758800000.123:4567): cwd="/root"重点字段含义:
auid=1000:真实登录用户。如果是 4294967295(即 -1),说明操作不是登录用户发起的,可能来自系统服务或内核。这个字段是取证的核心——它告诉你"是谁在登录状态下干的"。uid/euid:当前有效用户。如果 auid=1000 而 uid=0,说明这个普通用户sudo或su成了 root 再操作的——依然能追到源头,这正是 auid 的价值。comm/exe:发起操作的程序名和绝对路径。判断是 bash 手工操作还是某个脚本/webshell 进程。name:被操作的文件的完整路径。key:你定义的标签,方便检索。cwd:当时的当前工作目录,能还原操作现场。
日常排查常用命令
# 按 key 查最近改动
ausearch -k webroot_change --start recent
# 按时间范围
ausearch -k identity_change --start 2026-09-20 --end 2026-09-26
# 按可执行文件
ausearch -x /usr/bin/curl
# 按登录用户(追责)
ausearch -ua 1000
# 把 raw 日志翻译成人话(最推荐)
aureport --summary
aureport -k # 按 key 统计事件数
aureport -au # 认证事件报表
aureport -f # 文件操作报表
# 单条事件完整展开
ausearch -a 4567 --interpretaureport -k 特别有用——它会告诉你每个标签下积累了多少事件。如果你的 webroot_change 一天只有几条,那很健康;如果突然变成几千条,说明有程序在疯狂写网站目录,值得立刻查。
日志容量与轮转:审计日志能吃满磁盘
审计日志默认在 /var/log/audit/audit.log。这个文件增长很快,尤其是监控了网站目录之后,每个文件写入都会产生多条记录。它真的会把磁盘写满,而且是静默地写满。
三个必须调的参数,写在 /etc/audit/auditd.conf:
max_log_file = 200
num_logs = 10
max_log_file_action = ROTATE
space_left = 500
space_left_action = SYSLOG
disk_full_action = SUSPEND
admin_space_left = 100
admin_space_left_action = SINGLEmax_log_file:单个日志文件上限(MB)。200表示 200MB 就轮转。num_logs:保留多少个历史文件。10 × 200MB = 2GB 上限,按你的数据盘大小调整。space_left_action = SYSLOG:磁盘剩余到阈值时发系统日志告警,而不是直接关机。disk_full_action = SUSPEND表示磁盘满时暂停记录但不停机——这比HALT安全得多,否则审计日志满会导致服务器直接宕掉。
建议再配一条 logrotate 或者直接依赖 auditd 自身的轮转,并把这个日志目录纳入你的磁盘监控。
性能影响与规则瘦身
很多文章渲染 auditd"会拖慢系统",实际上:监控几十个具体路径的 -w 规则,CPU 开销几乎可以忽略。真正拖垮性能的是两类写法:
- 递归监控大目录。比如
-w /var/或者-w /www/监控整个网站根目录(含 node_modules、缓存目录、日志目录)——每次文件读写都过审计点,IO 就上来了。正确姿势是只监控"代码入口"和"配置",把runtime/、cache/、uploads/这类高频写入目录排除掉。 - 用
-p r或-p rwa。读操作的数量是写的几十倍,监控读会让日志和 CPU 双双爆炸。
排除目录的写法(注意排除规则要放在通配规则之后):
-w /www/wwwroot/ -p wa -k webroot_change
# 把高频缓存目录从监控中剔除
-a always,exit -F dir=/www/wwwroot/site/runtime -F perm=wa -F key=webroot_change_excluded更彻底的办法是用 -F path= 精确匹配而不是 -w 递归,但可读性会差一些。
把它接入你的告警链路
规则写好了,日志在记了,但如果没人看,等于没部署。最轻量的做法是写一个每天跑一次的巡检脚本,统计各 key 的事件数并邮件/推送到你的告警通道:
#!/bin/bash
# /usr/local/bin/audit_daily.sh
REPORT=$(aureport -k --summary 2>/dev/null)
echo "=== 审计事件日报 $(date +%F) ===" > /tmp/audit_report.txt
echo "$REPORT" >> /tmp/audit_report.txt
# 异常判定:webroot 改动超过阈值即告警
COUNT=$(aureport -k 2>/dev/null | grep webroot_change | awk '{print $2}')
if [ -n "$COUNT" ] && [ "$COUNT" -gt 50 ]; then
echo "警告:网站目录今日发生 $COUNT 次改动,请人工核查" >> /tmp/audit_report.txt
# 这里接你的告警通道:curl 到 webhook、mail、企业微信机器人等
fi
# 记录当日关键事件明细备查
ausearch -k webroot_change --start today --interpret >> /tmp/audit_report.txt配合 crontab 每天 8 点执行即可。这样你就从"事后翻日志"变成了"次日早上收到一份改动清单"——绝大多数恶意改动会在第一时间被发现。
常见坑位速查表
| 现象 | 根因 | 解决 |
|---|---|---|
| 规则加了但没日志 | 内核 audit=0,或 auditctl -s 显示 enabled=0 | 检查 /proc/cmdline,加 grub 参数并重启 |
| 重启后规则全没了 | 只用了 auditctl -w,没写进 rules.d | 写入 /etc/audit/rules.d/*.rules,augenrules --load |
| 目录改动不记录 | 路径没加尾部斜杠 | -w /path/to/dir/ |
| 日志爆炸、CPU 升高 | 用了 -p r 或监控了整个大目录 | 改成 -p wa,排除 runtime/cache 目录 |
| 想改规则改不了 | 已设 -e 2 不可变模式 | 只能重启后修改 rules.d |
| 日志文件丢了/被覆盖 | 磁盘满或 num_logs 太小 | 调大 max_log_file / num_logs,配磁盘监控 |
| ausearch 查不到 | key 名字写错,或时间范围不对 | aureport -k 先看有哪些 key |
小结:把"不可抵赖"变成日常
auditd 的价值不在于它能挡住攻击,而在于它能让攻击留下痕迹。对一个没有专职安全团队的草根站长来说,"事后能查清发生了什么"往往比"事前防住一切"更现实——你不可能防住所有 0day,但你可以保证任何人动了你的服务器,你第二天早上就会知道,并且知道是谁在什么时间、用什么程序动的。
建议的最小落地清单:监控 passwd/shadow/group/sudoers、sshd_config、网站根目录、crontab 相关路径、.ssh 目录,共六组规则;配置文件写进 rules.d;-e 2 锁死;每日巡检脚本接告警。全部加起来不到 20 行规则,却能补上个人站长安全体系里最容易被忽略、也最致命的那一环。