用户权限 04|/etc/sudoers 安全配置:从最小授权到「别把 ALL 随便给人」

前面三篇里反复出现一句话:桌面机默认给的是万能钥匙。

复制代码
$ 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 进单用户模式修文件
相关推荐
꯭自꯭闭꯭2 小时前
达梦事物特性及MVCC
linux·运维·数据库
亚川楼宇自控系统数据中心厂家4 小时前
实验室智能化管理系统|人环物智一体化管控平台
运维
zmsup5 小时前
Agent 上下文调优:结合 OpsArk 运维智能体,设计模型每一步真正需要的信息
运维·agent·上下文压缩·运维智能体·上下文调优
吴声子夜歌8 小时前
Nginx应用与运维——Nginx负载均衡应用实战(二)
运维·nginx·负载均衡
lisanmengmeng8 小时前
NRPE 添加命令(一)
linux·运维·服务器
pt10438 小时前
CML网络仿真入门-2:使用CML模拟网络实验环境
运维·网络协议
hasty9 小时前
2026-10-04_ABSENTIA鉴权不变量审计_CSDN_待发布
安全
沫璃染墨9 小时前
《从零入门Linux系统篇(五十七):线程篇·十——生产者消费者模型进阶:从环形缓冲区到POSIX信号量》
linux·运维·服务器·开发语言·c++·系统架构·信号处理
HAHAXX810 小时前
电商RPA批量上架通用方案:一套流程如何同时跑通拼多多、抖店、淘宝和跨境平台
java·运维·rpa