深度解析塑造未来的技术文章。

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 对自动化很方便,但如果范围设得太宽就很危险。一个以拥有 NOPASSWD sudo 权限的用户身份运行的被攻陷应用,实际上就等同于拥有了 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 服务器、排查权限提升事件之后,我的真实建议如下:

  1. 彻底禁用 root 登录。所有操作都通过 sudo 或 doas 完成。不应存在共享的 root 密码。
  2. 在 sudoers 中按用户组授权,而非按个人。通过组成员关系管理访问权限。有人离职时,把他从组里移除即可。
  3. 绝不给解释器或编辑器授予 sudo 权限。不要 sudo vim、sudo python、sudo less。如果需要编辑 root 所有的文件,请改用 sudoedit。
  4. 尽量减少 NOPASSWD 规则。仅用于自动化流程,并且只针对使用完整路径的特定命令。
  5. 开启 sudo 日志。至少记录所有 sudo 命令。对于敏感系统,应启用完整的输入输出日志。
  6. 简单系统可考虑 doas。如果不需要 LDAP 集成或复杂的匹配规则,doas 更容易配置正确。
  7. 积极缩短凭据缓存时间。将 timestamp_timeout 调整为 1 到 5 分钟,或在生产服务器上直接禁用缓存。

最好的权限提升机制,是能够以所需的最小权限、在所需的最短时间内完成操作,并留下完整审计记录的机制。其他方案都是妥协。

sudo 并不会消失。它已经深深嵌入到脚本、自动化流程、文档和肌肉记忆之中,不可能轻易退出历史舞台。但替代方案的生态比以往任何时候都更健康。无论你是坚持使用经过妥善加固的 sudo,切换到更简洁的 doas,还是为安全要求高的系统采用 run0,关键在于真正理解你所用的权限提升工具究竟做了什么。因为人们以为 sudo 做的事情,与它实际做的事情之间的差距,正是大多数安全事件的源头。