前面三篇里反复出现一句话:桌面机默认给的是万能钥匙。
$ sudo -l
用户 rainbow 可以在 rainbow-VMware-Virtual-Platform 上运行以下命令:
(ALL : ALL) ALL
(ALL : ALL) ALL------任何命令,任何身份。在家用虚拟机上这没什么,但只要是别人能连上来的机器,这就是个大口子。
我第一次动这个文件的方式很蠢:直接 nano /etc/sudoers,改了一行保存。再敲 sudo,得到的是一行冷冰冰的报错:
sudo: /etc/sudoers 中第 23 行附近有解析错误
sudo: 没有找到有效的 sudoers 资源,退出
sudo: 无法初始化策略插件
那一刻整台机器都提不了权了------因为 sudo 靠这个文件工作,文件坏了,它自己也起不来。最后是进 GRUB 调出救援模式才改回来的。
这篇文章讲的就是怎么不重演这一幕,以及怎么把权限收到「刚好够用」。
〇、先花两分钟:sudoers 是什么,为什么不能直接编辑
/etc/sudoers 是提权规则的唯一真源 。你敲 sudo 时,它做的第一件事就是读这个文件,判断:「这个用户、在这台机器上、能不能以那个身份、跑这条命令」。
它有三个特点,每个都对应一个坑:
| 特点 | 后果 |
|---|---|
| 语法极其严格:格式错一个字符,整个文件失效 | 写坏了就像我那次一样,全机器提不了权 |
| 权限必须是 0440 | 权限不对,sudo 会直接拒绝加载(它是故意的,防止被改) |
| 它自己需要 root 才能读 | 所以你改坏了之后,没法用 sudo 改回来 |
第三条是死循环的关键:改坏 → 不能 sudo → 不能用 sudo 修。
所以第一条规矩:永远用 visudo 编辑,不要用编辑器直接打开这个文件。
visudo 干三件事:加文件锁(防止两个管理员同时改)、保存前做语法检查(写错了会拦下你,让你选择重写或放弃)、以及原子替换(避免写一半断电把文件截断)。
sudo visudo # 编辑主文件 /etc/sudoers
sudo visudo -f /etc/sudoers.d/90-deploy # 编辑指定片段文件
sudo visudo -c # 只检查语法,不编辑(最常用的一条)
怎么读 :-c 是 check。养成习惯------改完就 sudo visudo -c 跑一遍,三秒钟的事:
$ sudo visudo -c
/etc/sudoers: parsed OK
看到 parsed OK(中文环境会显示「解析正确」)才算真的改完了。
版本情报(2026) :从 Ubuntu 25.10 起,
sudo已经换成 Rust 重写的 sudo-rs ,26.04 LTS 沿用。它日常用法和经典 sudo 一样,但并非 100% 兼容 ;原版作为sudo.ws/visudo.ws保留着。想知道自己用的是哪个,看软链接指向哪:
$ ls -l /usr/bin/sudo /usr/bin/sudo-rs lrwxrwxrwx 1 root root 22 Mar 13 2026 /usr/bin/sudo -> /etc/alternatives/sudo lrwxrwxrwx 1 root root 21 Aug 27 16:15 /usr/bin/sudo-rs -> ../lib/cargo/bin/sudo怎么读 :
../lib/cargo/bin/sudo是 Rust 版(cargo 是 Rust 的包管理器)装出来的路径,所以本机走的就是 sudo-rs。想切回经典版:sudo update-alternatives --set sudo /usr/bin/sudo.ws。
(万一已经改坏了、sudo 又用不了,还有两条退路:① 如果你的用户还在 sudo 组里,可以切到另一个 TTY Ctrl+Alt+F3 用 root 直接登录;② 进 GRUB 在启动参数后加 init=/bin/bash 进单用户模式,mount -o remount,rw / 之后修文件。这两条路都不轻松,所以还是别写坏。)
图示:sudoers 规则行四段结构,范围收窄靠第三段
一、规则行的语法:一句话拆成四段
/etc/sudoers 里真正管权限的行,都是这个结构:
谁 在哪台机器 = (以谁的身份) 能跑哪些命令
对着具体例子看就清楚了:
rainbow ALL = (ALL:ALL) ALL
│ │ │ │ └── 命令:ALL = 任何命令
│ │ │ └─────── 目标组:ALL
│ │ └────────── 目标用户:ALL
│ └────────────────── 主机:ALL = 任何主机
└────────────────────────── 用户:rainbow
怎么读 :从左到右问四个问题------谁 、在哪 、变成谁 、干什么。
四段里最容易被忽略的是「变成谁」那一段,而它恰恰是收权限的关键。看这条真实场景:
deploy ALL = (root) NOPASSWD: /usr/bin/systemctl restart nginx
读法:deploy 这个用户,在任何主机上,可以不用密码地、以 root 身份,执行「重启 nginx 这一条命令」。
对比一下:
| 写法 | 这个用户实际能干什么 |
|---|---|
rainbow ALL=(ALL:ALL) ALL |
什么都能干(等于给了 root) |
deploy ALL=(root) /usr/bin/systemctl restart nginx |
只能重启 nginx,别的都不行 |
monitor ALL=(root) /usr/bin/journalctl |
能看日志,但如果参数不限制,等于能读机器上一切文本 |
这就是「最小授权」:不是「给不给 root」,而是「让他在什么范围内行使 root」。
那自己机器上实际生效的是哪几条?把注释和空行滤掉就看到了:
$ sudo grep -vE '^\s*(#|$)' /etc/sudoers
Defaults env_reset
Defaults mail_badpass
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
Defaults use_pty
root ALL=(ALL:ALL) ALL
%admin ALL=(ALL) ALL
%sudo ALL=(ALL:ALL) ALL
@includedir /etc/sudoers.d
怎么读 :前四行是 Defaults 全局设置;root 那行是给 root 自己的;%admin 和 %sudo 才是给「组」的授权 ------Ubuntu 上一开局那句 (ALL : ALL) ALL,就是这两行给的。
(顺带留意 %admin ALL=(ALL) ALL 少了个 :ALL------那是「目标组」留空,在本机场景下几乎没差别,但写自己的规则时最好写全 (ALL:ALL)。)
二、secure_path:为什么 sudo 之后命令会找不到
上一篇(C1)里我们看过 sudo 会把自己的 PATH 整个换掉。现在来看它是在哪一行、怎么写的:
$ sudo grep -n secure_path /etc/sudoers
11:Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
怎么读 :Defaults 开头的是「全局默认设置」,不限某个用户。这一行的意思是:sudo 执行命令时,PATH 就固定用我给的这一条。
对照你自己的 PATH(/etc/environment 里那行):
| 目录 | 你登录后 | sudo 之后 |
|---|---|---|
/usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin |
✔ | ✔ |
/usr/games、/usr/local/games |
✔ | ✘ |
/snap/bin |
✔(重复两次) | ✔(一次) |
它为什么要这么做? 为了防一类经典攻击:你在自己可写的目录里放一个假的 ls,然后想方设法让 root 的 PATH 优先找到它------root 敲 ls 时执行的就是你的程序。固定 secure_path 就把这条路堵死了。
代价 是你会遇到「sudo 命令 提示 command not found,不加 sudo 反而能找到」:
| 情况 | 处理方式 |
|---|---|
| 想给 sudo 临时补一个目录 | sudo env PATH=$PATH 命令,或 sudo -E 命令(保留当前环境) |
| 这个目录所有管理员都该有 | 把它加进 secure_path(用 visudo) |
| 只是自己装的工具 | 装到 /usr/local/bin(它本来就在 secure_path 里) |
注意第三条------这是最省事的答案 :自己写的脚本别放在 ~/bin 或 /opt/xxx,放到 /usr/local/bin,sudo 和你自己都能直接调用。
三、NOPASSWD:最方便,也最危险
NOPASSWD: 是「执行这条规则命令时不用输密码」的标记。它在两种场景下是合理的:
-
自动化脚本:定时任务里没法交互输密码;
-
高频运维动作:一天重启二十次服务,每次输密码会让人放弃治疗。
但它有一个致命前提:必须把命令范围限死。
看这几种写法,效果天差地别:
| 写法 | 危险程度 |
|---|---|
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx |
✅ 可以接受:只能重启 nginx |
deploy ALL=(root) NOPASSWD: ALL |
⚠️ 等于免密 root,和给了 root 密码没区别 |
deploy ALL=(root) NOPASSWD: /usr/bin/vim |
☠️ 等于免密 root ------在 vim 里敲 :!bash 就拿到 root shell |
deploy ALL=(root) NOPASSWD: /bin/cat |
☠️ 能 cat /etc/shadow,等于密码泄露 |
deploy ALL=(root) NOPASSWD: /usr/bin/tee |
☠️ 能往任何位置写文件,包括写进 sudoers |
要记住的是这一类「危险命令」 :任何能起 shell、能执行别的命令、能读写任意文件的程序,一旦被免密授权,授权范围就等于无限。典型名单:
vim / vi / nano 编辑器 → 内部都能起 shell(:!bash)
less / more / man 分页器 → 都能 !命令
bash / sh / python / perl / awk / find 解释器 → 直接就是 shell
cat / tee / cp / mv / dd 文件操作 → 能读 shadow、能覆盖 sudoers
systemctl / journalctl 不加参数限制 控制/读取面太广
一条经验 :只要授权里出现了通配符 *,就得停下来想一秒。 比如 systemctl restart * 听起来只是「重启服务」,但如果可以顺带传参数,攻击面就跟着打开了。
写最小授权的正经姿势:
# 只允许重启这两个服务,且不接受额外参数
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl restart php8.3-fpm
(后面用逗号接着写,比用通配符安全得多。)
四、别动主文件:用 /etc/sudoers.d/
Ubuntu 的 /etc/sudoers 末尾通常有这么一段,这是全篇最该抄走的一条:
@includedir /etc/sudoers.d
它的意思是:把 /etc/sudoers.d/ 目录下的所有文件也读进来当规则。
顺手纠正一个老写法 :网上见到的多半是
#includedir /etc/sudoers.d(一个#)。那不是注释 ------#includedir是旧写法,@includedir是 sudo 1.9.1 之后的推荐写法,两者行为完全一样。真正要小心的是##includedir(两个#):那才是被注释掉,整个目录都不会被读,你写的规则自然全部失效。
为什么必须用它 :系统升级可能覆盖主文件、主文件越改越长越难维护,而你自己的规则放进 sudoers.d 里,升级不受影响、出问题只坏自己那一块。
$ ls -l /etc/sudoers.d/
-r--r----- 1 root root 863 Jan 15 2026 README
(我这台机器上这目录里就一个 README,说明系统还没放过任何自定义规则------干净是好事,你的规则将来会以同名文件的形式躺在它旁边。)
命名和权限有三条硬规矩(踩过的人都懂):
| 规矩 | 原因 |
|---|---|
文件名不要带 . (90-deploy 可以,90.deploy 不行) |
带点的文件会被 sudo 忽略(这个规则正是为了跳过 .dpkg-old、.bak 这类残留) |
文件名不要以 ~ 结尾 |
会被当成规则读,然后报语法错误 |
| 权限必须是 0440 | 用 visudo 建的话它会自动设好;手动 cp 进去记得改 |
第三条后面还跟着一个高频事故 :你用 visudo -f 建好规则,复制一份当备份,命名成 90-deploy.bak------看起来没事(带点会被跳过),但如果命名成 90-deploy~,sudo 会当场报错,然后你就又回到了开头那一幕。
所以规则文件只有一份,改了就是改了,不要在这目录里留备份。 备份放到别处去。
五、常用需求的标准写法
| 需求 | 做法 |
|---|---|
| 让某人拥有完整 sudo | sudo usermod -aG sudo 用户名(别去改 sudoers) |
| 让整个组都能提权 | sudoers 里写 %组名 ALL=(ALL:ALL) ALL(% 开头代表组) |
| 只允许免密重启某服务 | 用户名 ALL=(root) NOPASSWD: /usr/bin/systemctl restart 服务名 |
| 只允许免密查看某类日志 | 用户名 ALL=(root) NOPASSWD: /usr/bin/journalctl -u 服务名 |
| 提权时保留自己的环境变量 | Defaults:用户名 !env_reset(谨慎,会带来前面说的 PATH 风险) |
| 让脚本以特定用户常驻运行 | 别用 sudo ------写成 systemd 单元并指定 User=,这才是正路 |
最后一行值得单独说一句 :很多人用 sudo 去解决「让服务以别的身份跑」的问题(比如 sudo -u www-data python app.py),结果是进程挂在你的终端会话下,你一退出它就死。
「以某个身份长期运行」是 systemd 的活,不是 sudo 的活。(那是本专栏后面「服务搭建」部分的内容。)
六、五个坑
坑 1:用 nano / vim 直接改 /etc/sudoers。 没有锁、没有语法检查、没有原子替换。永远 sudo visudo。
坑 2:改完不检查。 sudo visudo -c 三秒。尤其是:改完必须重新开一个终端试一次 sudo -l------别等你需要提权的时候才发现规则写废了。
坑 3:把编辑器、解释器、cat、tee 放进 NOPASSWD。 见第三节,这等于免密 root。
坑 4:在 sudoers.d 里留备份文件。 带 ~ 的会被当规则读并报错;带 . 的会被静默忽略(你以为备份生效了,其实规则根本没加载)。改完记得 sudo visudo -c 加 sudo -l 双验证。
坑 5:以为「加进 sudo 组」和「写 sudoers 规则」是一回事。 加组是给全权 ((ALL:ALL) ALL,因为默认 sudo 组的规则就是这么写的),写规则是给范围 。想收权限,就得离开「加组」这条路,去 sudoers.d 里写明确规则。
速查表(文末收藏版)
编辑规则(永远用它) → sudo visudo
编辑片段文件 → sudo visudo -f /etc/sudoers.d/90-xxx
改完必须检查语法 → sudo visudo -c
看自己的提权权限 → sudo -l
规则语法 → 谁 主机=(以谁的身份) 命令
组授权 → %组名 ALL=(ALL:ALL) ALL
免密但限定命令 → 用户名 ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
sudo 的 PATH 在哪设 → Defaults secure_path="..."(/etc/sudoers 第 11 行)
sudo 找不到命令 → sudo -E 命令 / 或用 sudo env PATH=$PATH 命令
sudoers.d 三条规矩 → 不带 "."、不以 "~" 结尾、权限 0440
危险授权(等于 root) → NOPASSWD: ALL / vim / bash / python / cat / tee
官方写法参考 → man sudoers(搜 EXAMPLES 一节)
救援 → GRUB 加 init=/bin/bash 进单用户模式修文件
图示:sudoers 规则行四段结构,范围收窄靠第三段