Ubuntu 26.04 默认 sudo-rs 的 sudoers 配置变更
Ubuntu 26.04 将 sudo-rs 设为默认实现。核心变更在于 sudo-rs 不再支持命令参数的 glob 通配符匹配。若您的 sudoers 文件包含此类规则,升级后将失效。本文详细说明如何检查当前 sudo 版本,并提供兼容性配置的修改建议,确保系统权限管理平稳过渡。
sudo-rs 在 Ubuntu 上的变更
Ubuntu 26.04 LTS 默认使用 sudo-rs 作为 sudo 的实现,因此在全新服务器上运行 sudo 命令时,执行的是 Rust 重写版本,而非原有的 C 语言程序。大多数 sudoers 文件可以继续正常工作。唯一失效的规则是包含命令参数通配符的规则,因为 sudo-rs 不会对参数文本进行 glob 模式匹配。
Ubuntu 25.10 首先进行了切换,而 26.04 LTS 延续了这一变更。Ubuntu 24.04 LTS 不受影响,因为它仍默认选择原始的 sudo,除非手动安装 sudo-rs。此变更的影响点在于您 从 Ubuntu 24.04 升级到 26.04 的时刻,或者在较新版本上构建新服务器的时刻。如果您也运行过渡版本,LTS 与 Ubuntu 过渡版本在服务器上的区别 一文解释了哪些机器会优先遇到此类变更。
检查服务器当前运行的 sudo 版本
不要通过发行版版本号来推断。直接查询机器。
sudo --version
update-alternatives --config sudo
dpkg -l 'sudo*'请信任本地机器上的 sudo --version,不要盲目参考互联网上的版本对照表(包括本页面)。update-alternatives --config sudo 是答案的另一半:它列出了所有已安装的 /usr/bin/sudo 提供程序,并标记出当前选定的版本。软件包已安装并不代表它已被选中,因此请查看选定项,而非软件包列表。
在过渡期间,两种实现均已打包。Rust 版本为 sudo-rs,截至 2026 年 8 月,其在 26.04 中的版本为 0.2.13。由 Todd C. Miller 维护的原始版本打包为 sudo.ws,其程序带有 .ws 后缀:sudo.ws 和 visudo.ws。
为什么 Ubuntu 转向使用 sudo-rs
sudo 依赖 setuid root 机制。系统上的任何用户均可启动该程序,且程序会以完整权限运行,因此其内部的内存错误即意味着本地 root 提权漏洞。CVE-2021-3156 正是此类漏洞:这是一个任何本地用户均可触发的堆缓冲区溢出漏洞,且在已发布的代码中存在了约 10 年之久。Rust 能够在编译阶段捕获此类错误,这正是重写该程序的核心理由。
第二个原因是功能范围,这一点会直接影响您的配置。原始的 sudo 在过去 30 年间积累了庞大的功能集,而每一项功能都意味着有更多代码以 root 权限运行。sudo-rs 有意仅实现其中的一个子集。作者认为属于小众或存在潜在危害的功能均被剔除,因此某些多年来一直有效的 sudoers 配置项可能会直接失效。您的通配符规则就属于此类情况。
内存安全性消除了某一类错误,但这并不代表程序完全没有漏洞。自 sudo-rs 成为默认配置以来,它也发布过自身的安全补丁。请像对待其他软件一样对其进行补丁更新。
哪些 sudoers 规则仍然有效
该文件与原版一致。sudo-rs 会读取 /etc/sudoers 以及 /etc/sudoers.d/ 中的插件文件,且支持服务器运维人员常用的配置:
deploy ALL=(ALL:ALL) ALL,以及诸如%sudo ALL=(ALL:ALL) ALL的组形式NOPASSWD:和PASSWD:标签User_Alias、Runas_Alias、Host_Alias和Cmnd_Alias- 带有精确参数列表的命令,例如
/usr/bin/systemctl restart app-api - 命令后跟
"",表示仅允许执行不带任何参数的该命令 - 命令后跟
*作为其最后一个参数,表示允许执行带有任意后续参数的该命令 - 以
/结尾的目录路径,表示允许执行该目录下的任何命令 !用于从列表中排除某个命令Defaults的一个实用子集,包括secure_path、env_keep、env_check、timestamp_timeout、passwd_tries、editor、umask、targetpw、rootpw和use_pty
有两个默认设置的行为有所不同,容易导致用户困惑。env_reset 在 sudo-rs 中无法关闭:它始终处于开启状态。use_pty 默认开启,因此命令会在其自身的伪终端中运行。
为什么您的通配符 sudoers 规则不再匹配
通配符仅在一处仍然有效:命令的文件名。规则 %ops ALL = /sbin/fsck* 仍然允许 sudo fsck 和 sudo fsck_exfat,因为 * 是所匹配文件系统路径的一部分。
在参数列表中,sudo-rs 仅接受两种特殊形式,且均非模式匹配。"" 表示无参数。末尾的 * 表示任何后续参数。所有其他参数均按字面文本进行比较。因此 %ops ALL = /sbin/service ntp * 是有效的,因为 ntp 是字面量且 * 位于末尾。然而,像下面这样的规则无法实现您的预期:
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart app-*app-* 是参数中间的一个模式。sudo-rs 不会对其进行展开,因此该规则不涵盖 systemctl restart app-api,sudo 会拒绝执行该命令。以下两个命令可用于核实您服务器上任何规则的实际情况:以 root 身份运行 sudo -l -U deploy 可打印该账户实际可执行的命令,sudo visudo -c 则用于检查文件是否能被正确解析。在盲目修改配置前,请先运行这些命令进行验证。
通配符规则一直存在漏洞
在原始的 sudo 中,您输入的参数会被拼接成一个字符串,并使用 glob 模式与规则中的参数字符串进行匹配。glob 模式会匹配空格,这是几乎所有人都会忽略的一点。
sudo-rs 文档给出了最清晰的演示。规则 /bin/rm *.txt 同样允许 sudo rm -rf /home .txt,因为其中的 * 会吞掉 -rf /home ,且拼接后的字符串依然以 .txt 结尾。该规则本意是“仅限文本文件”,但实际含义变成了“只要行尾是 .txt,任何参数都可以”。
同样的情况也适用于 systemctl 示例。由于参数是作为拼接后的字符串进行比较的,因此末尾的模式也会匹配您在其后追加的任何内容。所以 restart app-* 不仅涵盖了 restart app-api,还涵盖了调用者添加的任何后续参数。参数中的模式会泄露其前后的参数,而命令的权限正是由参数决定的。sudo-rs 拒绝使用这种结构,而不是试图使其变得安全,因为这种结构不存在通用的安全形式。
将通配符替换为明确的命令列表
大多数通配符规则的存在,仅仅是因为有人不想输入四行命令。请直接输入这四行。
Cmnd_Alias APP_RESTART = /usr/bin/systemctl restart app-api, /usr/bin/systemctl restart app-worker
Cmnd_Alias APP_STATUS = /usr/bin/systemctl status app-api, /usr/bin/systemctl status app-worker
deploy ALL=(root) NOPASSWD: APP_RESTART, APP_STATUS确保路径正确。如果系统中的二进制文件位于 /usr/bin/systemctl,而规则却指定了 /bin/systemctl,则该规则永远无法匹配,且其失败表现与权限问题完全一致。请使用 command -v systemctl 进行确认,并粘贴其输出结果。
将规则放入独立的嵌入式(drop-in)文件中,而不是直接修改 /etc/sudoers,这样软件包升级时就不会覆盖你的配置:
sudo visudo -f /etc/sudoers.d/90-deploy
sudo visudo -c
sudo -l -U deploy命名文件时,请勿包含点号或末尾的波浪号。原始的 sudo 会忽略 sudoers.d 中包含点号的文件,因此 90-deploy.conf 是一种典型的静默失效操作,遵循此命名惯例不会产生任何额外成本。
当列表过长时使用 root 拥有的包装器
当允许的集合太大而无法列出时,请将决策逻辑从 sudoers 移出,转而使用一个由 root 拥有的简短程序。
sudo tee /usr/local/sbin/app-restart >/dev/null <<'EOF'
#!/bin/sh
set -eu
case "${1:-}" in
app-api|app-worker) ;;
*) echo "app-restart: not allowed: ${1:-}" >&2; exit 1 ;;
esac
exec /usr/bin/systemctl restart "$1"
EOF
sudo chown root:root /usr/local/sbin/app-restart
sudo chmod 0755 /usr/local/sbin/app-restart
ls -l /usr/local/sbin/app-restart此时 sudoers 端仅需指定一条命令:
deploy ALL=(root) NOPASSWD: /usr/local/sbin/app-restart *此处末尾的 * 是可以接受的,因为是由脚本而非 sudo 来决定允许执行的内容。前提是该脚本必须由 root 拥有且除 root 外任何人均不可写。如果 deploy 可以写入该文件,deploy 就能替换其内容并以 root 权限运行任何程序,这比你删除的通配符规则更危险。使用 ls -l 检查权限模式,如果你看不懂输出结果,学习解读 drwxr-xr-x 权限字符串 只需五分钟。同样的规则也适用于目录:/usr/local/sbin 也不能对该账户开放写权限,因为目录可写意味着其中的文件可以被整体替换。
为任务创建专用账户而非使用 sudo 规则
更好的做法是分析该命令为何必须以 root 权限运行。如果服务以专用用户身份运行,则该用户可自行管理服务,无需配置 sudoers。对于 systemd 单元,systemd 已将此类权限委托给 polkit,因此规则可以指定特定的单元和操作员:
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
action.lookup("unit") == "app-api.service" &&
subject.user == "deploy") {
return polkit.Result.YES;
}
});将上述内容保存为 /etc/polkit-1/rules.d/50-app-api.rules,随后 deploy 即可运行 systemctl restart app-api,完全无需 sudo 权限。请在实际使用的上下文中进行测试,因为在 SSH 会话中有效的规则,必须在 cron 中确认后才能投入生产环境。无论采用何种方式,执行任务的账户都应仅用于该项工作,这与 VPS 上的最小权限用户账户 的原则一致。
sudo-rs 中未实现的功能
sudo -E 未实现。请改用 Defaults env_keep += "HTTP_PROXY HTTPS_PROXY NO_PROXY" 定义所需变量,并注意 env_reset 始终处于开启状态,因此未保留的任何内容都会被清除。
LDAP 中的集中式 sudoers 存储已被移除。sudoers.ldap 和 cvtsudoers 未实现,且 sudo-ldap 软件包已在 26.04 版本中移除。通过 PAM 或 SSSD 进行的 LDAP 身份验证仍然有效。此处排除的仅限于目录策略部分。
INTERCEPT 未实现,该功能旨在阻止从已授权命令中进行 shell 转义。无论如何,它对坚定的用户无效。如果规则允许某人以 root 身份运行编辑器或解释器,他们就拥有了 root 权限,任何 sudo 选项都无法改变这一点。
会话记录未实现,因此不存在 I/O 日志,也没有 sudoreplay。日志仅发送至 syslog,且没有 logfile 选项可将其重定向至其他位置,因此 sudo 消息会记录在系统当前配置的 syslog 目标路径中。
是否应该切回 sudo.ws?
您可以这样做。在 26.04 版本周期内,原始版本会保留在软件包中,正是为了应对这种情况。
sudo apt install sudo.ws
update-alternatives --config sudo
sudo update-alternatives --set sudo /usr/bin/sudo.ws请从 --config 的输出中直接复制路径,不要从本页面复制,因为该列表才是您系统实际接受的路径。后续若要切回 sudo-rs,只需将替代项设置为该列表中对应的 sudo-rs 二进制文件路径即可。
在执行任何影响 sudo 的操作前,请务必保持第二个 SSH 会话处于登录且空闲的状态。如果 sudoers 文件解析失败,或者替代项指向了一个未安装的二进制文件,您将无法在远程服务器上获取 root 权限。这一习惯应与您在 新 VPS 上的前十分钟 所做的所有操作一样,成为您的标准流程。
将切回操作视为一个缓冲期限,而非最终解决方案。这能为您争取一周时间来正确重写规则。重写本身就很有价值,因为您删除的每一条通配符规则,其权限范围往往都超出了原作者的预期。
FAQ
为什么我的 sudoers 通配符规则在 Ubuntu 26.04 上失效了?
因为 Ubuntu 26.04 LTS 将 sudo-rs 选为默认的 sudo 实现,而 sudo-rs 不支持匹配命令参数中的通配符。它仅允许在命令文件名中使用通配符,"" 表示无参数,且仅允许在最后一个参数位置使用单个 *。像 /usr/bin/systemctl restart app-* 这样的规则将模式置于参数中间,因此该规则无法匹配任何内容,命令会被拒绝。请以 root 身份运行 sudo -l -U deploy 查看当前账户实际拥有的权限,然后将规则替换为具体的命令,或使用由 root 拥有的包装脚本。
如何在 Ubuntu 26.04 上切回原版 sudo?
原版 sudo 已打包为 sudo.ws。请使用 sudo apt install sudo.ws 安装它,然后通过 sudo update-alternatives --set sudo /usr/bin/sudo.ws 将其设置为候选项。请先运行 update-alternatives --config sudo 查看系统提供的确切路径,并在执行切换时保持第二个 SSH 会话开启。此操作无法恢复 sudo-ldap,因为它已从 26.04 中移除,无论你选择哪种实现方式。
sudo-rs 是否读取相同的 /etc/sudoers 文件?
是的。sudo-rs 会读取 /etc/sudoers 以及 /etc/sudoers.d/ 下的片段文件,并对用户、组、别名、执行身份规范以及 NOPASSWD 标签使用相同的语法。它实现了 sudoers 语言的一个子集,因此差异主要表现为某些构造缺失,而非构造行为不一致。请使用 sudo visudo 进行编辑,并在关闭会话前使用 sudo visudo -c 进行验证。
sudo-rs 中用什么替代 sudo -E?
sudo -E 未被实现,且在原版 sudo 中也已不被推荐,因为将调用者可控的环境变量传递给 root 进程是改变该进程行为的已知途径。请在 sudoers 中通过类似 Defaults env_keep += "HTTP_PROXY HTTPS_PROXY NO_PROXY" 的行明确指定你实际需要的变量。env_reset 在 sudo-rs 中始终处于开启状态且无法禁用,因此所有未保留的变量都会被清除。