Linux服务器sudo权限如何管理?最小权限配置与审计指南

在 Linux 运维里,最危险的状态不是"没有 root 权限",而是"人人都是 root"。很多团队为了省事,直接把开发、运维、外包人员的账号加进 sudoers 或者干脆让他们共用 root 密码。等到出事——比如某次误删生产库、某个离职员工带走配置、某次凭据泄露——才发现根本查不出是谁干的,因为所有操作都记在同一个 root 名下。

sudo 正是为了解决这个问题而生的:它让你不必共享 root 密码,而是把 root 的能力按需、按人、按命令地临时授予指定用户,并且把每一次提权操作记录到日志里。用好了,它是细粒度权限控制和审计的基石;用不好,一个 ALL=(ALL) NOPASSWD: ALL 就等于给自己开了一扇后门。

这篇文章讲三件事:sudo 的配置文件结构和读取顺序是怎样的,如何按照最小权限原则给不同角色配置不同的提权范围,以及如何做好 sudo 的日志审计和常见坑点规避。文中的配置示例可以直接拿去改,但在生产环境应用 visudo 之前,请务必保留一个可用的 root 通道——改坏 sudoers 会导致所有人无法提权。

一、sudoers 的结构与读取顺序

sudo 的权限判断本质上是一次"规则匹配":谁(用户或组)、在哪台主机上、能以谁的身份、免不免密、能跑哪些命令。这些规则写在 sudoers 文件里。但要注意,真正的配置入口是 /etc/sudoers 和 /etc/sudoers.d/ 目录,后者用于分文件管理,便于版本控制和按角色拆分。

改 sudoers 有一个铁律:只能用 visudo 编辑,不要用 vim 直接改。原因是 visudo 会在保存时做语法检查,语法错了它会拒绝保存;而直接 vim 保存一个语法错误的 sudoers,会导致 sudo 完全不可用。分文件编辑则用:

# 编辑主文件
visudo

# 编辑 /etc/sudoers.d/ 下的分文件,同样带语法检查
visudo -f /etc/sudoers.d/ops

# 只检查语法不编辑
visudo -c

规则的基本语法是这样一条:

用户名 主机名=(可切换的身份) [NOPASSWD:] 命令列表

举例,允许用户 alice 在任何主机上以 root 身份运行 systemctl 命令,且需要输入自己的密码:

alice ALL=(root) /usr/bin/systemctl

如果加上 NOPASSWD: 就是免密执行,这在自动化场景里常见,但也是审计上的盲点,后面会细说。

读取顺序上,/etc/sudoers 里通常有一行 #includedir /etc/sudoers.d,sudo 会按文件名的字典序读取该目录下的所有文件(忽略名字里带 . 或 ~ 的文件)。这个顺序很重要:sudo 在处理一条规则时,是找最后一个匹配的规则,而不是第一个。所以同名规则的先后会互相覆盖。为了避免混乱,建议给分文件用带序号的命名,比如 10-ops、20-deploy、30-audit。

还要清楚默认配置里几个关键行,它们决定了你的整体策略:

# 默认在提权时要求输入用户自己的密码
Defaults env_reset

# 定义哪些组有提权权限(这是最常用的授权方式)
%wheel ALL=(ALL) ALL          # RHEL/CentOS/Rocky 系常见
%sudo  ALL=(ALL:ALL) ALL      # Debian/Ubuntu 系常见

注意 % 开头表示组。把一个用户加进 wheel(RHEL系)或 sudo(Debian系)组,就等于给了全量提权,这是最简单也最粗放的授权方式。最小权限的目标,就是尽量少用这种全量授权。

二、最小权限:按角色设计授权方案

最小权限的核心思想是:只给完成当前工作所必需的最小命令集合,能不给全量就不给全量。下面用几个典型角色说明怎么设计。

只读巡检角色

适合监控、值班、客服等只需要看状态的岗位。给他们看日志、看进程、看服务状态的权限,但不给任何能改变系统的命令。

# /etc/sudoers.d/10-readonly
Defaults:readonly !requiretty
readonly ALL=(root) NOPASSWD: /usr/bin/systemctl status *, \
                              /usr/bin/journalctl, \
                              /bin/cat /var/log/*, \
                              /usr/bin/ss, /usr/bin/df, /usr/bin/free

这里每个命令都写了完整绝对路径和参数范围。绝对路径是必须的,否则 sudo 只匹配命令名,可能被 PATH 劫持利用。参数范围则尽量具体,比如 systemctl status * 只允许 status 子命令。

服务运维角色

需要重启服务、管日志、临时处理磁盘,但不该碰数据和用户管理。

# /etc/sudoers.d/20-ops
ops ALL=(root) /usr/bin/systemctl restart nginx, \
               /usr/bin/systemctl reload nginx, \
               /usr/bin/systemctl restart php-fpm, \
               /usr/bin/journalctl -u nginx, \
               /usr/bin/tail -f /var/log/nginx/*

注意这里没有用通配 * 放开整个 systemctl,因为那样等于允许重启任何服务,范围太大。每多写一个命令名,安全边界就清晰一分。

需要 root shell 的角色

有些运维场景确实需要完整的 root shell(比如紧急排障)。这种情况不要用命令白名单硬凑,而是明确授予 shell,但要配合免密策略和日志审计:

# /etc/sudoers.d/30-admin
admin ALL=(root) /bin/bash

要清醒地认识到:一旦允许某用户运行 /bin/bash、/usr/bin/su、/usr/bin/vim、/usr/bin/find、/usr/bin/less、/usr/bin/awk 这类命令,就等于给了全量 root,因为它们都能在其内部再起 shell 或执行命令。也就是说,把这些命令放进白名单,是一种"看起来最小、实际是全量"的假最小权限。审计时要特别留意这类"逃逸命令"。

常见的"提权逃逸"命令清单,配置白名单时应当排除:

命令 逃逸方式 建议
vim / vi :!bash 起 shell 不要放入白名单
less / more !bash 起 shell 不要放入白名单
find -exec bash \; 不要放入白名单
awk system("bash") 不要放入白名单
python / perl 起交互 shell 慎放,或用受限模式
systemctl edit 子命令可改 unit 限定子命令
tar / rsync 可通过参数执行命令 限定用法
env / dd / tee 配合其他命令提权 慎放

三、NOPASSWD 与 requiretty 的取舍

这两个参数是配置里最容易被误用的。

NOPASSWD: 表示执行该命令免输密码。好处是自动化脚本方便,坏处是一旦该用户的会话被劫持,攻击者可以无摩擦地提权。合理用法是只对真正用于自动化的少数命令免密,而不是对全量命令免密。

requiretty 默认在部分发行版(如老版 RHEL)是开启的,要求 sudo 必须在真实终端里运行,这会阻止自动化脚本和某些远程调用使用 sudo。现在大多数发行版已经默认关闭。如果你的自动化报 "sudo: sorry, you must have a tty to run sudo",可以考虑关闭它,但要知道这降低了"非交互式提权"的门槛。

# 针对特定用户关闭 requiretty
Defaults:deploy !requiretty

# 只对部署脚本用的命令免密
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart app

另一个有用的默认项是 Defaults secure_path,它规定了 sudo 环境下的 PATH,能防止用户通过篡改 PATH 让 sudo 执行到伪造的命令。保持它的默认值(通常是系统标准路径),不要随意加用户目录进来。

四、审计:让每一次提权都留痕

sudo 的审计能力主要体现在两处:一是提权命令被记入 authpriv 日志,二是可以通过插件或 PAM 做更完整的会话记录。

先看基础日志。默认情况下,sudo 会把"谁、在哪、以谁的身份、执行了什么、成功还是失败"写到 /var/log/secure(RHEL系)或 /var/log/auth.log(Debian系)。

# 查看所有 sudo 提权记录
grep -i sudo /var/log/secure        # RHEL/CentOS/Rocky 系
grep -i sudo /var/log/auth.log      # Debian/Ubuntu 系

# 用 journalctl 按时间查看
journalctl -t sudo --since "today"

# 统计每个用户提权次数(经验值:异常突增往往是排查线索)
grep -i "sudo:" /var/log/secure | awk '{print $6}' | sort | uniq -c | sort -rn

基础日志的局限是只记录"执行了哪条命令",不记录命令实际产生的输出。对于高安全要求的场景,需要开启 I/O 日志。现代 sudo 支持 log_input 和 log_output 两个选项:

# /etc/sudoers.d/40-audit
Defaults log_input, log_output
Defaults iolog_dir="/var/log/sudo-io"
Defaults iolog_file="%{seq}"

开启后,sudo 会把每次会话的输入、输出、时序都记录到 /var/log/sudo-io 下,可以用 sudoreplay 回放:

mkdir -p /var/log/sudo-io && chmod 700 /var/log/sudo-io

# 列出某个用户的所有会话
sudoreplay -l admin

# 回放指定会话(把 ID 换成实际会话号)
sudoreplay 000001

开启 I/O 日志会带来额外磁盘占用,经验上每个交互式会话的记录在几十 KB 到几百 KB 量级,日志量大时要注意轮转。配置 logrotate 定期清理,避免把磁盘写满。

审计落地还有两个配套动作。第一,sudo 日志一定要集中化,本地日志容易被入侵者删除或篡改,把它们实时转发到远程日志服务器才可靠。第二,针对关键命令设告警,比如提权执行 useradd、visudo、passwd、rm -rf 这类高危操作时触发通知。

# 一个简化的实时监控示例,配合 logger 或告警脚本使用
tail -F /var/log/secure | grep --line-buffered -iE "sudo:.*(useradd|usermod|passwd|visudo|rm -rf)" \
  | while read line; do
      # 这里换成你的告警方式,比如发邮件、发到 IM
      echo "$line" >> /var/log/sudo-alert.log
    done

五、变更安全与常见坑

sudoers 是最不能改错的文件之一,因为改错之后常规提权通道就断了。几条纪律:

改动前保留一个 root 通道。用 visudo 编辑本身有语法保护,但分文件里写错逻辑(比如把所有组都设成免密)visudo 检查不出来。建议改之前先 cp /etc/sudoers /root/sudoers.bak.$(date +%F),并且确认至少有一个本地 root 会话可用(云控制台 VNC 或物理终端)。

改完后开新会话验证,不要在原会话里直接退出。用 sudo -l -U 用户名 可以让 root 查看某个用户最终生效的 sudo 权限,这是验证配置有没有按预期生效的最快方式:

# root 查看 alice 的生效权限
sudo -l -U alice

# 用户自己查看自己的权限
sudo -l

如果真把 sudoers 改坏了导致无法提权,如果有物理/控制台 root 登录,直接修复即可;如果没有而只有 sudo 通道,那基本只能走救援模式了。这就是为什么"保留 root 通道"是硬要求。

还有一个易错点是分文件的权限。sudoers.d 下的文件建议权限 0440、属主 root:root,否则 sudo 会拒绝加载甚至报错。文件名里不要带点号(.),因为 sudo 会跳过带点的文件。

chmod 440 /etc/sudoers.d/20-ops
chown root:root /etc/sudoers.d/20-ops

常见问题

Q:把用户加进 wheel/sudo 组和写一条 user ALL=(ALL) ALL,有区别吗?
功能上基本等价,都是全量提权。区别在管理方式:走组更省事,走显式规则更清晰。从审计角度,显式规则更能看出"为什么给这个人权限",推荐关键人员用显式规则而非直接加组。

Q:为什么我配了 NOPASSWD 但还是要输密码?
最常见原因是规则匹配顺序:后面还有一条要求密码的规则把前面的覆盖了。sudo 取最后一个匹配项,检查一下 sudo -l -U 用户 的实际生效结果,以及分文件名的排序。

Q:sudo -i 和 sudo -s 有什么区别?
sudo -i 模拟登录,加载目标用户的完整登录环境(包括 profile、路径),相当于"以 root 登录";sudo -s 只是启动一个 shell,保留当前大部分环境变量。要真正模拟 root 环境用 -i,只想临时借权限用 sudo 命令 更合适。

Q:怎么快速找出系统里所有免密提权的配置?

grep -rn "NOPASSWD" /etc/sudoers /etc/sudoers.d/ 2>/dev/null

定期扫一遍,确认每一条 NOPASSWD 都是有意为之,清理掉历史遗留,这是缩小提权攻击面的关键动作。

Q:普通用户能自己看 sudo 日志吗?
默认不能,/var/log/secure 和 auth.log 通常只有 root 可读。这是好事,防止攻击者拿到低权限后清理痕迹。另外记得配置日志远程转发,否则本地日志被删就查不到了。

总结

  • sudoers 只能用 visudo(或 visudo -f)编辑,改前备份并保留一个可用的 root/控制台通道,改后开新会话验证。
  • 遵循最小权限:给角色设计具体命令的绝对路径白名单,能用子命令限定就用子命令,避免 ALL=(ALL) ALL 这种全量授权。
  • 牢记"逃逸命令"(vim、less、find、awk、python、su、systemctl edit 等),把 root shell 级别的命令排除在白名单外,否则"最小权限"是假的。
  • NOPASSWD 只对必要的自动化命令使用;保持 secure_path 默认;分文件命名用数字前缀,权限 0440、不带点号。
  • 审计靠三层:基础 sudo 日志(secure/auth.log)、I/O 会话日志(log_input/log_output + sudoreplay)、日志远程集中化与高危命令告警,三者叠加才算完整。