ClamAV 服务器恶意文件扫描实战:病毒库更新、上传目录定时查杀、inotify 实时拦截与隔离告警全流程

为什么网站服务器也要装杀毒软件

很多站长觉得「Linux 不会中病毒」,这话只说对了一半。Linux 本身被 Windows 式的病毒波及的概率确实低,但一台跑着网站、开放着文件上传、又被大量陌生 IP 反复扫描的服务器,会不断成为恶意文件的落脚点:有人往上传目录塞 webshell,有人把挖矿脚本丢进临时目录,有人拿你的服务器当中转站存放恶意软件。这些文件未必会「感染」系统,但它们的存在本身就是风险,也可能让你的服务器被服务商判定为滥用而封停。

ClamAV 是一款开源的命令行杀毒引擎,专为邮件网关和服务器场景设计。它的价值不是替代入侵检测,而是作为文件层面的一道兜底防线——尤其是在「上传目录」「临时目录」「共享目录」这几个恶意文件最爱藏身的地方做定时扫描。本文按个人站长的实际操作顺序,从安装、更新病毒库,到定时扫描、隔离与告警,走一遍完整流程,全部命令在 Debian 12 上实测过。

安装与病毒库更新

Debian/Ubuntu 一条命令装齐:

apt update
apt install -y clamav clamav-daemon

CentOS / Rocky 需要先启用 EPEL:

yum install -y epel-release
yum install -y clamav clamav-update

装完之后先更新病毒库。ClamAV 的病毒库分几个文件,主库叫 main.cvd,还有每日增量 daily.cvd 等。更新命令是 freshclam:

systemctl stop clamav-freshclam || true
freshclam
systemctl start clamav-freshclam

注意在手动跑 freshclam 前先停掉守护进程,否则会提示数据库被锁。更新成功后,/var/lib/clamav/ 下会出现几个 .cvd/.cld 文件。首次更新要下载几百 MB,慢是正常的。之后交给 clamav-freshclam 服务自动更新即可,它默认每天检查数次。

确认病毒库版本:

clamscan --version

输出里会同时显示引擎版本和病毒库版本与日期,如果日期是很久以前,说明更新没成功,要回头查 freshclam 的日志。

第一次全盘扫描:先摸底再定策略

直接对 / 全盘扫描会非常慢,还会报一堆「权限不足」和误报。正确的做法是先摸清哪些目录最需要关注,再针对性扫描。第一次可以先扫几个高风险目录:

clamscan -ri --bell \
  /var/www \
  /tmp \
  /var/tmp \
  /home 2>&1 | tee /var/log/clamscan-first.log

参数含义:-r 递归,-i 只显示感染文件(大大减少输出),--bell 发现病毒时响铃。扫完会打印统计,包括扫描文件数、感染数、耗时。第一次全跑可能要几十分钟,这是正常的,后面用增量或定时就不用这么折腾。

/tmp 和 /var/tmp 值得单独强调,因为大量挖矿脚本、反弹 shell 的落地文件都藏在这里,而且它们往往是可执行的。

识别误报与清理策略

ClamAV 对某些开源脚本会误报,尤其是那些带混淆、编码特征的文件。发现疑似感染时,不要立刻 rm,先核对:FOUND 后面的特征名是什么,比如 PHP.Shell、Unix.Trojan 之类。可以先把它隔离而不是删除,保留证据。

mkdir -p /var/quarantine
clamscan -ri --move=/var/quarantine /var/www

--move 把感染文件移到隔离目录而不是删除,这样万一误报还能还原。对于确认的 webshell,你还应该顺着它查访问日志,看看是谁、从哪个 IP 上传的,这一步比清理本身更重要——否则删了文件,上传入口还在。

如果是你自己写的、确定无害的脚本被误报,可以用 --exclude 排除,或把该文件的特征加入本地白名单文件(--exclude-dir、--exclude-pattern 也可用)。

定时扫描:用 systemd timer 而不是 crontab

生产环境不建议用 clamscan 的常驻模式死磕所有文件,更稳的是每天在低峰期扫一次上传目录与临时目录。用 systemd timer 的完整配置如下。

service 单元 /etc/systemd/system/clamscan-sites.service:

[Unit]
Description=Daily ClamAV scan for web directories
[Service]
Type=oneshot
Nice=19
IOSchedulingClass=idle
ExecStart=/usr/local/bin/clamscan-sites.sh

用 Nice=19 和 IOSchedulingClass=idle 把扫描压到最低优先级,避免刷爆 IO 影响网站响应。

扫描脚本 /usr/local/bin/clamscan-sites.sh:

#!/bin/bash
set -uo pipefail
LOG=/var/log/clamscan-daily.log
QUAR=/var/quarantine
mkdir -p "$QUAR"
echo "===== $(date '+%F %T') start =====" >> "$LOG"
clamscan -ri --move="$QUAR" \
  /var/www /tmp /var/tmp \
  >> "$LOG" 2>&1
# 若发现感染,触发告警
if grep -q "Infected files: [1-9]" "$LOG"; then
  grep "FOUND" "$LOG" | tail -20 | \
    mail -s "ClamAV 发现威胁 $(hostname)" you@example.com
fi

timer 单元 /etc/systemd/system/clamscan-sites.timer:

[Unit]
Description=Run ClamAV site scan daily at 4am
[Timer]
OnCalendar=*-*-* 04:00:00
RandomizedDelaySec=1800
[Install]
WantedBy=timers.target

RandomizedDelaySec=1800 让触发时间随机推迟最多半小时,避免每天固定时刻对磁盘造成脉冲压力。启用:

systemctl daemon-reload
systemctl enable --now clamscan-sites.timer
systemctl list-timers | grep clamscan

把扫描结果变成可读的告警

告警别刷屏,也别静默。日志文件会一直增长,配合 logrotate 限制大小;告警只在「感染数大于 0」时发一封邮件,邮件正文带上感染文件名列表就够了。如果服务器不发邮件,可以把同样的一行 grep FOUND 结果 POST 到你自己的通知接口,或写进一个被站点监控读取的 JSON 状态文件。

另外可以监控「扫描是否真的跑了」:如果 timer 因为故障连续几天没触发,你可能一直以为在保护,实际上什么都没做。在日志里记时间戳,再用一个简单的 check 脚本判断最近一次 start 距今是否超过 30 小时,超了就告警。这种「监控监控本身」的思路在运维里很常见。

与 inotify 联动:让新上传的文件立刻被扫

定时扫描有个天然的盲区:恶意文件可能在两次扫描之间就已经被访问执行。对于上传目录这种「内容高频变化」的位置,可以再加一道实时防线——用 inotify 监听文件写入,一旦有新文件落地就立即触发单文件扫描。核心工具是 inotifywait(来自 inotify-tools 包):

apt install -y inotify-tools
inotifywait -m -r -e close_write --format '%w%f' /var/www/uploads | \
while read f; do
  clamdscan --fdpass "$f" | grep -q FOUND && \
    mv "$f" /var/quarantine/ && \
    logger -t clamav-realtime "quarantine: $f"
done

这里用 close_write 事件表示一个文件写完了,避免扫到只写了一半的临时文件。--fdpass 把文件描述符直接传给 clamd,避免权限问题。这段逻辑同样可以做成一个 systemd service 常驻。实时扫描很轻(只扫新增的那一个文件),不会像全盘扫描那样压垮 IO,非常适合上传目录。它和定时全扫是互补关系:实时抓增量,定时兜底存量。

扫描策略的分层设计

一台服务器上不同目录的「风险等级」并不相同,把所有目录一视同仁地扫既浪费资源又掩盖重点。务实的分层做法是分三档。第一档是高风险目录:网站上传目录、临时目录、共享目录、可执行脚本落地点,这些必须扫,而且可以配合实时监听。第二档是中等风险:网站代码目录、用户家目录,适合每天低峰期定时扫。第三档是低风险:系统自身目录、包管理器的文件,除非你已经怀疑被入侵,否则没必要天天扫——它们几乎不变,扫了也是浪费。

把这三档策略写进扫描脚本的注释里,也写进你的运维笔记。将来接手的人一眼就知道为什么某些目录被排除,而不是误以为漏配了。分层不是为了省事,而是让有限的计算资源集中在真正会出问题的地方。

签名更新失败的排查

freshclam 更新失败是最常见的「以为在保护其实没有」的坑。典型原因有几个:一是 outbound 被防火墙挡了,freshclam 需要访问 CDN 节点,检查是否放行了 80/443 出站;二是 /var/lib/clamav 权限不对,freshclam 以 clamav 用户运行却没有写权限;三是 DNS 解析异常,导致数据库镜像域名解析失败;四是本地磁盘满了,下载不下来还不报错。排查顺序建议先看 journalctl -u clamav-freshclam -n 30,再去 /var/log/clamav/freshclam.log 看具体报错。

还有一点容易忽略:如果服务器长期无法访问外网,可以用另一台有网的机器下载 cvd 文件,再拷到 /var/lib/clamav 手工导入。虽然麻烦,但比病毒库停在几个月前、每天跑一个形同虚设的扫描强得多。可以写一个检查脚本,判断病毒库日期是否超过 7 天,超了就告警——这又是一次「监控监控本身」。

性能与资源考量

ClamAV 扫描很吃 IO,尤其在低配 VPS 上。几个务实的做法:一是只扫必要目录,别全盘;二是用 nice/ionice 压低优先级;三是把 clamd 常驻服务用起来(clamdscan 通过 socket 复用已加载的病毒库,比每次启动 clamscan 加载几百 MB 库快得多)。启用 clamd 后改用:

clamdscan -ri --move=/var/quarantine /var/www

内存方面,clamd 常驻大约占用几百 MB,配置里可以通过 MaxThreads、ConcurrentDatabaseReload 等参数调整。服务器内存小于 1G 的话,考虑用 --bytecode-timeout 和限制扫描并发,或者干脆只在夜间跑一次性 clamscan 而不常驻 clamd。

小结

ClamAV 不是万能的,它挡不住逻辑漏洞、SQL 注入或错误配置,但它在文件层面提供了一道成本极低的兜底:定时扫一遍上传目录和临时目录,发现恶意文件就隔离并告警。对个人站长来说,安装、更新病毒库、写好定时扫描和告警,是一套一次投入长期受益的安全基建。记住三条原则:先摸底再定策略、隔离而不是直接删除、以及日志和告警要能让你在出事时第一时间知道。

Last modification:October 9th, 2026 at 10:25 pm

Leave a Comment