ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Linux安全提权:普通用户执行root脚本的sudo与setuid全解析

Linux安全提权:普通用户执行root脚本的sudo与setuid全解析 我是一个在服务器上折腾了七八年的老运维今天想好好聊聊 Linux 权限管理里一个非常实用、但也经常被搞出安全问题的操作如何让普通用户安全执行 root 权限的特定脚本。很多人一遇到“普通用户要执行管理员操作”的需求第一反应就是给账号加 sudo甚至直接把用户加进 sudoers 白名单。但这在真实的生产环境里其实是个很危险的做法。我见过不止一次因为 sudo 权限给得太宽一条误操作命令直接把线上服务搞挂或者把系统文件权限改乱。今天这篇文章我就结合自己踩过的坑把sudo 规则精确授权、setuid 位、策略执行封装、以及几种安全兜底方案全部拆开讲清楚。不管你是刚入行的运维新人还是已经带团队的老手这套思路都值得收藏。文章会从需求分析开始讲清楚什么时候该用哪种方案再给出具体可复制的配置示例最后把我实际运维中遇到的高频问题排查经验也一并分享出来。内容偏实操适合对 Linux 用户权限、脚本执行、安全基线有一定基础但又想更进一步理解“最小权限原则”怎么落地的朋友。1. 需求拆解你到底需要哪种“临时提权”很多朋友一上来就问“怎么让普通用户执行 root 脚本”但实际需求往往分成好几类。如果需求没拆清楚后面方案选型就会很混乱。1.1 四种最常见的真实场景我在实际工作中遇到过这么几种典型情况基本覆盖了绝大多数“普通用户需要 root 能力”的需求场景 A运维脚本定期执行。比如每天凌晨要跑一个备份脚本脚本里需要读取只有 root 能访问的配置文件或者需要操作系统级别的日志目录。这个脚本是固定的、内容可控的普通用户不会去改它。场景 B业务程序需要特定系统调用。比如某个监控采集程序需要读取其他用户进程的 CPU 内存占用或者需要绑定低端口。这类程序是编译好的二进制普通用户直接运行会报权限错误。场景 C开发同学需要重启某个服务。团队里开发人员经常需要重启 Web 服务、查看服务状态但又不应该让他们随意改系统配置、看其他敏感文件。场景 D完全不确定脚本内容只是想让某个用户“能执行管理员操作”。这种情况最危险也最不建议直接授权。1.2 选型前必须先想清楚的三件事在确定技术方案之前我建议你先问自己三个问题第一脚本内容是否完全可信如果这个脚本会被普通用户编辑或者脚本内部会动态拼接命令那么任何形式的提权都是在给你的系统开门。一旦脚本被注入恶意命令等于把 root 权限直接交了出去。第二提权范围是“单条命令”还是“整套流程”如果只是执行一个固定的二进制程序用 sudo 精确到程序路径就够了。如果需要执行一个脚本但脚本内部又会调用其他命令那这些被调用的命令也必须在授权范围之内否则 sudo 会拦截失败。第三这个权限是长期有效还是临时借用长期有效的需求适合 sudoers 精确配置或者 sudo 封装脚本。临时借用可以考虑一次性授权、或者运维平台临时审批。提示任何时候都不要为了图省事直接把普通用户加入 root 组或者给一个ALL(ALL) ALL的 sudo 规则。这基本等于把系统管理员权限复制了一份给业务用户出了事连审计日志都很难定位。想清楚这三点之后下面这些具体方案才有意义。否则你只是把一个危险的权限换了一种危险的给法。2. 最稳妥的日常方案sudoers 精确规则授权sudo 是 Linux 系统自带的提权工具本身设计得很安全但前提是你得会用。绝大多数安全问题都是因为规则写得太大、太模糊。2.1 sudoers 文件应该怎么改sudo 的配置文件在/etc/sudoers正规做法是不要直接编辑这个文件而是用visudo命令。visudo 会在保存前做语法检查如果格式写错它会阻止你保存避免搞出“sudo 全线不可用”的悲剧。我习惯在/etc/sudoers.d/目录下为不同需求单独建文件。比如给某个运维用户授权重启 Web 服务我会新建一个/etc/sudoers.d/web-admin内容如下# 允许 user1 以 root 身份无密码执行 systemctl restart nginx user1 ALL(root) NOPASSWD: /usr/bin/systemctl restart nginx这里解释一下每一段的含义user1被授权的用户名。ALL允许该用户从任何主机登录执行也可以限制为某个内网 IP后面我会讲。(root)允许切换到的目标用户身份是 root。NOPASSWD:执行时不需要再输入密码。如果去掉这项执行时需要输入用户自己的密码安全性更高一点但频繁操作时会比较烦。最后一段具体允许执行的命令和参数。参数必须写完整否则 sudo 会按模糊匹配处理存在被绕过风险。2.2 脚本授权的两种写法别用错如果普通用户需要执行的是一个脚本文件而不是系统命令sudoers 的写法就要格外小心。正确写法推荐user1 ALL(root) NOPASSWD: /usr/local/bin/backup.sh这种写法授权的是“执行这个脚本文件本身”。脚本内部调用什么命令sudo 不会再管。所以前提是脚本内容可信、且普通用户不能编辑它。错误写法千万别用user1 ALL(root) NOPASSWD: /bin/sh /usr/local/bin/backup.sh这种写法的意思是“允许以 root 身份执行/bin/sh并且参数是 backup.sh”。但问题在于sudo 对参数的控制非常弱用户完全可以换成/bin/sh /etc/passwd或者任意其他文件甚至直接执行/bin/sh进入一个 root shell。等于你把一把万能钥匙给了别人锁还没换。2.3 关键参数command 部分为什么必须写到底sudoers 的一个核心设计就是“绝对路径 完整参数”。我给你举个我真实见过的反面例子。某团队在 sudoers 里写了这样一行dev ALL(root) NOPASSWD: /usr/bin/systemctl restart nginx注意这里是/usr/bin/systemctl restart nginx没有通配符看起来挺安全。但实际上问题不在这一行而在于 dev 用户还可以执行其他 systemctl 子命令吗不会sudo 会拦截。真正的问题出在如果脚本路径写得是/usr/bin/systemctl *那用户就能执行systemctl stop firewalld、systemctl disable sshd甚至systemctl reboot。这就不再是“允许重启 Nginx”了而是“允许管理系统上所有服务”。所以我的经验是sudoers 里的命令路径全部写绝对路径参数不允许出现通配符除非你非常清楚自己在做什么。另外建议定期用sudo -l查看当前用户的 sudo 权限确认实际授权范围。2.4 限制来源 IP 和登录方式如果你的服务器有运维跳板机或者只允许内网访问那么 sudoers 里最好也把来源地址限制住。例如user1 192.168.10.0/24(root) NOPASSWD: /usr/local/bin/backup.sh这样即使跳板机被攻破攻击者也只能从该网段发起提权至少多了一层限制。另外很多系统管理员喜欢给普通用户配置 SSH 密钥登录后再配合 sudo 使用。我建议普通用户的 SSH 登录尽量禁止 root 直接登录而是用普通用户登录后按需使用受限 sudo 执行任务。这样审计日志里能明确看到是谁、在哪台机器、用了哪条 sudo 命令。3. 让普通用户执行“你自己的脚本”sudo 策略封装有时候你需要普通用户执行的不只是一个简单命令而是你自己写的一整套运维脚本脚本里有大量逻辑判断、命令调用和日志输出。这种情况下直接用 sudo 授权脚本本身虽然可行但审计和管理都不太方便。我来分享一个更可控的封装思路把需要提权的操作写成独立脚本再通过 sudo 只暴露一个“入口 wrapper”最后在 wrapper 里做二次校验。3.1 一个典型的“备份提权 wrapper”示例假设我们要实现普通用户 user1 可以执行/opt/scripts/backup_system.sh这个脚本内部会调用tar、rsync、systemctl等命令。第一步先写好真正的提权脚本路径设置为 root 所有# /opt/scripts/backup_system.sh #!/bin/bash set -euo pipefail LOG_FILE/var/log/backup_$(date %F_%H%M).log echo [$(date)] backup start $LOG_FILE tar -czf /backup/web.tar.gz /var/www/html rsync -av /backup/ user1backup-server:/backup/ systemctl reload nginx echo [$(date)] backup done $LOG_FILE第二步在 sudoers 里授权这个 wrapperuser1 ALL(root) NOPASSWD: /opt/scripts/backup_system.sh第三步普通用户执行sudo /opt/scripts/backup_system.sh这样做的好处是普通用户只能执行这一个入口无法直接调用内部的 tar、rsync 或 systemctl。即使他临时想看脚本内容也没问题但没有执行权限去改脚本。我们再把脚本文件权限设置成 root:rootchmod 750普通用户可以执行但不可写安全性大大提高。3.2 脚本内部安全性不要让命令参数被用户拼接有人可能会问如果我在脚本里支持用户传入参数比如指定备份目录那 sudo 授权还能安全吗答案是可以但必须非常小心。最常见的安全漏洞是“参数注入”。例如脚本这么写#!/bin/bash tar -czf /backup/web.tar.gz $1用户执行sudo /opt/scripts/backup_system.sh /var/www/html; rm -rf /如果脚本里没有对参数做校验就会出现灾难。我的建议是对于底层脚本尽可能不接收用户原始输入。如果必须接收也要做严格的白名单校验比如只允许目录路径位于某个固定前缀下并且过滤掉所有特殊字符。具体可以用正则匹配if [[ $1 ~ ^/var/www/[a-zA-Z0-9_/-]$ ]]; then : else echo invalid path 2 exit 1 fi3.3 为什么我强烈建议“普通用户不可写脚本”很多人为了图方便把提权脚本放到用户家目录或者权限设置成了 777。这是大忌。试想一下如果普通用户可以修改这个提权脚本那他就等于可以修改“以 root 身份运行的逻辑”。他只要在脚本里加一行useradd attacker或者chmod 777 /etc/shadow下一次执行 sudo 脚本时就是以 root 权限完成了恶意操作。所以脚本文件本身必须是 root 拥有权限至少750最好700然后通过 sudo 规则单独授权给指定用户。这样普通用户只有“执行权”没有“修改权”。注意如果你用 sudo 执行的是一个 shell 脚本sudo 的secure_path配置也要检查一下。/etc/sudoers里默认有secure_path它决定了 sudo 环境下 PATH 内容。如果脚本内调用的命令路径在默认 PATH 之外脚本会执行失败或者从用户自定义 PATH 中加载恶意同名命令。建议在脚本开头写死命令绝对路径或者保持系统默认 PATH。4. 少用但必须会setuid 位与辅助工具对比除了 sudoLinux 下还有几种可以让普通用户执行特权程序的办法包括 setuid 位、pkexec、以及通过守护进程调用。它们各自有适用场景也有明显的坑。4.1 setuid 位是什么什么时候能用setuid 位是一个特殊的文件权限位。当一个可执行文件设置了 setuid 位那么任何用户执行这个文件时进程的有效用户 ID 会临时变成文件属主。如果文件属主是 root普通用户运行它时这个进程就拥有了 root 权限。举个例子/usr/bin/passwd就是典型的 setuid 程序普通用户能用它修改自己的密码而修改密码需要写/etc/shadow普通用户本身没有这个权限。我们也可以设置自己的脚本chmod 4755 /opt/scripts/backup_system.sh但这里有个大坑对于 shell 脚本Linux 内核出于安全考虑在很多发行版上会忽略 setuid 位。也就是说你给.sh文件设置4755运行时仍然是以调用者的权限执行的根本不会生效。只有编译后的二进制程序比如 C 程序才能可靠使用 setuid 位。如果确实要使用 setuid需要把逻辑写进一个编译好的 C 小工具里然后把这个工具以 root:root 权限部署。比如#include stdio.h #include stdlib.h #include unistd.h int main() { setuid(0); system(/opt/scripts/backup_system.sh); return 0; }然后编译、设置 root 属主、加 setuid 位gcc -o backup_helper backup_helper.c sudo chown root:root backup_helper sudo chmod 4755 backup_helper这种方式可以用但审计性不如 sudo。setuid 程序执行时不会像 sudo 那样在/var/log/auth.log里留下谁执行了哪条命令的记录。所以一般情况下我不会推荐 setuid 作为首选。4.2 sudo 和 setuid 的对比到底选谁从安全性、可维护性、审计能力三个维度看sudo 完胜 setuid。对比项sudo 精确规则setuid 程序审计日志有详细日志基本没有权限控制粒度可按用户、主机、命令、参数控制只能按文件授权维护难度改 sudoers 即可需要改源码重新编译误用风险低规则写好后高容易被恶意利用适用场景绝大多数运维管理需求极少场景比如专用系统工具我见过有些团队为了追求“不用输入密码”就把核心运维命令封装成 setuid 程序结果出了问题根本查不到是谁什么时候执行的。后面换成 sudo NOPASSWD虽然还是不用输密码但至少日志完整。4.3 pkexec 和守护进程方案简述除了 sudo 和 setuid还有两个方案值得知道。pkexec是 PolicyKit 的命令行工具。它可以在桌面上弹出授权框或者根据 polkit 规则允许指定用户执行某条命令。相比 sudopkexec 的规则写在/usr/share/polkit-1/actions/和/etc/polkit-1/rules.d/里配置方式更现代化。但实际运维中很多服务器没有桌面环境pkexec 的交互体验不如 sudo 顺滑。守护进程方案是把需要 root 权限的操作封装成系统服务普通用户通过调用服务的控制接口来触发。比如可以写一个 root 权限的 systemd service普通用户只能通过systemctl --user里的某个受限接口提交任务后台再由代理执行。这种方式最安全、审计最完整但实现成本也最高。适合企业级运维平台的场景比如批量发布、日志清理、定期巡检这类高频操作。5. 实战演示完整配置一个“普通用户可执行 root 备份脚本”前面讲了不少理论现在我来走一遍完整流程。从零开始把需求变成最终可用的配置你照着操作就能在自己环境里复现。5.1 需求定义我们希望实现以下目标普通用户ops可以执行/opt/scripts/db_backup.sh。该脚本会以 root 权限执行数据库备份、压缩、移动到备份目录。普通用户不能查看数据库密码文件不能修改备份脚本。执行过程需要输入 ops 用户的密码以增加审计安全。所有 sudo 调用都能在系统日志中查到。5.2 环境准备与脚本创建假设系统是常见的 Linux 发行版默认带 sudo、bash。创建脚本目录并写脚本sudo mkdir -p /opt/scripts sudo tee /opt/scripts/db_backup.sh /dev/null EOF #!/bin/bash set -euo pipefail BACKUP_DIR/backup/mysql DATE$(date %F_%H%M) mkdir -p $BACKUP_DIR # 这里是模拟备份命令实际生产环境请使用真实数据库备份工具 tar -czf $BACKUP_DIR/db_$DATE.tar.gz /var/lib/mysql find $BACKUP_DIR -type f -mtime 7 -delete echo [$(date)] backup completed: $BACKUP_DIR/db_$DATE.tar.gz EOF然后设置属主和权限sudo chown root:root /opt/scripts/db_backup.sh sudo chmod 750 /opt/scripts/db_backup.sh到这一步普通用户 ops 即使能看到脚本也无法编辑它。5.3 创建普通用户并配置 sudo 规则创建用户 ops并加入需要的辅助组比如 ops 可能需要读取某些日志但这不是必须的sudo useradd -m ops sudo passwd ops然后创建独立的 sudoers 配置文件sudo visudo -f /etc/sudoers.d/ops-backup在打开的编辑器里写入ops ALL(root) /opt/scripts/db_backup.sh注意我这里没有加 NOPASSWD意思是执行时需要输入 ops 自己的密码。写入后保存。验证语法sudo visudo -c如果输出parsed OK就说明配置没问题。5.4 普通用户实际操作验证切换到 ops 用户进行验证sudo -u ops -i sudo /opt/scripts/db_backup.sh第一次会提示输入 ops 的密码输入后脚本执行。如果希望验证脚本内部命令是否还需要额外授权可以在脚本里直接写一个需要 root 权限的操作比如touch /root/test_backup_marker然后在脚本中执行。当普通用户运行 sudo 脚本时脚本内部已经是 root 身份所以这个 touch 也能成功。这就是 sudo 包装脚本的特权传播特性。如果要查看执行记录可以查看sudo tail -f /var/log/auth.log或者在使用 journald 的系统上journalctl -u sudo -f你会看到类似ops : TTYpts/0 ; PWD/home/ops ; USERroot ; COMMAND/opt/scripts/db_backup.sh的记录。提示如果脚本里有一些命令不在默认 PATH 中一定要在脚本开头export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin避免 sudo 环境下找不到命令。5.5 权限收紧如何避免 ops 看到敏感文件有些脚本内容本身会包含数据库密码普通用户一旦可读就等于泄露。更安全的做法是将脚本设为仅 root 可读可执行sudo chmod 700 /opt/scripts/db_backup.sh但这样的话普通用户还能执行吗答案是可以因为 sudo 是以 root 身份执行这个脚本的。sudo 在执行命令前不要求执行者对脚本有读取权限因为执行者是 rootroot 能读取一切文件。所以如果普通用户根本不需要了解脚本内容设置700是最佳选择。这样即使有人查看脚本内容也看不到敏感数据。6. 常见问题与排查技巧实录这部分是我真正在日常运维中验证过的内容很多坑如果不踩一遍光看文档是发现不了的。6.1 用户执行时报错“对不起用户 xxxx 无权以 root 的身份执行”这个报错最常见的原因是 sudoers 规则没匹配上。检查几个地方用户名是否正确。命令路径是否完全绝对路径。是否存在其他更早的 sudoers 规则覆盖。是否在/etc/sudoers.d/下创建了文件但文件权限不对。sudoers.d 下的文件必须属主 root权限440否则 sudo 会直接忽略并在日志里提示权限不正确。修复方式sudo chown root:root /etc/sudoers.d/ops-backup sudo chmod 440 /etc/sudoers.d/ops-backup sudo visudo -c6.2 脚本本身能直接执行但 sudo 执行时报“找不到命令”这是因为 sudo 有secure_path默认可能不含/usr/local/bin或/opt/scripts。两个解决方案方案一脚本内写全绝对路径。比如把tar写成/bin/tar。方案二修改 sudoers 的 secure_path。比如在/etc/sudoers里增加Defaults secure_path/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/scripts我更推荐方案一因为这样脚本不依赖 sudo 环境变量可移植性更好。6.3 sudo 执行脚本不生效状态码 126 或 127126 通常表示脚本文件没有执行权限。即使 root 能读但内核要执行一个脚本解释器必须能找到。检查chmod x。127 通常表示解释器不存在比如脚本第一行写了#!/usr/bin/python3但系统里没有 python3。用head -1 script.sh检查。6.4 普通用户执行 sudo 脚本时脚本内部又调用 sudo 导致死循环像我前面写的脚本如果脚本内部有sudo systemctl reload nginx而脚本本身已经是用 root 身份运行的那么就会变成“root 用户再 sudo 到 root”有时候会触发 sudo 的恶意命令拒绝策略。解决办法很简单脚本内部不要再写 sudo因为整个脚本已经运行在 root shell 下。直接写systemctl reload nginx即可。6.5 怎么临时给某用户一次性授权如果只是让某用户执行一次特定命令可以在 sudoers 中临时加一条带时间限制的规则但 sudo 本身没有“过期时间”功能。更实用的做法是使用运维平台的临时授权接口或者简单粗暴地在用户执行完命令后马上删除 sudoers 文件。我个人的习惯是给 sudoers 文件按用途命名并备注日期。比如ops-temporary-backup-20250101执行完毕后立即删除sudo rm /etc/sudoers.d/ops-temporary-backup-20250101这样即使有审计也能清楚看到什么时间段开了什么权限。6.6 检查当前用户实际拥有哪些 sudo 权限作为运维人员我经常需要快速确认某个账号到底能执行什么。方法很简单sudo -l -U ops这会列出所有和 ops 相关的 sudo 规则。如果输出太多可以针对用户看sudo -lU ops这个命令在排查“为什么用户能执行某条命令”时非常有用能直接看到最终生效的规则是哪一条。7. 安全加固别让授权变成后门最后再聊几个我踩过的安全细节这些很容易被忽略但影响非常大。7.1 小心 sudo 包装脚本里的“环境变量注入”脚本如果使用了用户可控的环境变量比如$PATH、$LD_PRELOAD、$PYTHONPATH就可能被利用。举个最简单的例子某脚本执行tar但通过环境变量把tar指向恶意程序。解决方法是在脚本开头重置环境变量或者至少在sudoers里设置Defaults env_reset大多数发行版默认已经开启 env_reset但如果你自己改过 sudoers就要确认没把它关掉。7.2 sudoers 规则顺序越早匹配越优先sudoers 规则是“先匹配先生效”。如果你有一个宽规则写在前面后面再写细规则也白搭。所以我的建议是显式的精确规则永远写在最前面兜底规则写在最后。比如避免写这样的宽规则ops ALL(root) NOPASSWD: /usr/bin/*这种规则等于开放了整个/usr/bin下所有命令危害极大。如果你确实需要通配也要缩小到具体目录和具体不可绕过的二进制。7.3 审计提醒定期检查 sudo 日志我习惯每周花五分钟快速扫一下 auth.log 里的 sudo 记录重点看有没有异常频率的提权操作。如果发现某账号在凌晨频繁执行 sudo即使规则看起来没问题也要排查是不是账号被滥用。日志查看方法sudo grep sudo /var/log/auth.log | tail -100在 journald 系统上journalctl _COMMsudo --since 7 days ago如果不想手动看也可以简单写个 cron 任务把 sudo 日志汇总发到运维群。但这块属于扩展内容今天先不展开。8. 最后的个人经验如果你问我这几年在权限管理上最大的体会我觉得就一句话权限最小化绝不是一句口号而是每一次配置都要落到具体路径、具体参数、具体用户上。sudo 本身很强大强大到可以一句话就让你拥有整个系统的权限也可以强大到只让你执行一个严格校验过的备份脚本。差别就在于你愿不愿意花那几分钟去写清楚规则。另外还有一个容易被忽视的点脚本的可维护性和权限管理同样重要。我见过太多团队花大力气把 sudo 规则写得滴水不漏但提权脚本本身写得一塌糊涂命令到处用相对路径变量不校验出了问题没人敢改。久而久之这些“安全”的规则就变成了定时炸弹。所以我的建议是提权脚本要像生产代码一样对待版本管理、代码评审、权限控制一个都不能少。这样配合 sudo 精确规则才能形成一个真正可靠的权限体系。如果你现在正好有一个“普通用户执行 root 脚本”的需求我建议你从最小场景开始先选一台测试机写一个简单的脚本按我上面的步骤配置 sudoers然后用sudo -l -U 用户名验证授权范围再实际操作一遍。等你把这套流程跑通了再往生产环境推广。这个顺序能帮你避开绝大多数低级事故。希望这篇文章能让你少踩几个坑。如果后续有机会我还会继续分享关于 Linux 权限控制、自动化运维脚本设计、以及安全审计方面的一些实战心得。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表