我在 WordPress 后台里,造了一个 root 权限的文件管理器
事情是从一个 6GB 的镜像开始的。
那天我要把一批系统镜像和安装包传到内网服务器上。用 scp 当然可以,命令我闭着眼都能敲,但传完还有一堆"小事":挪目录、改权限、解压、替换配置文件……每一次都要回到黑乎乎的终端。
我是一个能不动终端就不动终端的人。服务器上明明有一个我天天打开的 WordPress 后台,为什么不能直接在里面管文件?
先说清楚:这件事听起来就很危险
WordPress 的后台进程叫 www-data,权限很低,这是好事。而我想管的东西里,有 /etc 里的配置、有 /srv 里的下载资源、有 Web 目录——说白了,需要"接近 root"的文件权限。
最省事的做法,是给 www-data 免密 sudo 所有命令。也是最疯的做法:网站一旦被攻破,攻击者直接拿到整台服务器。这个念头刚冒出来就被我自己按死了。
后来又想过几个折中方案:单独装个文件管理面板?又多一个要维护的服务;用 FTP?那是上个时代的答案。绕了一圈,最终方案是:把 root 关进一个只会做固定动作的盒子里。
一个只认"清单"的特权助手
盒子的实现是一个 sudo helper。网页端把要做的操作打包成一段 base64 的 JSON——比如"列出这个目录""把这个文件移动到那里"——再通过 sudo 调用 /usr/local/bin/llx-fs。
helper 会校验操作类型,只认十来个固定的动作:list、mkdir、newfile、rename、delete、copy、move、read、write、chmod、put、readfile。它不执行任意命令,不读任意位置的文件,更不会给你一个 shell。参数再花哨,最后也只能落进这十二个动作里。
sudoers 里给它开了一个精准的口子:
Defaults:www-data !requiretty
www-data ALL=(root) NOPASSWD: /usr/local/bin/llx-fs只允许 www-data 免密切换到这一个文件,其他命令一概不行。也就是说,"网页进程能拿到 root"这件事,被压缩成了"网页进程能递一张十二格的清单"。
把 root 关进一个只会十二个动作的盒子里,这是我给这个功能划下的安全底线。
它长什么样:一个网页版资源管理器
打开 WordPress 后台,左侧菜单里多了一项"文件管理"。点进去,是一个仿 Windows 资源管理器的界面。
左侧是快速访问,顶上是可以直接点击编辑路径的面包屑地址栏。选中文件能下载、在线编辑(2MB 以内的文本)、重命名、改八进制权限;能新建文件夹和文件,复制剪切粘贴,递归删除;列表支持搜索和排序,右键有菜单,键盘也能用:F2 重命名、Del 删除、Ctrl+A/C/X/V、Backspace 返回上级。
上传是最花心思的部分。文件可以直接拖进列表区,右下角会浮出一个进度条:百分比、已传/总量、实时速度、剩余时间。传一个 6GB 的镜像时看着速度数字跳动,居然有点解压。
把上限一点点撬开
PHP 默认只让上传 2MB 的东西——这在今天简直像个冷笑话。为了让 6GB 的镜像能过,我把 PHP-FPM 和 Nginx 的几道上限分别撬开:upload_max_filesize 和 post_max_size 调到 8G,超时、内存限制放开,Nginx 的 client_max_body_size 同步调成 8g。
改完重启 PHP-FPM、reload Nginx,大文件稳稳落地。这里有个小提醒:这几个参数分别在 PHP-FPM 的 conf.d 和站点的 Nginx 配置里,改完记得两边的服务都要重载,只改一边会互相打脸。
技术实现:从点击到落盘
整套东西一共五块,全部放在服务器上,没有引入新服务:
wp-content/mu-plugins/llx-filemanager.php # 菜单、AJAX、权限、nonce
wp-content/mu-plugins/llx-fm/fm.js|fm.css # 前端界面与交互
/usr/local/bin/llx-fs # sudo 入口(wrapper)
/usr/local/bin/llx-fs.php # 固定动作实现(root)
/etc/sudoers.d/llx-webadmin # 只放行 llx-fs 一条网页和 helper 之间的协议非常土:把操作打包成一段 JSON,base64 编码后作为唯一参数传进去。helper 解开、校验 op 在允许清单里,然后执行。想手工调试,一行命令就能用:
payload=$(printf '%s' '{"op":"list","path":"/srv/downloads"}' | base64 -w0)
sudo -u www-data sudo -n /usr/local/bin/llx-fs "$payload"目前支持的 op 一共十二个:list、mkdir、newfile、rename、delete、copy、move、read、write、chmod、put、readfile。每个 op 只接受固定参数,路径做规范化,不碰 shell,也不接受"执行命令"这类花样。
前端上传走 admin-ajax,用 XMLHttpRequest 的 upload 进度事件算速度和剩余时间,界面上的进度条就是从这里来的;编辑保存、删除、移动这些操作,统一走同一套 AJAX + helper。
要让 6GB 的文件过五关斩六将,这几处上限都要放开:
# /etc/php/7.4/fpm/conf.d/99-llx-upload.ini
upload_max_filesize = 8G
post_max_size = 8G
max_input_time = -1
max_execution_time = 0
memory_limit = -1
# nginx 站点配置
client_max_body_size 8g;改完两边都要生效:systemctl restart php7.4-fpm 加 nginx -s reload,只动一边会互相打脸。
权限模型上,插件里每个 AJAX 动作都会先检查当前用户是否管理员(manage_options),再校验 nonce;helper 侧则只信 sudoers 放行的那一条命令。默认起始目录可以用 WordPress 选项改:
update_option('llx_fm_home', '/srv/downloads');如果要做审计,可以在 helper 里加一行日志(谁、什么 op、哪个路径、结果如何),排查问题时非常好用——毕竟这套东西的每一步都值得留痕。
安全账,还是要算清楚
这套东西本质上是把"文件层面的 root"交给了 Web 进程。所以给自己立了几条规矩:后台只在内网访问,绝不暴露公网;WordPress 和插件保持更新;后台用强密码;能不用的时候关掉。
关掉的成本很低,三条命令删掉 sudoers 规则、helper 和插件文件,它就从"root 文件管理器"退回成普通站点后台。这也是当初把它设计成"独立盒子"的原因:能开,也要能关。
便利是会上瘾的,所以每个便利后面,都应该留一个总闸。
一个小小的延伸
现在传下载资源变成了一件很顺的事:文件从文件管理器拖进 /srv/downloads 的对应分类,下载站首页下次访问就会自动出现它;图标和说明也会有专门的脚本去认领——这部分故事我写在另一篇里了。
如果你也维护着一台跑 Web 服务的服务器,可以想想:你每天有多少"其实可以顺手做掉"的运维动作,正在逼你开终端?
当然,动手之前请先想清楚权限边界。方便的尽头是权力,权力的尽头是责任。
有类似需求的朋友,评论区聊聊你们是怎么管服务器文件的。
本文由个人实践整理,文中服务器地址、账号等敏感信息均已做模糊处理。