
如果有一天你的服务器突然没有人能执行 sudo那不是黑客入侵八成是有人手滑改了/etc/sudoers。我见过不止一次这样的现场线上服务器/etc/sudoers被某位老哥用普通文本编辑器改出一个语法错误然后公司全员都无法提权连 root 都进不去。这类事故在 Linux 运维里属于最尴尬的那一种因为/etc/sudoers是 sudo 命令的唯一信任根它能把你从普通用户变成 root也能把所有人都锁在门外。这篇文章就从这个文件出发把它的设计原理、语法规则、实际配置和排错经验完整过一遍。无论你是刚入门 Linux 的开发者还是已经管了一段时间服务器的运维只要你想搞懂 sudo 为什么这么设计、怎么配置才安全、出了问题怎么救这篇内容应该都能给你一份很实用的参考。1. sudoersLinux 提权体系的真正核心很多人刚学 Linux 的时候觉得 root 密码就是一切只要登录进去想干嘛干嘛。但真正到了生产环境root 密码往往被放在保险柜里日常操作全部靠 sudo。这时候/etc/sudoers就成了整个权限体系最核心的开关它决定了谁能用 root 的权限、能用多少、能用到什么时候。1.1 为什么不是直接编辑 /etc/sudoers而是用 visudo这是新手最容易忽略的问题。既然/etc/sudoers是个文本文件为什么不能直接用 vim 打开改答案是sudo 读取这个文件时对语法正确性要求极高只要有一行配置写错比如少了个冒号、多加了个空格、括号没闭合sudo 立马罢工任何用户都无法提权。而visudo这个命令在保存退出前会做一次完整的语法校验$ sudo visudo如果语法有问题它会明确告诉你第几行出错并且拒绝保存同时询问你是否要强制保存。我在实际工作中见过很多用普通编辑器改 sudoers 导致全站提权失败的案例根因基本都是绕过了 visudo 的校验。因此我的建议很直接编辑 sudoers 永远用 visudo没有例外。1.2 sudoers 的属主和权限位为什么必须是 root:root 0440用ls -l /etc/sudoers看一下正常的输出应该是-r--r----- 1 root root 2097 Feb 20 10:15 /etc/sudoers属主是 root属组也是 root权限是 0440。这组权限有它的道理sudo 本身是一个 setuid root 程序运行时会读取/etc/sudoers但 sudo 不会像普通程序那样“谁写的都信”。它要求这个文件必须由 root 拥有并且不能对组和其他用户开放写权限。如果权限被错误改成 0777sudo 会直接拒绝使用这个文件并提示/etc/sudoers is world writable。这里有个很容易被忽视的坑很多人喜欢用chmod去修 sudoers 的权限结果越修越糟。正确的做法是重新执行$ sudo chown root:root /etc/sudoers $ sudo chmod 0440 /etc/sudoers但要注意如果当前 sudo 已经因为权限问题不可用了你需要用pkexec或者物理机救援模式去恢复这就涉及 4.1 里会更细的恢复方案。1.3 一句话说清 sudo 与 su 的本质差异很多初学者分不清sudo和su其实它们不是一回事。su的全称是 substitute user意思是切换成另一个用户通常需要输入目标用户的密码一旦成功就进入对方的 shell。而sudo是 superuser do它的重点不是“切换用户”而是“用另一个身份执行这一条命令”。举一个最简单的生活类比su就像你把自己的钥匙给了所有人谁拿到都能进房间sudo则是给不同的人发不同等级的工牌技术员只能进机房财务只能进账房谁想多进一个房间都要单独审批。这种细粒度控制正是现代 Linux 服务器推荐用 sudo 而不是把 root 密码给所有人的原因。2. sudoers 语法拆解权限规则背后的四要素理解了 sudoers 是信任根接下来更重要的是看懂里面的每一行规则到底在说什么。sudoers 的语法看起来很抽象但只要抓住四个要素基本就能读懂了。2.1 规则行的完整结构user host(runas) commandsudoers 里最常见的一条规则长这样zhangsan ALL(ALL:ALL) ALL拆开来看zhangsan用户名或者用户组用户组用%开头比如%admin。ALL主机名或 IP表示规则在哪些主机上生效。这个是为了配合多主机共享同一份 sudoers 文件设计的。(ALL:ALL)可以切换到的目标用户和目标用户组。左边是用户右边是组。如果只写(ALL)就表示只能以任意用户身份执行但组保持默认。最后一个ALL允许执行的命令列表。不能用相对路径必须是完整绝对路径。所以这行规则的意思是用户zhangsan可以在任何主机上以任意用户身份执行任意命令。很多默认安装的系统会给%sudo用户组配置这样一条规则这也是新装的 Ubuntu 里第一个用户就能 sudo 的原因。理解了这个结构再往下写规则就有章法了。比如你想让 zhangsan 只能用 root 身份重启 nginx可以写成zhangsan ALL(root) /usr/bin/systemctl restart nginx注意这里命令写了systemctl restart nginxsudoers 会按完整的字符串匹配如果用户执行systemctl reload nginx这条规则是不匹配的。如果你只写/usr/bin/systemctl那就等于允许 systemctl 带任意参数风险完全不同。2.2 别名机制User_Alias、Cmnd_Alias、Host_Alias当服务器数量和用户数量上升sudoers 里逐行堆规则会变得很难维护。sudoers 提供了三种别名机制用户别名User_Alias、命令别名Cmnd_Alias、主机别名Host_Alias。最常见的用法是把一组常用命令定义成命令别名比如Cmnd_Alias SERVICE_CMDS /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx, /usr/bin/systemctl restart mysql Cmnd_Alias DISK_CMDS /usr/sbin/fdisk, /usr/sbin/mkfs.ext4 ops-team ALL(root) SERVICE_CMDS ops-team ALL(root) DISK_CMDS这样管理的优势非常明显如果后期要新增一个服务重启命令只需要在SERVICE_CMDS里追加一条所有引用这个别名的用户自动获得权限不用重复修改几十行规则。别名必须大写这是 sudoers 语法的强制要求写小写会直接报错。2.3 标签TagsNOPASSWD、PASSWD、SETENV、NOEXECsudoers 除了定义“能不能执行”还能定义“怎么执行”。标签写在命令列表的前面用冒号和逗号组合jenkins ALL(root) NOPASSWD: /usr/bin/git pull, /usr/bin/systemctl restart app这条规则让 jenkins 用户执行这两个命令时不需要输入密码适合自动化任务。反过来如果某条命令想要强制输密码可以用PASSWD覆盖它的默认行为。默认情况下如果前面有NOPASSWD后面的命令都会继承免密属性除非你再标记为PASSWDjenkins ALL(root) NOPASSWD: /usr/bin/git pull, PASSWD: /usr/bin/systemctl restart app除了 NOPASSWD还有两个标签值得熟悉SETENV允许用户在 sudo 时通过sudo VARvalue command设置环境变量默认 sudoers 策略是禁止这么做因为环境变量是提权攻击的高风险入口。NOEXEC禁止被执行的命令再调用其他程序适用于某些可能进入交互式 shell 的工具。标签的本质是给命令增加了一层策略限制在写规则时多想想“这种权限是不是一次性、是否可被滥用”就能挑出合适的标签。2.4 通配符与命令匹配的实际陷阱sudoers 支持通配符而且语法上和 shell 的通配符类似。比如www-data ALL(root) /usr/bin/tail -f /var/log/nginx/*这个看起来很合理但实际有坑sudoers 的通配符不会递归匹配子目录而且*可以匹配/。也就是说/var/log/nginx/*能匹配/var/log/nginx/access.log但也能匹配/var/log/nginx/../../etc/passwd。虽然 sudo 作为 root 执行时对路径有解析但历史上确实出现过利用通配符绕过命令限制的案例。更安全的做法是尽量避免在命令规则里用通配符特别是文件路径如果确实需要尽量把路径限制到明确目录并用sudoedit代替编辑器。sudoedit是一个特殊的命令它会自动处理临时文件权限避免用户在编辑文件时通过编辑器提权。后面实战部分我还会再提到它。3. 从最小化原则设计 sudo 规则常用场景配置实战很多人把 sudoers 当成一个“放行列表”想到什么命令就加什么最后权限越来越大。正确的姿势应该是反过来先考虑某个人/某个角色最需要哪几条命令再把这几条命令写进 sudoers。这就是最小权限原则。下面我用几个实际场景来演示。3.1 给开发同学配置特定服务的重启权限假设公司有一个名叫 dev 的开发他负责的模块部署后需要重启 nginx。运维不可能把 root 密码给他但也不想让他每次重启都来找人。这时可以在/etc/sudoers.d/下面新建一个文件比如/etc/sudoers.d/dev-nginx内容写dev ALL(root) /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx然后执行sudo visudo -c检查语法。由于命令列表是精确匹配dev 不能通过 systemctl 去做 create、enable 等操作这就把权限收得很紧。不过这里有一个很容易踩的坑有些系统上 systemctl 的实际路径不是/usr/bin/systemctl而是/bin/systemctl如果写错路径规则永远匹配不上。可以用which systemctl查一下真实路径。3.2 让运维账号免密执行只读命令很多监控系统需要用普通用户身份去查看系统状态但又不希望每次都输密码。假设有一个monitor用户想让它免密执行ss -tlnp、df -h、uptime等命令可以这样写monitor ALL(root) NOPASSWD: /usr/sbin/ss -tlnp, /usr/bin/df -h, /usr/bin/uptime放到/etc/sudoers.d/monitor文件里再跑一遍visudo -c。这里有一个细节ss命令有时候在/usr/sbin/ss有时候在/usr/sbin/ss取决于发行版。如果路径不对monitor 执行sudo ss -tlnp会提示 command not allowed。所以我的习惯是写完规则后马上切到该用户验证$ sudo -u monitor sudo -l这样能直接看到 monitor 用户被授权了哪些命令避免部署完才发现规则没生效。3.3 限制普通用户使用 root shell 的五种写法有些场景下你希望用户能做一些管理操作但绝对不能让他们获得 root shell。下面几种写法风险是递增的完全不定制默认ALL不推荐。只允许特定命令比如只允许 restart nginx完全不开放/bin/bash或/bin/sh。允许使用sudo -i但限制为只能执行单条命令例如user ALL(root) /bin/su -c some_command但这条其实很容易被绕过。禁止sudo su -在 sudoers 里不要给su以 root 身份执行的权限。用NOEXEC标签让用户在 sudo 下无法执行任何子进程相当于把 shell 关掉。最安全的是方案 2只列命令不给 shell。如果有人真的需要交互式管理员环境应该走独立的运维审计系统而不是直接发 root shell。别觉得这很麻烦生产环境里大量安全问题都出在“只是临时用一次 root shell”上。3.4 使用 sudoers.d 拆分管理可维护性优于一切/etc/sudoers主文件一般不要乱动更推荐的做法是在/etc/sudoers.d/目录里按角色或按人拆分文件。Ubuntu 和 CentOS 默认主文件末尾都会有这样一行#includedir /etc/sudoers.d凡是这个目录下的文件只要属主是 root、权限不是 0440 且有规则都会被 sudo 自动加载。这带来的好处是改动某个业务线权限时不会影响全局也方便用版本管理工具单独跟踪某个文件的变化。要注意两个约定目录里的文件名不能包含.比如dev-nginx可以dev.nginx不会被加载文件权限同样必须是 0440否则 sudo 会忽略它并报错。我习惯在文件名里直接体现角色比如20-dev-team、30-monitor数字前缀便于控制加载顺序。4. 我踩过的 sudoers 坑语法错误、环境变量与 secure_path配置 sudoers 最怕的不是规则不符合需求而是你根本不知道它哪里错了。下面的几个坑基本每一个我都实际遇到过有些还引发过线上事故。4.1 一个空格引发的信任链崩溃——语法错误的后果与恢复手段有一次我在一台 CentOS 7 上给用户添加规则手滑把一个逗号打成了分号并且用的是普通编辑器保存然后整个 sudo 全部失灵。任何用户执行sudo都会收到 /etc/sudoers: syntax error near line 9 sudo: parse error in /etc/sudoers这时候即便再想运行sudo visudo也没用了因为 sudo 本身已经不可用。恢复的办法通常有几种如果当前环境有pkexec可以用它提权到 root再执行pkexec visudo修复。如果没有 pkexec在虚拟化平台上可以挂载救援模式或者用init/bin/bash等方式进入单用户模式来修复文件。如果有定时备份直接恢复/etc/sudoers及其目录下文件。所以这里强烈建议每次修改之前先备份一份$ sudo cp /etc/sudoers /etc/sudoers.bak这个习惯成本极低但能救命。还有一点线上服务器如果改动 sudoers改完后立刻执行$ sudo visudo -c输出/etc/sudoers: parsed OK才算真正安全。4.2 sudo 下找不到命令secure_path 与 PATH 的博弈“我用 sudo 执行一个自定义脚本报 command not found但我明明装好了”是另一个高频问题。这其实是 sudo 默认重置环境变量造成的。sudoers 里有这样一行默认配置Defaults secure_path /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin当用户执行sudo时PATH 会被重置成这个安全路径你自己在.bashrc里加的自定义路径全部失效。解决办法有三个最稳妥把脚本所在目录写进规则时使用绝对路径比如/usr/local/bin/my-script。修改 secure_path 追加路径不建议全局改因为安全路径的作用就是防止用户通过篡改 PATH 欺骗 sudo 加载恶意程序。使用sudo env PATH$PATH临时带着当前 PATH 执行但这同样有安全风险。实际生产里我更推荐把脚本统一放到/usr/local/bin或/usr/local/sbin下面并不额外修改 secure_path。4.3 sudo 执行带通配符的命令被拒目录级权限的坑之前提到通配符有匹配问题这里讲一个更具体的例子。有人配置了admin ALL(root) /bin/rm -rf /data/backup/*然后管理员执行$ sudo rm -rf /data/backup/*.tar.gz结果被拒绝command not allowed。原因是 sudoers 的匹配不是正则而是 shell 风格的模式*能匹配任意字符但整个命令行必须和配置的模式完全一致。/data/backup/*.tar.gz和/data/backup/*并不匹配所以规则失效。更安全的做法是把约束放到目录结构上或者干脆不要在 sudoers 里允许rm -rf而是封装成一个有参数校验的自定义脚本再授权。4.4 为什么我建议永远不要用 vi 编辑 sudoers我没有否定 vi 本身而是说不要在编辑 sudoers 时直接用vi /etc/sudoers。原因是 vi 保存时不会做任何 sudoers 语法检查一旦写错代价非常大。即使visudo也是默认调用 vi它和裸 vi 的区别在于保存后会自动跑语法检查、文件锁和权限校验。如果你确实不喜欢 vi可以用环境变量让 visudo 调用其他编辑器比如$ sudo EDITORnano visudo还有一些团队把编辑器预检脚本做成 hook在保存之前跑visudo -c进一步防止错误。不管怎么配置核心是编辑 sudoers 必须经过 visudo 这一层不能绕过。5. 调试与审计让 sudoers 从“黑盒”变成“白盒”很多 Linux 管理员把 sudoers 配置完后就不管了直到某次权限诡异才想起来查。其实 sudo 本身提供了一整套检查和审计工具学会它们能让你少踩很多坑。5.1 sudo -l 检查当前用户的实际权限想知道当前用户能在 sudo 下做什么最简单的是$ sudo -l它会列出匹配该用户的所有规则包括命令、别名、标签。如果规则里用了 NOPASSWD它也会显示。这个命令在生产排障里非常好用比如开发说“我执行 sudo xxx 报 command not allowed”你可以先sudo -l看规则是否真的包含那个命令。5.2 sudo -U otheruser -l 查看其他用户的有效权限如果你有 root 或 sudo 权限还可以检查其他用户$ sudo -U zhangsan -l这个命令对“帮别人排查”非常有效。有时候用户自己看不了自己的权限因为涉及安全策略但管理员可以统一查看。5.3 使用 visudo -c 做配置静态检查visudo -c是每次修改完 sudoers 之后必须执行的一条命令。它会解析所有相关文件检查语法、权限位、文件属主。如果全部正常会看到/etc/sudoers: parsed OK /etc/sudoers.d/README: parsed OK /etc/sudoers.d/monitor: parsed OK如果某个文件权限不对它也会提示警告。我习惯定期执行一次顺便把异常文件找出。5.4 翻 /var/log/auth.log 排查 sudo 失败的常见原因在 Debian/Ubuntu 上sudo 的审计日志放在/var/log/auth.logCentOS/RHEL 则看/var/log/secure。排查问题时可以直接搜关键字$ sudo grep sudo /var/log/auth.log | tail -50常见输出和含义如下日志内容含义user not in sudoers用户根本不在任何授权规则里incorrect password attempt密码错误或连续输错被锁定command not allowed命令不在授权列表里规则未匹配conversation failedPAM 验证异常可能是会话问题timestamp too far in the past时间戳缓存过期需要重新输密码把日志和sudo -l对照起来几乎能解决 80% 的 sudo 权限问题。5.5 sudo 命令详查时间戳缓存、TTY、环境变量残留sudo 默认有 15 分钟的凭证缓存时间由timestamp_timeout控制。这个参数在 sudoers 的 Defaults 一节里可以改但建议不要改成 0否则自动化脚本会频繁输密码也不建议改太大因为短时间内提升了提权窗口风险。另一个常被问到的是requiretty。历史上内核会对“必须要有 TTY 设备”做检查某些自动化工具在非 TTY 环境执行 sudo 会被拒绝。现代系统默认已经放宽但如果你的环境对此有要求可以显式配置或关闭Defaults !requiretty最后是关于环境变量。sudo 在默认情况下会重置掉危险变量比如 LD_PRELOAD、LD_LIBRARY_PATH。如果你在写扩展或调试时发现这些变量在 sudo 下总是消失其实这是安全设计不要强行放行。6. 从体系角度加固 sudo安全最佳实践sudoers 的配置不是一个静态工作它应该随着组织角色、服务器数量、安全要求不断调整。最后这部分是我在长期运维中总结的几个最佳实践。6.1 始终使用最小权限原则最小权限原则不仅适用于“给用户安排角色”也适用于“给命令安排参数”。能用 reload 就不用 restart能用精确命令就不用通配符。每次写规则前问自己一句用户真的需要这条命令吗如果只是偶尔用宁可走审批流程也不要在 sudoers 里留永久后门。6.2 用 Cmnd_Alias 管理命令集合前面已经说过别名机制这里再强调一遍把所有同类命令收敛到别名里语义化命名比如NGINX_CMDS、DOCKER_CMDS。这样做最大的好处是审计时一眼能看出某个角色拥有哪些能力而不是散落在几百行规则里。我见过很多服务器/etc/sudoers里堆了几十条规则最后谁也说不清哪些命令被放行了这种状态本身就是安全隐患。6.3 禁止 root 直接 SSH但允许 sudo现代 Linux 服务器推荐的做法是关闭 root 用户的 SSH 直接登录至少在 sshd_config 中设置PermitRootLogin no然后所有管理员先用普通用户登录再通过 sudo 提权。这样每次管理操作都会留下日志并且即使普通用户被攻破攻击者也需要再获得 sudo 权限才能造成更大的破坏。此策略必须配合一个健壮 sudoers 规则体系否则管理员会被逼到到处用 su。6.4 审计和保存 sudo 日志sudo 默认会通过 syslog 记录日志但这只保证“有日志”。生产环境里建议配置独立的认证日志服务器集中保存或在 sudoers 里添加一行Defaults log_output, log_input这个标签会把 sudo 会话的输入输出全部记录到/var/log/sudo-io/目录后续可以通过 sudoreplay 回放。这对追踪“谁在 sudo 里执行了什么”非常有价值。6.5 周期性 review 和自动化检查 sudoers维护 sudoers 不应该等人出了事故再做。我建议每个季度做一次 review重点看三件事用户是否还在职、命令别名里的命令是否仍然需要、通配符是否出现了预期之外的匹配。也可以用脚本定期跑visudo -c和sudo -l汇总把规则变化纳入变更管理平台。自动化检查的意义是让权限配置像代码一样具备版本历史和可回滚性。另外有一点我一直想强调sudoers 是给“人”用的但现代自动化平台往往需要更标准的认证授权中心。如果服务器规模到了上百台就不适合在每台机器上手改 sudoers 了应该考虑集中配置管理工具去统一下发规则。这时候/etc/sudoers.d/这个拆分机制会成为自动化工具最好的落点规则文件由配置管理系统生成既统一又可控。最后再分享一个我个人的习惯改完任何 sudo 相关配置都要立刻打开一个新终端测试一遍而不是在原会话里“感觉没问题”。因为 sudo 的某些限制是会保留在一个会话里的新终端更能反映真实权限情况。很多线上问题其实都出在“懒得测试”这一步。