返回博客

改了 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 用户执行)已经没有可疑 entry
  • ps -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::writeCache::set,在 ThinkPHP 的 runtime 树下落文件。以 root 身份运行,就照样污染 runtime 目录。

坑:crontab -l 看不到 /etc/crontab

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'

三个关键点:

  1. 第 6 列 USER 保留 root——系统级 cron 文件格式要求第 6 列必须有 user 名,改成 www 不会让命令以 www 运行,只会让 cron 用 www 身份启动外壳;配合 sudo -u www 才是正确姿势
  2. 重定向 >> 也包进内层——写在 bash -c '...' 外部的重定向由外层 root shell 打开,log 文件 owner 仍是 root;包进内层才由 www 打开
  3. 改后手工试跑验收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 全部过一遍。