sudo 演进史:从 Unix su 到现代权限管理
回顾 sudo 从 Unix su 演变至今的历程,对比 doas、polkit 与 run0,并总结常见安全陷阱与最佳实践。

你可能没想到:在终端里输入 sudo 的密码时,屏幕上完全没有任何反馈。没有星号,没有圆点,什么都没有,光标就停在那里。这个设计决定做于 20 世纪 80 年代,四十多年来一直让用户困惑。很多人会以为终端卡死了,于是反复输入密码,或者干脆认为哪里出了故障。Ubuntu 终于决定默认显示星号,结束了长达 46 年的静默密码输入传统。
这个小小的改动其实折射出一个更大的问题:Linux 的权限提升机制是几十年间各种决策拼凑而成的产物,有的很精妙,有的相当值得商榷,而且都背负着向后兼容的包袱。搞清楚我们是怎么走到今天的,有助于你在当下做出更好的安全决策。
在 sudo 之前:su 的时代
Unix 最初的权限提升工具是 su,即“substitute user”(切换用户)。它只做一件事:把整个 shell 会话切换到另一个用户,通常是 root。你输入 su,敲入 root 的密码,完成管理工作后,再退出回到自己的普通账户。
$ su
Password: [type root's password]
# whoami
root
# apt install nginx
# exit
$ whoami
normaluser
随着 Unix 系统的规模超出少数可信运维人员的范围,su 的问题也越来越明显。所有需要管理员权限的人都必须知道 root 密码。有人离职时,你得修改 root 密码,再把新密码告诉其他所有人。系统里也没有审计记录,一旦有人切换成 root,所有操作都会记为“root”,无从得知究竟是谁执行了什么。更糟的是,su 给的是完整的 root shell,这意味着一个手误就可能毁掉整个系统。
经典的惨剧案例:管理员本想输入 rm -rf /tmp/old_files,结果在输入 rm -rf / 之后不小心按了回车。在完整的 root shell 里,这条命令没有任何东西能拦住它执行。su 无法区分“这个人只需要重启 nginx”和“这个人需要对整个系统的无限制访问”。
sudo 如何解决 root 密码问题
sudo(“superuser do”)于 1980 年由 Bob Coggeshall 和 Cliff Spencer 在纽约州立大学布法罗分校创建。其核心思路简单却影响深远:不再共享 root 密码,而是让每个用户用自己的密码,以 root 身份运行被允许的特定命令。系统管理员通过一个配置文件来控制谁能运行什么。
这一设计一次性解决了几个问题:
- 不再共享 root 密码。每个用户使用自己的凭据进行身份验证。有人离职时,只需撤销其 sudo 权限,无需轮换密码。
- 细粒度权限控制。你可以允许开发者重启某个特定服务,而无需给予完整的 root 权限。
- 审计追踪。每条 sudo 命令都会记录执行者的用户名、执行的内容以及执行时间。
- 临时提权。sudo 不会提供一个不受限的 root shell,而是只以提升的权限运行单条命令,之后即恢复为普通权限。
# Run a single command as root
$ sudo apt install nginx
[sudo] password for priya:
# That's YOUR password, not root's
# The audit trail in /var/log/auth.log:
# Mar 20 14:23:01 web-03 sudo: priya : TTY=pts/0 ;
# PWD=/home/priya ; USER=root ; COMMAND=/usr/bin/apt install nginx
sudoers 文件:强大却危险
sudo 的配置文件位于 /etc/sudoers,它的语法堪称 Linux 系统管理中最让人头疼的东西之一。一个语法错误就可能让你彻底无法使用 sudo,因此才有了 visudo 命令,它会在保存前校验文件。
# /etc/sudoers syntax:
# WHO WHERE = (AS_WHOM) WHAT
# Let priya run anything as root on any host
priya ALL=(ALL:ALL) ALL
# Let the 'webdev' group restart nginx only
%webdev ALL=(root) /usr/bin/systemctl restart nginx,\
/usr/bin/systemctl reload nginx
# Let deploy user run deploys without a password
deploy ALL=(root) NOPASSWD: /opt/deploy/run.sh
# DANGEROUS: This looks restrictive but isn't
bob ALL=(root) /usr/bin/vim
# Bob can now run: sudo vim, then :!bash to get a root shell
上面最后那个例子——允许某人通过 sudo 运行 vim——是一个经常让人踩坑的安全隐患。Vim(以及许多其他程序)都能派生出 shell 命令。只要用户能执行 sudo vim,实际上就拥有了不受限制的 root 权限。less、man、awk、find、python 以及其他几十个命令也是同样的道理。GTFOBins 项目整理了一份完整清单,列出了那些可以用来逃逸受限 shell 的二进制程序。
sudo 的怪癖与安全陷阱
sudo 在四十多年的历史中积累了一些相当奇怪的行为。了解这些怪癖对安全至关重要:
- 凭据缓存。输入密码后,sudo 默认会缓存 15 分钟。在此期间,任何 sudo 命令都无需再次认证。如果你离开了一个已解锁的终端,他人可以在最多 15 分钟内执行 sudo 命令。运行
sudo -k可以立即清除缓存。 - tty_tickets 设置。默认情况下,sudo 的凭据缓存是按终端隔离的。但在某些系统上,在一个终端中完成认证后,所有终端的 sudo 都会被解锁。请检查你的
Defaults设置。 - 环境变量。sudo 会清理大部分环境变量,但并非全部。
LD_PRELOAD和LD_LIBRARY_PATH会被剥离,但如果env_keep配置有误,攻击者仍可能注入恶意的库路径。这一问题已成为多个真实提权漏洞的根源。 - NOPASSWD 陷阱。
NOPASSWD对自动化很方便,但如果范围设得太宽就很危险。一个以拥有NOPASSWDsudo 权限的用户身份运行的被攻陷应用,实际上就等同于拥有了 root 权限,而且连密码都不需要破解。
# Security hardening for sudoers:
# Require password every time (disable caching)
Defaults timestamp_timeout=0
# Or reduce cache to 1 minute
Defaults timestamp_timeout=1
# Ensure credential cache is per-terminal
Defaults tty_tickets
# Log all sudo I/O (records full terminal sessions)
Defaults log_input, log_output
Defaults iolog_dir=/var/log/sudo-io
# Require root password instead of user password
# (prevents compromised user accounts from sudo-ing)
Defaults rootpw
# Show asterisks when typing password
Defaults pwfeedback
现代替代方案:doas、polkit 与 run0
sudo 的复杂性催生了多种替代方案,每一种都体现了不同的设计理念。
doas:去掉复杂性的 sudo
OpenBSD 的 doas(“dedicated OpenBSD application subexecutor”)由 Ted Unangst 在 2015 年创建,直接原因是 sudo 已经复杂到难以审计。它的完整配置通常只需两三行:
# /etc/doas.conf — the entire configuration
permit persist priya
permit nopass deploy as root cmd /opt/deploy/run.sh
# That's it. Compare this to a typical sudoers file.
# 'persist' is like sudo's credential caching
# 'nopass' is like NOPASSWD
doas 的源代码大约 2500 行 C 代码,而 sudo 则超过 15 万行。对于不需要 sudo 高级功能(比如按命令参数匹配或 LDAP 集成)的系统来说,doas 在配置、审计和加固方面都要简单得多。我在个人服务器上用了好几年,至今没有怀念过 sudo。
polkit:细粒度的桌面权限
PolicyKit(polkit)采取了完全不同的思路。它不包装单个命令,而是定义应用程序可以请求的操作。当桌面应用需要修改网络设置时,它会向 polkit 申请权限,polkit 则根据规则决定是授予、拒绝,还是弹出提示让用户确认。
这就是为什么你的 Linux 桌面可以无需密码挂载 U 盘,却要求验证身份才能安装软件。它的粒度是操作级别,而不是命令级别,这更符合桌面用户实际的权限思维方式。
run0:systemd 的彻底重构
systemd 的 run0 是最新的成员,它的工作原理与 sudo 从根本上不同。它不是在你现有的会话中以 root 身份运行命令(这需要 setuid 位,并带来各种安全隐患),而是请求服务管理器启动一个以 root 运行的新服务。你的用户会话本身从不获得提升的权限,特权进程是完全独立的。
这种设计绕开了整类 sudo 漏洞:没有 setuid 二进制文件,没有凭据缓存,也没有环境变量注入的攻击面。代价是它依赖 systemd,因此对 BSD 系统和精简的 Linux 环境来说并不适用。但对于基于 systemd 的服务器而言,它可以说是目前最安全的选择。
实用建议
多年管理 Linux 服务器、排查权限提升事件之后,我的真实建议如下:
- 彻底禁用 root 登录。所有操作都通过
sudo或doas完成。不应存在共享的 root 密码。 - 在 sudoers 中按用户组授权,而非按个人。通过组成员关系管理访问权限。有人离职时,把他从组里移除即可。
- 绝不给解释器或编辑器授予 sudo 权限。不要
sudo vim、sudo python、sudo less。如果需要编辑 root 所有的文件,请改用sudoedit。 - 尽量减少 NOPASSWD 规则。仅用于自动化流程,并且只针对使用完整路径的特定命令。
- 开启 sudo 日志。至少记录所有 sudo 命令。对于敏感系统,应启用完整的输入输出日志。
- 简单系统可考虑 doas。如果不需要 LDAP 集成或复杂的匹配规则,doas 更容易配置正确。
- 积极缩短凭据缓存时间。将
timestamp_timeout调整为 1 到 5 分钟,或在生产服务器上直接禁用缓存。
最好的权限提升机制,是能够以所需的最小权限、在所需的最短时间内完成操作,并留下完整审计记录的机制。其他方案都是妥协。
sudo 并不会消失。它已经深深嵌入到脚本、自动化流程、文档和肌肉记忆之中,不可能轻易退出历史舞台。但替代方案的生态比以往任何时候都更健康。无论你是坚持使用经过妥善加固的 sudo,切换到更简洁的 doas,还是为安全要求高的系统采用 run0,关键在于真正理解你所用的权限提升工具究竟做了什么。因为人们以为 sudo 做的事情,与它实际做的事情之间的差距,正是大多数安全事件的源头。


