改了 crontab -e 还是有 root 文件?Linux cron 三级投放点排查实录
某后台管理系统的 PHP-FPM 运行时目录(runtime/log/、runtime/cache/)长期出现 root 拥有的文件,导致 www 用户写入失败、请求 500。第一轮排查找到了 root 的 user crontab,把任务改成 sudo -u www -- bash -c '...' 后提交——第二天仍然产生 root 新文件。
问题没有消失,只是变少了。这说明还有第二个来源。
现象
find runtime -uid 0 -type f每天凌晨仍有新增的 root 文件crontab -l(root 用户执行)已经没有可疑 entryps -eo user,cmd | grep php也没看到 root 跑的进程
根因
Linux cron 有三个独立的投放层,互不可见:
| 层级 | 路径 | 特征 |
|------|------|------|
| 系统级 | /etc/crontab + /etc/cron.d/* | 第 6 列显式指定 USER |
| 用户级 | /var/spool/cron/<user> | crontab -e 管理,无 USER 列 |
| systemd timer | systemctl list-timers --all | [Service] 段 User= 缺省即 root |
crontab -e 只能看到当前用户的 user crontab。系统级的 /etc/crontab 是完全独立的文件,crontab -l 不会展示它的内容。
本案的第二个污染源藏在 /etc/crontab 第 17 行:
0 9 * * * root cd /www/wwwroot/<project> && /usr/bin/php think sync:health-check --format=json >> /var/log/<project>-sync.log 2>&1虽然 >> 重定向到 /var/log/(不是 runtime 内部),但 php think sync:health-check 命令本身会调用 Log::write 和 Cache::set,在 ThinkPHP 的 runtime 树下落文件。以 root 身份运行,就照样污染 runtime 目录。
crontab -l(或 crontab -e)只管 /var/spool/cron/<user> 这一级,对 /etc/crontab 和 /etc/cron.d/* 完全无感知。即使把 user crontab 里的条目全部修干净,系统级的 root 任务依旧默默跑着。两者是平行的两套系统,没有任何合并入口。
正解
第一步:三级全审
# 1. 系统级 crontab
sudo cat /etc/crontab
# 2. cron.d drop-in
for f in /etc/cron.d/*; do echo "=== $f ==="; sudo cat "$f"; done
# 3. 所有用户的 user crontab
for u in $(cut -d: -f1 /etc/passwd); do
echo "=== user: $u ==="
sudo crontab -u "$u" -l 2>/dev/null
done
# 4. systemd timer(别漏这个)
systemctl list-timers --all命中规则:用户列 == root 且命令行含项目路径或 php think,即为风险条目。
第二步:改造 /etc/crontab
改之前先备份:
sudo cp /etc/crontab /tmp/etc-crontab.bak.$(date +%Y%m%d_%H%M%S)改造方式——命令整体包进 sudo -u www -- bash -c '...':
- 0 9 * * * root cd /www/wwwroot/<project> && /usr/bin/php think sync:health-check >> /var/log/<project>.log 2>&1
+ 0 9 * * * root sudo -u www -- bash -c 'cd /www/wwwroot/<project> && /usr/bin/php think sync:health-check >> /var/log/<project>.log 2>&1'三个关键点:
- 第 6 列 USER 保留
root——系统级 cron 文件格式要求第 6 列必须有 user 名,改成www不会让命令以 www 运行,只会让 cron 用 www 身份启动外壳;配合sudo -u www才是正确姿势 - 重定向
>>也包进内层——写在bash -c '...'外部的重定向由外层 root shell 打开,log 文件 owner 仍是 root;包进内层才由 www 打开 - 改后手工试跑验收:
sudo -u www -- bash -c 'cd ... && php think ...'先 exit=0,再确认runtime/下新文件 owner 是 www
三级全审找到所有投放点,改造时把整条命令(含 >>)包进 sudo -u www -- bash -c '...'。不要只改 user crontab,也不要把外层 USER 列改成 www——那不是切换执行身份的机制。
其他陷阱备忘
- 宝塔面板会用
chattr +i锁定 cron 文件,需要先chattr -i /etc/crontab才能编辑 - systemd timer 的 service unit 没写
User=时默认 root,比 cron 更隐蔽 php think依赖HOME/PATH等环境变量,sudo -u www后会重置,若命令报错可加sudo -E -u www保留环境
一句话外卖
crontab -l只是冰山一角——Linux cron 三级投放点互相独立,治 root 文件污染必须把/etc/crontab、/etc/cron.d/*、所有用户 spool 和 systemd timer 全部过一遍。