AIDE文件完整性监控:基线与降噪实操

文件被改了,你为什么不知道

站点被入侵之后,最让人后怕的不是「被黑了」,而是「被黑了三个月才发现」。绝大多数人靠什么发现?靠搜索引擎给出「此网站可能已被篡改」的提示,或者靠用户投诉。这两个渠道都太晚了。

中间这段时间里,攻击者可以做什么?往 index.php 里插一段跳转代码,往 wp-content 里丢一个改了名的 webshell,往 .htaccess 里加一条重写规则把所有移动端用户导到别的域名。这些动作的共同点是:文件系统里的字节变了,但没有一条日志会告诉你。

AIDE(Advanced Intrusion Detection Environment)解决的就是这件事。它给每个文件算一个校验和,加上权限、属主、大小、修改时间、inode 一起存成基线数据库,之后定期重新算一遍,和基线对比,不一致就报警。这篇文章讲的是在个人站的服务器上真正把它用起来,而不是装完跑一次 aide --init 就完事。

第一步:先划清楚要监控什么

AIDE 最容易失败的地方不是配置语法,而是范围。全盘监控一台跑了一年多的服务器,第一次初始化就要跑十几分钟、数据库几百兆,之后每天扫描都跑不完一次,于是有人把定时任务关掉,监控就死了。反过来只监控网站目录,那攻击者改 /etc/crontab 或者往 /usr/local/bin 里放个后门,你完全看不见。

我的建议是把文件分成四层,每层用不同的监控强度:

  • 核心层(必须监控,每次扫描):网站代码目录、/etc 下的配置、/root/.ssh、crontab 相关目录、计划任务目录、开机自启目录。这些地方任何变化都值得看一眼。
  • 系统层(监控,但允许大量变化):/bin、/sbin、/usr/bin、/usr/sbin。库文件包升级会大量改动,要配过滤规则,否则每次 apt 升级就是几千条告警。
  • 数据层(只监控权限和属主):上传目录、缓存目录、日志目录。内容天天变,校验和没有意义,但「权限从 644 变成 777」或「属主从 www 变成 root」是强烈的异常信号。
  • 排除层(完全不看):/proc、/sys、/dev、/run、/tmp、各种缓存目录、数据库文件、日志文件正文。
# /etc/aide/aide.conf.d/zz-site.conf  —— 自定义规则组
SITE_CORE = p+i+n+u+g+s+m+acl+xattrs+sha256
SITE_PERM = p+i+n+u+g+acl+xattrs

# 核心层
/srv/www/                    SITE_CORE
/etc/                        SITE_CORE
/root/.ssh/                  SITE_CORE
/var/spool/cron/             SITE_CORE
/etc/cron.d/                 SITE_CORE
/etc/systemd/system/         SITE_CORE

# 数据层:只看权限与属主
/srv/www/uploads/            SITE_PERM
/var/log/                    SITE_PERM

# 排除层
!/srv/www/cache/
!/srv/www/wp-content/cache/
!/var/log/.*\.(log|gz|[0-9])$
!/proc
!/sys
!/run
!/tmp

规则字母的含义要弄明白,不然改错了也不知道错在哪:p 权限、i inode、n 硬链接数、u 属主、g 属组、s 大小、m 修改时间、acl 访问控制列表、xattrs 扩展属性、sha256 内容哈希。

这里有一个必须知道的坑:m(修改时间)和 s(大小)在 OpenVZ 或 LXC 这类容器里经常不可靠,因为容器文件系统的时间戳行为和外网不一致,会出现「每天都有几百个文件报告时间变化但内容没变」的噪声。如果你的服务器是 VPS 的容器方案,先把 m 去掉,只靠 sha256 判内容,噪声会少一大半。

第二步:初始化基线,别在有怀疑的时候做

基线是「你相信系统是干净的」那一刻的快照。如果你在站点已经被入侵的情况下初始化,那攻击者的 webshell 就成了基线的一部分,以后永远查不出来。

所以正确顺序是:先做一次确认,确认系统干净(或者干脆重装),然后再初始化。确认的手段包括:查最近修改的文件、查 webshell 特征、看进程和监听端口、看 cron 里有没有可疑条目。这些做完之后再跑:

aide --init
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db

注意第二步必须手动做。aide --init 只生成 aide.db.new,不会自动替换旧基线。这个设计的意图是让你先检查新基线里的内容对不对,再决定是否启用。很多人第一次装完没 mv,结果 aide 一直报「数据库不存在」,还以为是配置问题。

初始化完把 aide.db 备份到服务器之外(比如本地电脑或对象存储)。这一步非常关键:如果服务器被入侵,攻击者很可能顺手把基线数据库也改掉,让你的监控永远报「无变化」。基线文件在服务器之外有一份副本,你才能恢复出可信的对比基准。

第三步:每天跑一次,但别在业务高峰跑

AIDE 扫描是纯 IO 密集操作,一次全盘扫描在普通 VPS 上要几分钟到十几分钟。放在业务高峰跑,会明显拖慢网站响应。用 systemd timer 安排在凌晨,并且加两个约束:一个是 Nice 和 IOSchedulingClass 让它在 IO 队列里让路,另一个是设一个执行时限,防止它卡死在某个大目录上。

# /etc/systemd/system/aide-check.service
[Unit]
Description=AIDE daily check
After=network.target

[Service]
Type=oneshot
Nice=19
IOSchedulingClass=idle
IOReadBandwidthMax=/ 20M
ExecStart=/usr/bin/aide --check
SuccessExitStatus=0

# /etc/systemd/system/aide-check.timer
[Unit]
Description=Daily AIDE check

[Timer]
OnCalendar=*-*-* 04:17:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target

RandomizedDelaySec 在多台机器上很有用,避免同一时刻一起扫描、把存储打满。Persistent=true 保证机器在 4 点关机、5 点开机时,起来会补跑一次,而不是跳过一整天。

这里有个细节:aide --check 在有变化时返回非零(2 或以上),systemd 会把它记成 failed。这在 systemd 的 systemctl --failed 里会一直亮着红灯,看起来像服务坏了。要么在 service 里写 SuccessExitStatus=0 2,要么接受这个红灯,把它当作「有变化」的信号。我个人倾向后者——红灯本身就是个视觉提醒。

第四步:让告警真的能被看到

定时任务跑起来但没人看报告,等于没做。AIDE 的默认行为是把报告写到标准输出,systemd 会把它收进 journal,也就是 journalctl -u aide-check 能看到,但没人会每天去看。

最省事的做法是写一个包装脚本:把输出存成带日期的文件,grep 一下关键段落,有变化就发一封邮件或者推一条消息。这里不依赖复杂的监控系统,一段 shell 就够:

#!/bin/bash
set -u
STAMP=$(date +%Y%m%d)
OUT=/var/log/aide/report-$STAMP.txt
mkdir -p /var/log/aide

/aide --check > "$OUT" 2>&1
rc=$?

# 只看「新增文件」「删除文件」「内容变化」三类
CHANGES=$(grep -E '^(Added|Removed|Changed):|^File: ' "$OUT" | wc -l)

if [ "$rc" -ne 0 ] && [ "$CHANGES" -gt 0 ]; then
    SUBJECT="[AIDE] $STAMP 检测到 $CHANGES 处变化"
    # 只截取报告前半部分,避免邮件过大
    sed -n '1,200p' "$OUT" | mail -s "$SUBJECT" ops@example.com
fi

# 只保留最近 30 天报告
find /var/log/aide -name 'report-*.txt' -mtime +30 -delete

关键在于报告的解读。AIDE 报告里最常见的三类行是 Added、Removed、Changed。正常运维会产生大量 Changed:系统升级改了 /usr/bin 下的几百个文件,你更新主题改了几十个 CSS,日志轮转删了一堆旧日志。这些都不是入侵。

真正需要警觉的组合是这几种:

  • 网站目录里新增了 .php 文件,尤其是在 uploads、cache、images 这种本来不该有可执行文件的目录里。这是 webshell 的经典落点。
  • 核心配置文件被改:/etc/passwd、/etc/sudoers、/root/.ssh/authorized_keys、/etc/crontab。这类文件一年可能都不变一次,变了就是大事。
  • 可执行文件被改但大小没变:这是二进制后门替换的典型手法,用相同大小的载荷覆盖原文件,只改内容不改元数据。所以规则里一定要带哈希,光看 s 和 m 会漏。
  • 属主从普通用户变成 root,或者权限变成 setuid:这是提权铺垫,尤其在 /tmp 和上传目录里。

降噪:让报告里的「噪声」变成「背景」

AIDE 用得下去的关键在于降噪。如果每天收到三十条「Changed」,一周之后你就会把邮件规则改成一概忽略,监控就废了。降噪有两个层次:

静态降噪是把明显会变的东西排除掉。缓存、会话、日志正文、数据库文件、临时目录,这些不进基线。系统包的校验和则可以换个思路:不要排除 /usr/bin,而是把 apt 的升级动作和 AIDE 扫描错开,并且在升级后主动重新初始化一次基线。这样每次基线更新都在你的控制之下,「基线变化」和「异常变化」就分开了。

动态降噪是给报告分类。包装脚本里可以按路径前缀把变化分成三组:/srv/www 下的归「网站」、/etc 和 /root 下的归「系统」、其余归「其他」。日常只看前两组,第三组降级成周报。

还有一个很实用的技巧:给基线维护一个「白名单文件」,记录你已知的、预期内的变化(比如「升级 PHP 8.2 会改动 /etc/php/8.2」),脚本里做差集。这比在 aide.conf 里一条条写排除规则要灵活得多,因为排除规则是永久的,而预期变化是一次性的。

和 auditd 怎么分工

这里要说清楚一件事:AIDE 和 auditd 不是二选一,它们看的是两个不同的问题。

AIDE 看的是结果:文件现在长这样,和上次比不一样了。它不知道是谁改的、什么时候改的、用什么进程改的。它的优势是覆盖面广、离线可查、不需要内核支持。

auditd 看的是过程:谁在几点几分,以什么身份,用哪个程序,对哪个文件发起了写操作。它的优势是有完整的因果链,可以直接拿去取证;劣势是规则要一条条写、写多了性能会掉、日志量大。

实用的分工是:AIDE 做日常巡检和发现,auditd 只对最关键的少数文件做写入监控(/etc/passwd、/etc/shadow、网站根目录的 .php 文件)。AIDE 报告说 /srv/www/index.php 变了,你就用 ausearch -f /srv/www/index.php 查出是哪个进程改的——这是完整闭环的排查路径。只上 AIDE 不上 auditd,你能知道「被改了」但查不出「怎么被改的」;只上 auditd 不上 AIDE,你只能看到被监控的那几个文件的写入,漏掉真正的入口。

基线更新:别让它成为永久噪声

AIDE 的日常维护比初始化更重要。每次计划内的变更(系统升级、主题更新、新装插件)之后,都应该更新基线,否则同一个变化会每天报一次,直到你麻木。

# 更新流程:改完 → 扫描 → 人工确认 → 替换基线 → 异地备份
aide --update
diff <(cat /var/lib/aide/aide.db.new.list) <(cat /var/lib/aide/aide.db.list) | head -50
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
scp /var/lib/aide/aide.db backup-host:/backup/aide/$(date +%F).db

这里的「人工确认」不能省。如果直接 mv 就把新的状态当成基线,那你恰好把一次入侵造成的改动「合法化」了。至少要扫一眼变化列表里有没有网站目录下的新 PHP 文件。

建议把这个流程做成一个脚本,和发布流程绑在一起:每次部署上线之后自动 aide --update 生成候选基线,但替换要人工执行。这样既不会因为忘记更新基线而产生噪声,也不会因为自动替换而把异常吞掉。

小结

AIDE 的价值不在于「装了」,而在于「基线可信、范围合理、告警有人看、更新有节奏」这四件事。范围太大会跑不完,范围太小会漏关键位置,基线脏了整个系统的判断都会错,告警没人看等于没装,更新不及时会让你对真实异常麻木。四件事里最容易被跳过的是「基线备份到服务器之外」——它平时毫无存在感,但真出事时是你唯一能恢复判断能力的依据。

Last modification:November 3rd, 2026 at 01:30 pm

Leave a Comment