尧图精选

Shell脚本权限最小化实战:从chmod到sudo的安全加固指南

🕒 发布时间:2026/10/2 1:47:02 📁 来源:尧图网络
1. 为什么要专门聊脚本的权限最小化写自动化脚本这件事几乎每个运维都干过。刚开始的时候大部分人图省事直接chmod 777一把梭脚本倒是能跑了但安全隐患也随之埋下。前两天遇到一个案例某台服务器上有个定时备份脚本权限是-rwxrwxrwx任何人都能改。黑客通过一个漏洞拿到低权限账户后往里加了一行curl … | sh之后每隔几分钟就把服务器上的数据往外传。之所以能这么顺利就是因为这个脚本对“其他人”开放了写入权限。换句话讲攻击者不是靠多高深的技术突破的纯粹是脚本权限太宽松等于大门敞着没关。给 Shell 脚本做权限最小化核心不是“不让脚本执行”而是让脚本、脚本所在目录、以及调用脚本的身份都只暴露恰好够用的权限。一个只能被 root 读取和执行的脚本就算被低权限用户看到文件名也读不到内容一个只有运维组可写的脚本即便有人想注入恶意代码也写不进去。把“失控时的损失范围”压到最小这就是“最小权限原则”在脚本管理里最实在的用法。这篇文章适合谁看日常写脚本的运维、SRE、DevOps 工程师以及那些给团队维护公共脚本库、担心脚本被篡改的管理员。下面这些思路和命令我建议你在自己的环境里都跑一遍比干看理论有用得多。2. 先把 Linux 权限模型里的几个关键点掰清楚2.1 脚本的“读权限”比“执行权限”更重要这一条很多人没意识到。Shell 脚本本质是文本文件执行的时候由bash、sh、zsh这类解释器去读取内容再逐条解释执行。所以解释器必须能读这个文件。如果你把脚本设置成-wx------也就是属主有写和执行权限却没有读权限那你直接./script.sh会报错$ ./script.sh bash: ./script.sh: Permission denied注意这里的 Permission denied 不是因为文件没有 x 执行位而是因为 bash 尝试读取脚本内容时没有 r 权限。测试方式很简单把脚本改成chmod 700能跑改成chmod 500也能跑改成chmod 300就跑不了。也就是说一个可执行的脚本实质上必须可读。理解这一点之后你对脚本权限的设计会更谨慎读权限比执行权限更需要控制。一个可读的脚本别人至少能分析你的逻辑、找出你调用的其他文件路径一个可写但不可读的脚本几乎不存在但理论上可以别人可以直接覆盖危害更大。同时注意执行方式的差异用./script.sh方式要求文件本身同时具备 r 和 x 权限用bash script.sh方式只要求脚本具备 r 权限因为bash是按参数去读取的。所以生产中如果有人绕过你的权限设计用bash /path/to/script.sh去执行一个你没有给 x 权限的脚本只要他对文件有读权限照样能跑。后面我会专门讲这个坑怎么规避。2.2 属主、属组、其他人脚本名的右三段不是摆设在ls -l输出里权限位一共九位分为三组属主、属组、其他人。很多人只关心属主权限觉得“反正这文件是我创建的别人也碰不到”。但脚本一旦放到共享目录、或者被某个服务调用属组和其他人的权限就立刻变成安全边界。举一个常见的配置错误脚本放在/usr/local/bin下属主是 root权限却是-rwxr-xr-x。这意味着任何能登录系统的用户都可以读取并执行这个脚本。如果脚本内部包含数据库连接信息那等于把这些敏感信息送给了所有本地用户。正确做法是先问自己这个脚本到底需要被谁执行如果只需要 root 自己执行那就chmod 700如果需要某个运维组执行那就chgrp一个专用组然后chmod 750如果只需要某个服务账号执行那就把属组改成服务账号的组其他人一律不给权限。最后一组“其他人”权限是权限最小化最容易出问题的位置。很多人的脚本是755觉得没有777那么夸张应该没事。但755的最后一个5意味着所有系统用户都能读和执行。对大多数内部运维脚本来说这个权限仍然太宽。默认心态应该是除了明确需要的人其他人什么权限都不该有。2.3 数字权限映射不要只背 777/755要背 7/6/5/4数字权限的本质是三组rwx的二进制组合。有一个快速换算办法7 rwx读、写、执行全开6 rw-可读可写不可执行5 r-x可读可执行不可写4 r--只读0 ---无权限对于脚本文件常见的合理组合有场景属主属组其他人数字写法单管理员专用rwx------700线上稳定脚本只运行不改r-x------500团队共享组内可改rwxr-x---750团队共享组内只能跑不能改r-xr-x---550只允许属主修改组内运行rwxr-x---750只有属主能读文件不执行rw-------600注意740和750的差别740中组内只有读权限没有执行权限所以组内成员不能直接运行脚本但可以用bash script.sh绕过因为能读750中组内有读和执行权限组内成员可以正常执行。我在团队项目里通常给部署脚本用750给只读配置类文件用640给需要被 root 之外的服务用户执行的脚本用750并把属主设为服务账号或专用组。数值方式比chmod ux,g-w,o-rwx这种符号方式更不容易出错因为它每一步都在明确“三组权限分别是什么”不容易漏掉某一个段。2.4 umask脚本“出生”时继承的安全基因很多人管理脚本权限只管chmod却忽略了umask。umask 决定了新建文件的默认权限。如果你的 umask 是022那新建脚本默认是644也就是其他用户可读如果你的 umask 是027新建脚本默认是640其他用户完全不可见。对脚本目录来说我建议在项目根目录或.bashrc里显式设置umask 027或者创建脚本时用 install 命令一步到位install -m 750 -o root -g ops /path/to/source.sh /usr/local/bin/run.shinstall -m的好处是创建文件的同时就把属主、属组、权限全部设置好不会出现“文件刚创建时是 644等调试完才想起来改权限”的窗口期。窗口期里脚本内容可能已经被别人读走了。3. 最小权限在脚本上的四种落地场景3.1 场景一只有管理员本人使用的脚本自己的脚本权限设多大我的建议是700或500。700意味着只有属主能读、写、执行其他任何本地用户都看不见内容。如果这个脚本已经很稳定没有必要频繁改动那就降到500属主自己也只能读和执行不能通过手滑把它改了。我遇到过一个挺典型的失误一个管理员在自己家目录写了个带数据库明文密码的同步脚本权限是644。他以为在家目录里别人看不到但当时那台机器的/home目录挂载是普通用户可遍历的另一个同事因为账号权限范围没有控制好直接cat出这个脚本数据库密码就这样泄露了。后来把脚本改成700问题才解决。所以不要用“目录里反正只有我自己”这种假设来代替权限控制文件权限是最后一道锁锁必须锁上。3.2 场景二运维团队共用的脚本库团队协作场景里常见的错误是给共享脚本设755让所有登录用户都能执行。正确做法是建一个专用组groupadd ops usermod -aG ops zhangsan usermod -aG ops lisi然后调整脚本和目录权限chown -R root:ops /opt/scripts chmod -R 750 /opt/scripts这样只有ops组内的成员可以读取和执行目录里的脚本其他用户连列出目录内容的权限都没有因为目录本身也是750。需要注意对目录执行chmod 750和“目录里的脚本文件chmod 750”是两个维度目录权限决定你能不能进入目录、列出文件名文件权限决定你能不能读取或执行文件。两个都要管缺一不可。如果团队内有人只需要执行脚本不需要看到脚本内部的敏感变量理论上是做不到的——执行脚本就意味着解释器读取脚本也就是执行者能读脚本。这一点要跟团队成员说清楚能执行脚本的人一定看到了脚本源码。所以脚本里如果有机密信息即便是共享组也要控制组内成员的范围。更敏感的内容应该放配置文件中并限制文件的读取权限脚本运行时动态读取。3.3 场景三被其他服务或系统用户调用的脚本比如 Web 服务要调用一个发布脚本以www-data用户身份运行。此时属于“系统用户需要访问脚本”的情况。很多人直接chmod 755一了百了但这里有更细的控制空间。把脚本的属组改成www-datachown root:www-data /usr/local/bin/web-deploy.sh chmod 750 /usr/local/bin/web-deploy.sh然后确认执行脚本的进程确实是www-data用户或者属于www-data组。这样普通用户对这个脚本既没有读权限也没有执行权限脚本内可能存在的密钥、路径、逻辑都不会暴露。权限最小化不是说“干脆不让服务调用”而是把能调用的范围精确限制到那个服务账号本身。如果服务账号只需要执行不需要修改那就用550而不是750。同理如果服务账号只需要读取一个配置不需要执行脚本那640就够了。每多给一个权限位都要问一句这个权限真的被用到了吗3.4 场景四定时任务cron里的脚本Cron 环境里最容易踩权限坑因为 cron 任务通常继承一个极简环境PATH 很可能只有/usr/bin:/bin而且很多脚本是在 root 身份下运行的。我给定时任务写脚本时一般会做这几件事脚本归属 root权限设700或500不给其他用户执行机会。脚本内部所有外部命令都写绝对路径比如/usr/bin/date、/bin/mkdir防止 PATH 劫持。临时文件用mktemp生成而不是固定写/tmp/xxx.log。固定路径的临时文件容易被其他用户预创建成符号链接脚本再往里写数据时就会落到攻击者指定的文件里。如果脚本里需要生成带日期的时间戳字符串不要用date %Y-%m-%d_%H%M%S以外更花哨的格式然后到处拼统一用变量存档避免不同位置格式不一致也不要把日期字符串直接作为文件名拼接进固定路径那会在/tmp下留下可预测的文件名。定时任务里的脚本有一个思维要改过来这个脚本虽然是我们写来干活的但如果它以 root 身份运行任何能篡改脚本内容的用户都等于拿到了 root 的执行权。所以 cron 脚本的更严格检查点是文件属主必须是 root权限至少是750不能644脚本所在目录也必须是 root 所有且不可被其他用户写入。4. sudo 调用脚本时如何做到既方便又不过度授权4.1 别让所有运维人员都能 sudo 执行任何脚本现实环境里运维团队经常会用sudo执行部署脚本。但很多人配置 sudo 时直接给了ALL或者给了ALL(ALL) ALL等于团队里每个人都有 root 权限。这已经超出了“脚本权限最小化”的范畴属于账号管理层面的权限失控。真正稳妥的做法是把 sudo 权限限定到具体命令路径。假设团队需要执行/usr/local/bin/deploy.sh和/usr/local/bin/check.sh在/etc/sudoers.d/ops-scripts里这样写Cmnd_Alias DEPLOY_CMDS /usr/local/bin/deploy.sh, /usr/local/bin/check.sh ops ALL(root) NOPASSWD: DEPLOY_CMDS然后给/etc/sudoers.d/ops-scripts设权限chown root:root /etc/sudoers.d/ops-scripts chmod 440 /etc/sudoers.d/ops-scripts用visudo -c校验语法。这样配置后ops组或者叫别的名字的成员只能以 root 身份执行这两个指定的脚本不能执行其他任何 root 命令。有人可能会说用户能执行脚本脚本内容又是 root 拥有的但如果脚本里有内嵌命令能通过sudo跳出来吗不会因为 sudo 限制的是命令本身用户不能通过这个脚本把自己变成一个随便执行命令的 shell。前提是脚本内部没有类似eval $*、bash -c $1这种把参数当命令执行的写法。如果你写脚本时允许传任意参数那 sudo 的最小化授权也被架空了。所以这类脚本一定要写死参数格式并校验参数列表。4.2 脚本内部命令也要用绝对路径防止 PATH 劫持权限最小化不止文件权限还有命令解析路径的安全。看一个例子#!/bin/bash tar -czf /backup/data.tar.gz /var/lib/data如果普通用户在当前目录放了一个叫tar的可执行文件里面是恶意程序然后通过某种方式让脚本以相对路径去执行tar而脚本的 PATH 又被设置了优先搜索当前目录那么实际执行的可能是恶意tar。解决办法很简单脚本开头显式定义 PATH并在外部命令前加绝对路径。#!/bin/bash export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /bin/tar -czf /backup/data.tar.gz /var/lib/data或者至少去掉当前目录在 PATH 里的可能性。像 cron 环境默认 PATH 就不带当前目录但交互式 shell 可能带了所以最稳妥的还是绝对路径。写脚本时多敲几个字母换来的是一层“不依赖环境变量”的确定性。4.3 sudo 权限的自查命令权限最小化做得对不对要定期自查。我常用的两个命令sudo -l查看当前用户能执行哪些 sudo 命令立刻就能发现是否被授予了过宽的权限。另外find /usr/local/bin -type f -perm -0002 -print find /usr/local/bin -type f -perm -ow -print-perm -0002表示“其他用户可写”的文件。如果脚本目录里跑出来一堆带这种权限的文件说明权限最小化还没真正落地。还可以用namei -l /usr/local/bin/run.sh查看脚本路径上每一层目录的权限因为即使脚本权限没问题如果上层目录权限太松攻击者可以重命名或移走脚本再放一个同名恶意文件进去。目录权限是容易被忽略的连环问题。5. 高频踩坑清单这些权限问题我在生产环境里真见过5.1 给了 x 位却用 bash 执行导致“可执行”的白名单形同虚设前面提过bash /path/to/script.sh只需要读权限不需要执行权限。这意味着如果你的脚本设的是640属组内的用户虽然不能直接./script.sh但完全可以用bash script.sh来执行里面的内容。所以我做脚本权限规划时会明确一个问题哪些人只允许“读”哪些人允许“执行”。如果某个脚本只该被www-data组执行就不要给其他组成员任何读权限。比如chmod 750对组内成员是允许读和执行的chmod 710则组内成员只能执行、不能读取内容看起来更安全但普通 bash 脚本在710下依然无法运行解释器读不了内容所以对 shell 脚本来说710没有实际意义。真正想控制“只能跑不能读”是做不到的除非用编译型封装比如shc但这会引入新的维护复杂度我不建议轻易搞。5.2 脚本被 777 之后攻击者做了三件“简单”的事某次内部演练我模拟过一个脚本被 777 之后的攻击链条攻击者用低权限账号找到/home/app/backup.sh发现它是 777。在脚本末尾追加了一行echo attacker /tmp/pwned隔半小时执行一次。脚本原本的目标是/backup目录攻击者把tar czf的目标路径改到了临时目录再把压缩包通过脚本后续逻辑传走。整个过程不需要任何漏洞利用就是单纯利用文件权限。这也解释了为什么我用find -perm -0002查权限时态度极其严肃一个 777 文件代表任何人都可以修改它这对脚本来说等于任意代码执行。生产中真的不需要 777你的脚本要么需要被一个人修改要么需要被一群人修改绝不可能需要被“所有登录用户”修改。如果遇到一时搞不清该给什么权限的情况先按最严格的方式设600然后逐个测试谁需要执行再逐步放开成700、750。权限可以慢慢加但一开始就给大了后患无穷。5.3 SUID/SGID脚本权限里的隐蔽后门很多教程都会警告不要给脚本设置 SUID 权限因为大多数 Linux 系统上的解释器出于安全考虑会忽略脚本文件的 SUID 位但这不代表没有风险。SUID 位如果被设置在某些二进制程序上它们运行时会以属主身份执行。在脚本上即使系统忽略 SUID攻击者也可以利用这种奇怪的权限位状态绕过某些安全检查或者配合弱配置造成混乱。更常见的问题发生在目录的 SGID 位SGID 目录会让目录下新建文件的属组自动继承目录的属组这是团队协作的一个实用特性但如果不加留意目录属组被设成了更宽泛的组新建脚本会默认变成该组可写。我建议每个季度做一次扫描find / -type f -perm -4000 -o -type f -perm -2000 2/dev/null找到 SUID/SGID 文件后逐个确认这是不是你真正需要的。脚本类文件上一旦出现s或S权限位基本可以直接取消chmod u-s /path/to/script.sh chmod g-s /path/to/script.sh6. 我在实际维护中的几个收尾建议最后分享几件我在真实环境里长期坚持做的事。第一每次部署脚本到生产环境我都会顺便跑一条命令看权限四件套属主、属组、其他用户权限、目录上层权限。第二脚本目录的变更都用版本管理工具跟踪脚本文件本身设只读权限防止线上被手改。第三对可疑的高权限文件扫描写成一个小脚本放进 cron 定期跑输出异常结果。第四新建脚本时默认以750起步只有当明确理由出现时才改成其他值并且这个理由会写在变更记录里。有一回线上报障一个定时导出任务偶发失败排查到最后发现脚本本身权限没问题但脚本所在的目录在某个版本更新时被改成了777测试人员为了省事在里面写临时文件后来目录里的脚本被另一个自动化任务覆盖了。那次事故之后我给自己定了一条规矩任何目录只要里面有需要长期运行的脚本目录权限就必须和其他所有目录一样按最小权限原则设置不能因为“只是临时目录”就放开。最小权限原则听起来是一个很抽象的安全概念但在 Shell 脚本上它其实就是一组非常具体的数字700、750、550、640外加find扫描、sudo 配置、路径防护。每一条都能解释得清清楚楚每一条也都能防止一类真实事故。把这几件事做进日常脚本管理流程比用任何花哨的安全工具都更扎实。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →