Shell脚本权限最小化:从chmod 777到sudo策略的完整加固指南
最近在整理公司内部的一堆遗留脚本时发现一个特别有意思的现象越是老脚本权限越“豪放”chmod 777几乎是标配。问就是“能跑就行”但真出了问题哭都来不及。这个章节我打算把Shell脚本权限最小化这件事彻底讲透包括为什么要做、怎么做、以及踩坑之后怎么排查全是从实际操作里总结出来的经验不是照着手册念。先说结论权限最小化不是什么高深理论核心就一句话——给脚本和运行脚本的人只分配完成工作所必需的最小权限不多给一分。但这句话落地到Shell脚本的具体场景里牵扯到文件权限位、属主属组、执行方式、sudo策略、定时任务、环境变量、特殊权限位等多个层面每一层都有讲究。这篇内容适合刚入门Shell脚本的开发者也适合已经在线上环境维护脚本但没系统性梳理过权限体系的运维同学。1. 想搞懂权限最小化先搞懂Linux的权限模型1.1 文件权限的三组三权别只停留在“看得懂”层面Linux文件权限的基本模型是“三组三权”属主u、属组g、其他人o三组读r、写w、执行x三种权限。大多数人都知道chmod 755代表属主可读写执行、属组和其他人可读执行但落到实际场景里很多人忽略了一个关键视角权限不仅描述“谁能碰这个文件”还在描述“进程以什么身份在跑”。Shell脚本的执行身份是由执行它的进程决定的。你手动在终端跑./script.sh它就是以你的用户身份运行被 cron 调用就是以 cron 任务的配置身份运行被Web服务调用就是以Web服务的运行用户运行。这意味着脚本文件本身的权限位决定了“谁能启动它”而启动它的那个用户决定了“它能碰什么资源”。很多人在做权限最小化时只盯着文件本身的权限位忽略了运行身份这个更大的权限边界等于门锁换了个高级的钥匙却挂在门框上。还有个基础概念必须提——umask。umask 决定了新创建文件默认拿到的权限它的逻辑是“从默认最大值里扣掉某些位”。一般系统默认umask 022新建文件是644新建目录是755。如果服务器上有人把 umask 设成了000所有新建文件全是666那权限最小化就无从谈起因为“默认状态”本身就是裸奔。提示排查问题时先执行umask看当前值再ls -l看实际文件权限。如果两者对不上说明文件可能是被chmod硬改过的顺着这个线索往往能找到问题根源。1.2 “最小权限”在Shell场景里到底指什么把最小权限原则拆到Shell脚本场景实际包含四个维度缺一不可文件系统权限脚本文件、配置文件、日志文件各自的读写执行权限。谁可以看脚本内容谁可以改脚本谁可以执行脚本要分别控制。很多人把“可读”和“可执行”混为一谈其实对脚本来说可读意味着里面的密码、路径、业务逻辑全部暴露比可执行更危险。运行身份权限脚本以什么系统用户跑这个用户拥有哪些系统资源权限。同样是执行一个备份脚本用 root 跑和用一个普通用户配合sudo跑风险完全不在一个量级。外部命令权限脚本内部调用了哪些外部命令这些命令是否在用户的PATH里是否可能被劫持。这个维度最容易被忽略后面细讲。数据访问权限脚本读写的数据文件、数据库、API密钥等敏感资源的权限边界。这四个维度不是孤立的。一个典型的例子脚本本身权限设得再严格比如700如果它以 root 身份运行而脚本内部有一段curl 某地址 | bash的动态执行逻辑那脚本的所有防御措施全部作废。权限最小化必须是个体系不是单点加固。2. 先看反面教材线上脚本最常见的五个权限作死操作2.1 chmod 777 与“能跑就行”心态chmod 777的问题不用多说任何人可以读、写、执行。对脚本来说可写比可执行更可怕——任何人都能往你的脚本里塞一句话而脚本下次以特权身份执行时这句恶意代码就跟着提权了。攻击者不需要拿到root只需要能找到某个定时执行的高权限脚本并写入内容就能实现权限提升。实际操作中很多777是这么来的开发人员在本机调试不停改脚本发现权限不对就chmod 777图省事然后脚本被复制到生产服务器时顺手保留了权限。生产环境虽然可能要求755或700但没人复查问题就沉淀下来了。排查方法很直接定期扫描所有含执行权限的脚本列出权限大于755的重点审计。用一个简单的find命令就能扫出来find /usr/local/bin /opt /home -type f \( -name *.sh -o -name *.py \) -perm -ow -ls这条命令找出其他用户组有写权限的脚本文件。只要出现结果基本都算违规除非你能明确说出“这个脚本就是需要公开可写”的理由。2.2 SUID/SGID脚本里的定时炸弹SUID位chmod us的作用是让程序执行时以文件属主的身份运行而不是以调用者的身份运行。对二进制程序来说这是个强大的能力但对Shell脚本来说这个机制风险极高。Linux内核从很早的版本起就忽略脚本的SUID位但它会在旧版本内核或某些特定系统上有兼容性陷阱而且SGID位在某些场景下仍然会对脚本生效。真正危险的场景是脚本拥有4755这样的权限——root属主带SUID位。一旦脚本内容可被其他用户修改或者脚本内部有可被利用的漏洞攻击者等于拿着一个root权限的提权入口。即使脚本本身是安全的它如果调用了外部命令而攻击者能在PATH中插入恶意同名命令脚本就会在root身份下执行恶意程序。所以我的原则很简单脚本一律不设SUID/SGID。如果有非root用户需要执行特权操作用sudo的精细化配置来替代SUID可控性高得多。这条要记死。2.3 直接把脚本写在 /tmp 或者Web可访问目录脚本的存放位置本身就是权限体系的一部分。/tmp目录是所有用户可写的脚本放这里意味着每个人都能改它。更糟糕的是如果脚本以root身份通过cron定时执行攻击者只需要往/tmp下的同名文件里注入恶意内容就能在下次定时任务触发时获得root权限。这类攻击叫“临时目录竞态”它是真实存在的经典攻击手法。Web可访问目录同理。脚本放在nginx或apache的静态目录下等于把脚本源代码公之于众里面但凡有点密钥、数据库连接串、内部路径全部泄露。哪怕脚本不需要被Web服务执行只要目录是Web根目录风险就存在。存放脚本的标准做法是把脚本统一放到专用目录如/usr/local/bin、/opt/scripts或者用户家目录下的私有目录并确保目录权限不被其他用户可写。目录的可写权限和文件的可写权限要同等重视因为目录可写意味着可以增删里面的文件比单个文件可写优先级别更高。3. 实操上手给脚本做一套标准的权限最小化改造3.1 第一步梳理脚本的“真实身份需求”拿到一个脚本先别急着改权限。第一步要做的是回答三个问题这个脚本需要以哪个系统用户执行它需要访问哪些文件或目录它内部调用了哪些外部命令以一个线上数据导出脚本为例。脚本叫export_data.sh作用是每天凌晨从数据库导出当天订单数据压缩后放到/data/backup然后清理5天前的旧备份。梳理出来的需求是需要一个专门用户比如app_export来跑这个用户需要对数据库连接配置有读权限对/data/backup有写权限对旧备份文件有删除权限。而不需要的权限包括root权限、对系统配置目录的写权限、对其他用户的文件读权限。把需求梳理清楚了权限最小化的执行方案就自然浮现了。大多数人的问题恰恰是跳过这一步直接chmod 755然后扔进cron。这样权限不是“设计”出来的而是“蒙”出来的迟早出问题。3.2 第二步精确设置文件权限位基于梳理出来的需求逐项设置权限。对于上面的导出脚本推荐方案是脚本文件权限750属主是root:app_export即只有root能改脚本内容app_export组的用户能执行。为什么不让app_export直接拥有脚本因为脚本文件归属越少人能改越好root负责维护执行组负责使用这是职责分离的体现。chown root:app_export /usr/local/bin/export_data.sh chmod 750 /usr/local/bin/export_data.sh数据库连接配置文件640属主root:app_export只有root能读写、app_export组的成员能读。配置里通常有账号密码不给执行权限也不给其他用户读权限。chown root:app_export /etc/export_db.conf chmod 640 /etc/export_db.conf备份目录750属主app_export:app_export只有该用户能写。备份数据可能含客户信息属主和属组都是专用用户其他用户一律不可见。chown app_export:app_export /data/backup chmod 750 /data/backup注意这里的一个细节脚本运行时它对配置文件是“读”对备份是“写”。但脚本执行时是以app_export身份跑的所以判断权限是否充分的依据是app_export的身份而不是root。很多人习惯用root调试root能访问一切导致权限问题在调试阶段根本暴露不出来一上线就报Permission denied。3.3 第三步sudo 精细化——给非root用户“够用的特权”如果某个运维人员需要手动触发脚本但脚本里有些步骤需要root才能完成比如读取只有root可读的系统日志方案不是把脚本本身变成root可执行而是通过sudo配置让指定用户以root身份执行这一个脚本。在/etc/sudoers.d/下新建配置文件比直接编辑/etc/sudoers更规范还能避免语法错误把sudo整个搞挂。一个配置示例Cmnd_Alias EXPORT_SCRIPTS /usr/local/bin/export_data.sh ops_user ALL(root) NOPASSWD: EXPORT_SCRIPTS这里用了Cmnd_Alias好处是后续有多个脚本时可以统一管理。NOPASSWD表示执行该命令不需要输入密码——这是有意的设计便于自动化调用但前提是sudoers里严格限定了命令路径和参数。注意sudo配置里命令路径必须写绝对路径不要写成export_data.sh否则sudo可能匹配不到而且绝对路径能防止PATH劫持。如果脚本需要参数要明确参数是否允许或者用通配符精确匹配不要写成/usr/local/bin/export_data.sh *这种宽松配置。还要注意一个常见的坑sudo执行脚本时脚本内部再次调用其他命令是不走sudo权限的。也就是说sudo ./export_data.sh只让脚本本身以root跑脚本内部执行的rm、tar等命令仍以脚本调用者的身份运行——除非脚本内部对特定命令也做了sudo包装。这个机制设计如此目的是防止“一个sudo把整个世界都提权”实际使用时要清楚这一点别指望脚本里所有命令自动获得root权限。4. 场景化实战三种典型场景下的权限最小化方案4.1 定时任务场景cron里的权限陷阱与正确姿势cron是Shell脚本最常见的宿主机场景这里面的权限坑尤其多。第一坑是crontab里直接写* * * * * /root/script.sh脚本以root身份每分钟跑一次。如果脚本本身权限没守住其他用户可写等于固定时间窗口的提权漏洞。正确做法是为定时任务创建专用用户。以app_cron用户为例先创建身份再把相关目录和文件权限规划好useradd -r -s /usr/sbin/nologin app_cron chown -R app_cron:app_cron /opt/scripts chmod 750 /opt/scripts然后在该用户的 crontab 中配置任务。用crontab -u app_cron -e编辑而不是写入root的crontab。这样即使脚本被攻破攻击者拿到的也只是app_cron的权限而不是root的。cron还有一个环境变量坑cron环境下PATH往往很精简系统默认PATH可能不包含/usr/local/bin等目录。脚本里如果直接调用命令名而不是绝对路径在手动执行时没问题在cron里就报command not found。这不属于权限最小化但排查权限问题时经常一起冒出来。建议脚本开头显式设置PATH并且关键命令全部使用绝对路径一举两得。4.2 Web服务场景nginx PHP脚本的权限切割服务器上跑着nginx或者apache的脚本权限问题更复杂。以常见的nginxPHP环境为例nginx的工作用户通常是www-dataPHP-FPM也可能以www-data运行。如果某个PHP脚本需要调用外部的Shell脚本完成任务这里就出现一个权限边界问题。第一种情况PHP脚本以www-data身份执行Shell脚本那么Shell脚本的权限要保证www-data能读到。但Web脚本往往是攻击者的首要目标——一个LFI本地文件包含漏洞就能让攻击者读走任何www-data能读的文件。所以凡是Web用户能读的脚本一律不能包含敏感信息。数据库密码、API密钥等必须放到只有特定用户可以读的配置文件里Web用户负责调用调用时从环境变量或专属配置读取。我的经验做法是在/etc/下放一个专用配置文件权限600 root:www-dataPHP通过env读取Shell脚本通过source读取两边都不在代码里硬编码敏感信息。即使Web脚本被打穿攻击者拿到的也只是“能执行某个特定脚本”的能力而不是“读走所有密码”的能力。第二种情况如果Shell脚本需要更高的权限比如操作只有root能碰的设备文件方案仍然是sudoers白名单而不是把Web用户加到sudo组。配置如下www-data ALL(root) NOPASSWD: /usr/local/bin/clean_cache.sh然后PHP脚本里通过shell_exec(sudo /usr/local/bin/clean_cache.sh)调用。这样攻击者即便控制了PHP脚本也只能触发这一个白名单命令不能执行任意系统命令这是Web环境里权限最小化的核心。4.3 敏感数据处理场景密钥、密码与临时文件的权限管理脚本里处理敏感数据时权限最小化的要求更高。先说密钥和密码文件权限至少600属主必须是运行服务的专用用户其他用户一律不可读。但有个细节要注意——脚本执行的中间过程也可能泄露敏感数据。比如脚本里mysql -p$DB_PASS这个密码会在进程列表里短暂出现任何能执行ps的用户都能看到。避免办法是改用.my.cnf配置文件并设置600权限或者通过环境变量注入。临时文件的权限同样要管。脚本生成临时文件时用mktemp而不是固定路径的/tmp/xxx因为mktemp创建的临时文件默认权限就是600且文件名随机避免了多用户环境下临时文件被猜解或劫持的问题。写法TMP_FILE$(mktemp /tmp/export_XXXXXX) chmod 600 $TMP_FILE处理完数据后及时清理临时文件trap rm -f $TMP_FILE EXIT确保脚本退出时无论成功失败都收拾干净。很多人把精力花在脚本逻辑上对临时文件的管理却很随意这是敏感数据场景里最容易被忽略的环节。5. 遇到权限问题如何排查一个排查流程速查表5.1 从“Permission denied”反推权限配置的五步检查脚本报Permission denied时别急着加权限。按照下面的顺序排查大多数问题五分钟内定位第一步确认文件是否存在且当前用户对该路径有执行权限ls -l /path/to/script.sh id第二步查看文件所在目录的权限。执行权限不仅看文件本身还看从根目录到脚本文件路径上每一层目录的“x”权限。比如/home/user/script.sh即使文件是755如果/home/user是700其他用户依然无法进入目录执行脚本。用namei -l /path/to/script.sh可以一次性列出路径上每一层的权限非常实用。第三步确认运行身份。当前用户是否属于脚本属组用id看组成员。文件是750 root:deploy你如果是deploy组成员就有执行权限如果不在组内且文件没有“其他用户”的执行权限就拒绝。很多多人协作环境里“明明有权限”的错觉就是因为没搞清楚自己在不在组里。第四步检查是否有ACL访问控制列表干扰。getfacl /path/to/script.sh能看到ACL规则。有时候文件权限位看着是640但ACL里给某个用户单独加了读权限或者反过来把权限收得过紧。第五步查看SELinux或AppArmor策略。RHEL系服务器上遇到过好几次诡异问题——文件权限完全正确但就是执行不了最后定位到SELinux的httpd_execmem等策略限制了特定用户执行特定类型文件。用getenforce确认SELinux状态必要时用ausearch查日志确认是不是被安全策略挡了。5.2 常见排查工具与实用命令手册排查脚本权限问题时这几个命令是高频主力建议贴在终端边上随时用命令用途关键参数与技巧ls -l查看文件权限加-Z可同时查看SELinux上下文namei -l查看路径各级目录权限直接定位“路径上哪一层堵住了”id查看当前用户和组确认是否属于目标属组getfacl/setfacl查看/修改ACLACL优先级高于常规权限位sudo -l查看当前用户的sudo权限确认用户能执行哪些白名单命令strace -f -e tracefile跟踪进程内所有文件操作解决“脚本内部命令权限不足”的疑难杂症find -perm -ow扫描过度开放的权限定期巡检必备其中strace是压箱底的工具。比如脚本自己执行正常但从Web端调用就失败肉眼根本看不出问题用下面的命令能追踪到具体是哪个文件的哪个操作被拒绝了strace -f -e tracefile -o /tmp/strace.log /usr/local/bin/export_data.sh打开日志看open、access、execve这些系统调用的返回值和错误码立刻就能定位到是哪个路径、哪个文件、什么操作出了问题。我处理过的最典型的案例脚本里用到了/sbin/iptables而Web用户调用的环境PATH不包含/sbin手动执行时正常Web调用时却静默失败——strace日志里清清楚楚显示execve(/usr/local/bin/iptables, ...) -1 ENOENT。5.3 一份可以直接抄的权限设置对照表根据脚本的不同用途参考下面这个对照表进行权限设置能省去大量纠结时间系统级运维脚本root维护、root执行700属主root。只有root能看、能改、能执行。适用场景如系统初始化脚本、内核参数调整脚本。业务脚本root维护、特定组执行750属主root、属组业务组。root可维护业务组成员可执行。适用场景如数据导出、报表生成等。个人工具脚本个人使用700属主自己。防止其他用户窥探和篡改。供所有人执行的只读脚本所有用户可执行、仅root可改755属主root。适用场景如通用工具脚本但前提是脚本内部不涉及敏感数据。配置文件600或640。属主选服务专用用户属组选择需要读取的组。一律不给“其他用户”任何权限。日志文件640属主服务用户、属组adm。日志可追加不可改给运维组读取权限即可。这套表格不是死规矩核心原则是能少给的权限绝不多给能给到组就别给到所有人可读和执行权限要分开考虑。按照这个思路去设置基本不会出大错。6. 脚本内部编码层面的权限最小化细节6.1 避免PATH劫持绝对路径与安全PATH脚本执行外部命令时命令解析依赖PATH环境变量。安全风险在于如果脚本以特权身份执行而PATH里包含当前目录.或者可写目录攻击者可以在这些目录下放一个与命令同名的恶意可执行文件脚本就会在毫不知情的情况下执行恶意程序。防御方式有两种推荐组合使用。第一种是在脚本开头显式定义安全PATHexport PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin第二种是对关键命令使用绝对路径调用比如把rm写成/bin/rm把tar写成/bin/tar。绝对路径的好处是彻底绕开PATH解析缺点是脚本可读性变差所以通常只对特权脚本或涉及执行外部程序的脚本这么做。我个人习惯是系统级脚本全部用绝对路径普通业务脚本定义安全PATH即可。原则还是那个——脚本越接近特权边界越要用绝对路径。6.2 脚本内部的文件操作也要最小化权限脚本在运行过程中如果创建文件默认权限取决于当前的umask。例如备份脚本生成的压缩包如果umask是022文件就是644其他人可读。对包含敏感数据的备份文件来说这是不可接受的。所以脚本内部涉及敏感文件创建时显式设置umaskumask 077umask 077意味着新文件权限是600新目录权限是700只有运行脚本的用户能访问。在脚本开头设置一次即可后续创建的临时文件、备份文件、输出文件都默认走这个权限。有些脚本还会修改系统文件或者把自己的执行结果写入rc.local、systemd服务文件等位置。这类操作要特别谨慎——一旦脚本有了“修改系统启动项”的权限它的安全等级就等同于root了。此时必须确保脚本文件本身极其严格700root-only并且内容经过review不能有动态拼接命令的逻辑。6.3 别小看隐藏的敏感操作命令注入与不可信任输入权限最小化除了管文件和身份还要管“脚本的输入”。脚本如果接受外部参数并且参数直接拼进命令就有命令注入风险。典型的反面例子dir$1 ls -l /data/$dir如果调用方传入../../; rm -rf /tmp之类的参数命令会变成多条。这在脚本以低权限运行时尚且是个风险一旦脚本以特权身份运行就是灾难。防御方式是参数白名单校验比如只允许字母数字和短横线case $dir in *[!a-zA-Z0-9_-]*) echo 非法参数 2 exit 1 ;; esac这不是严格意义上的“权限”问题但它是权限最小化的延伸——既然已经给了脚本特权就要确保脚本不会在不可信任输入下越权操作。7. 每次实战里都会用到的三条经验先分享一个定位问题的土办法如果脚本在手动执行时正常、在 cron 或 Web 调用时异常第一个怀疑点永远是“环境不一致”第二个是“身份不一致”第三个才是“权限配置错”。环境不一致排查PATH和HOME身份不一致排查用户和组权限配置错用namei和strace来验证。按这个顺序来基本不走弯路。再分享一个定时巡检的小技巧。我习惯在每周巡检时跑一次过度权限扫描目标不只是脚本还包括配置文件、日志文件和备份数据文件。扫描命令用find -perm -ow配合-type f把结果单独拉出来看一遍。整个巡检五分钟内完成但能避免绝大部分因权限松弛引发的低级事故。最后说一个边界情况脚本审计不能只看权限位要连内容一起审。我遇到过权限设得非常完美700 root的脚本内容里却写死了数据库密码并且密码通过echo输出到日志。权限做得再好也防不住“自己在脚本里埋雷”。所以做权限最小化的同时建议同步用grep -nE (password|passwd|secret|token|api_key) /opt/scripts/*.sh扫一遍脚本里的敏感信息把硬编码的密钥全部迁移到配置文件或密钥管理服务。权限最小化是一门系统工程文件权限只是入口把身份、命令、输入、数据全链路都收紧才算真正做到了位。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →