sudo 权限委派实战:sudoers 语法、NOPASSWD 风险与命令白名单的提权陷阱

为什么「一直用 root」是最贵的运维习惯

几乎每个个人站长都经历过这样的启动阶段:拿到一台 VPS,root 密码登录,安装 Nginx 和 MySQL,改配置,上线。整个过程顺畅得像呼吸,于是 root 就成了日常操作的习惯。真正的问题不是 root 会立刻出事,而是它同时消灭了两样东西:误操作的刹车事故的可追溯性。用 root 执行 rm -rf 只是少敲一个字母,而一旦真的删错,没有任何一层权限会拦你;更麻烦的是当服务器上出现异常文件、异常进程时,因为所有人都是 root,你无法从权限归属上判断是谁、用什么身份改的。

这篇文章不打算重复「要禁用 root 登录」这类已经写过很多次的内容,而是聚焦一个更具体的问题:如何用 sudo 把 root 权限拆成一份一份、可以精确发放和随时收回的授权。内容包括 sudoers 的真实语法结构、最常见的三种危险配置、用 sudo -l 审计授权面、命令白名单的绕过陷阱,以及一套个人站长可落地的授权方案。

sudo 的本质:它不是「临时的 root」,而是权限委派引擎

很多人把 sudo 理解成「输入自己的密码就能变成 root」,这个理解只覆盖了最常用的一种配置。sudo 的真实模型是:谁(User)在哪台机器上(Host),可以以谁的身份(Runas),执行哪些命令(Command)。这四段拼起来才是一条完整的 sudoers 规则,语法形式是:

用户 主机=(目标用户[:目标组]) 命令列表

举例,允许运维用户 deploy 在任意主机上,以 root 身份执行 systemctl 重启 nginx:

deploy ALL=(root) /usr/bin/systemctl restart nginx

把它读成一句话就是:「deploy 可以在任何机器上,作为 root,运行这一条命令」。括号里是 Runas 段,它决定了「升到谁」;如果写 (ALL:ALL) 就表示可以切换成任何用户和任何组,这是很多教程的默认写法,也是风险最大的写法。

这里有个容易忽略的细节:sudoers 的匹配是按命令的绝对路径做的。写 /usr/bin/systemctl 和写 systemctl 在严格模式下效果不同,写后者会被当作「在 secure_path 里查找」,而攻击者如果能控制 PATH,就可能让自己目录下的假 systemctl 被调用。所以命令白名单必须写绝对路径,这不是洁癖,而是安全边界本身。

第一层审计:动手改之前先看清现有授权面

在改任何配置前,先做一次只读审计。sudo 自带一个非常实用的参数:

sudo -l

它会以当前用户的身份,列出「你现在被允许做什么」。对每个用户都跑一遍,就能得到一张授权地图。更彻底的做法是直接列目录:

ls -l /etc/sudoers.d/
sudo grep -rn "^[^#]" /etc/sudoers /etc/sudoers.d/

第二条命令的作用是过滤掉所有注释行,只留下真正生效的规则。为什么强调这一点?因为 /etc/sudoers 里通常有大段以 # 开头的示例和说明,直接看文件会觉得改了也没动多少,但真正生效的往往就是那么几行。审计时要找三类可疑特征:

  • 出现了 NOPASSWD: ALL,等于给了免密完整 root
  • Runas 段写成了 (ALL:ALL),等于给了身份切换的完全自由
  • 命令白名单里出现了万能命令,例如 vim、find、tar、less、awk 这类可以直接 spawn shell 的程序

陷阱一:NOPASSWD 让密码这道闸门彻底失效

NOPASSWD 是最常见的便利性妥协。它的初衷是让自动化脚本免交互执行特权命令,但在错误的地方使用会带来两个后果。

首先是失去二次确认。正常情况下,即使用户的密码被泄露,攻击者至少还需要知道 sudo 密码才能提权;一旦配上 NOPASSWD,只要拿到该用户的 SSH 会话或任何能执行命令的入口,就直接等于 root。

其次是失去留痕的心理门槛。sudo 默认会把每次调用记录到 syslog,但 NOPASSWD 下这些调用会变得极其频繁,你很难从中发现异常。

正确的做法是把 NOPASSWD 精确限制到具体命令,而不是整条 ALL。例如让备份脚本无需密码调用 rsync:

backup ALL=(root) NOPASSWD: /usr/bin/rsync
backup ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx

这两行的授权面非常清楚:它只能跑 rsync 和重启 nginx,不能读 /etc/shadow,也不能装内核模块。这比 NOPASSWD: ALL 的价值高出一个数量级。

陷阱二:命令白名单其实是一种「伪边界」

这一节是本文最需要记住的部分。很多人以为「只允许 sudo 执行 vim」是安全的,因为 vim 只是个编辑器。但只要被授权的程序本身能执行 shell 或读写任意文件,这条白名单就已经被绕过了。

用 vim 举例,被授权者可以这样把自己变成 root shell:

sudo vim -c ':!/bin/sh'

原因是 vim 有一个内部命令 :!,它的语义是「调用外部 shell 执行命令」,而这个 shell 继承了 sudo 赋予的 root 身份。同类程序还有:

less /etc/shadow      # 在 less 里按 ! 可执行 shell 命令
find / -exec /bin/sh \; # -exec 直接执行任意程序
tar -cf /dev/null /tmp --checkpoint=1 --checkpoint-action=exec=/bin/sh
awk 'BEGIN{system("/bin/sh")}'

这四条是最经典的 GTFOBins 提权路径。结论很明确:授权命令白名单时,必须假设「程序自身的功能」都是可用的。因此 vim、less、find、tar、awk、python、perl、nmap、tee、dd 这些「图灵完备」或能读写任意文件的程序,都不应该出现在普通用户的 sudo 白名单里——除非你确实接受等同于 root 的授权。

那如果业务上真的需要让某个用户读某几个日志文件呢?答案是不要授权程序,而是授权文件。用 sudoers 的目录授权配合最小权限,或者直接用文件所属组来解决:

usermod -aG adm opsuser     # 加入能读日志的组

让用户通过组身份读日志,而不是通过 sudo 跑 cat——后者虽然看着无害,但没人能保证以后不会有人把 /etc/shadow 也加进那个参数列表。

陷阱三:sudoers 语法一错,可能把所有人锁在门外

修改 /etc/sudoers 时最容易忽视的是:它必须通过 visudo 编辑,且保存时会做语法校验。如果直接 nano /etc/sudoers 写错一个字符(例如漏掉了 Runas 的括号、把 ALL 写成 A.L),下一次 sudo 会因为语法错误直接拒绝所有请求,而你能用的补救手段就只剩单用户模式或挂载救援盘。

几条硬性规则:

  • 永远用 visudo,不要用其他编辑器直接改
  • 自定义规则放进 /etc/sudoers.d/ 下的独立文件,文件名避免带点和波浪号(.~ 结尾的文件会被忽略)
  • 新文件权限设为 0440,属主 root:root,否则 sudo 会拒绝加载
  • 改完立即用 visudo -c 做全量语法检查

第三点尤其容易踩:很多人在 /etc/sudoers.d/ 下写完文件忘了 chmod,规则静默不生效——不报错,就是不认。遇到「明明写了却没生效」的情况,先查权限再看语法:

sudo visudo -c
sudo ls -l /etc/sudoers.d/

一套个人站长可落地的 sudo 方案

个人站长的场景其实很清晰:自己一个人用,但希望日常操作不直接是 root。下面这套方案在单人或两三人协作的服务器上都够用。

第一步,禁用 SSH 的 root 直登,创建普通账号并加入 sudo 组:

adduser ops
usermod -aG sudo ops
# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no

第二步,日常操作完全不使用 sudo,只有真正需要特权时才显式调用。查看当前身份用 id,需要提权时用 sudo 前缀,这样每一处特权操作都会在日志里留下明确边界:

id
sudo systemctl daemon-reload
sudo nginx -t && sudo systemctl reload nginx

第三步,给「必须自动化的特权动作」开精准的 NOPASSWD 白名单,例如证书续期成功后自动 reload:

# /etc/sudoers.d/zz-deploy
ops ALL=(root) NOPASSWD: /usr/bin/systemctl reload nginx
ops ALL=(root) NOPASSWD: /usr/bin/systemctl restart php8.2-fpm

第四步,把 sudo 的调用记录单独留一份。默认 sudo 日志混在 auth.log 里,把它抽出来看会舒服得多:

grep -i "sudo:" /var/log/auth.log | tail -50
journalctl -t sudo --since "24 hours ago"

验证授权是否真的按你设想生效

改完配置不要凭感觉。用 sudo -l -U ops 以 ops 用户的视角打印授权清单,逐条核对:

sudo -l -U ops
# 期望看到类似:
# User ops may run the following commands on web01:
#     (root) NOPASSWD: /usr/bin/systemctl reload nginx

再做一个负向测试:故意尝试一条不该被允许的命令,确认它被拒绝。

sudo -u ops sudo /usr/bin/vim -c ':q'
# 期望: ops is not in the sudoers file / command not allowed

能跑通的负向测试才算配置正确。只测「允许的通了」而不测「禁止的拦了」,等于没验证。

小结

sudo 的价值不在于「少用 root」,而在于把 root 权限变成一份可以被精确描述、被审计、被收回的授权。三个要点值得反复回看:第一,命令白名单必须写绝对路径,并且要意识到 vim、find、tar 这类程序本身就是提权通道;第二,NOPASSWD 要精确到命令,绝不留 ALL;第三,sudoers 只走 visudo,改完必做 visudo -c 和负向测试。做到这三点,你就把「一直用 root」换成了「只在必需的那一步、以明确记录的方式使用特权」。

Last modification:September 22nd, 2026 at 10:24 pm

Leave a Comment