在日常开发和运维中,Permission denied大概是出现频率最高的报错之一。脚本明明就在那里,./run.sh却提示没有权限;配置文件改不动;同事能访问的目录,换个人就进不去。这些问题背后其实都指向同一套机制——Linux 权限。本文围绕 Linux 权限管理的基础部分展开,把权限模型、权限串解读、chmod/chown/umask的用法、特殊权限位以及一套完整的多角色目录授权实战讲清楚,最后给出排查清单和工程建议。适合刚接触 Linux 的同学系统入门,也适合已经会用但没理清细节的开发者查漏补缺。
1. Linux 权限管理入门:从一次 Permission denied 说起
1.1 权限问题为什么总在最关键的时候出现
很多初学者第一次真正意识到权限的存在,往往是在部署服务的时候。比如把应用目录放在/opt下,用root启动一切正常,换成普通用户app启动就报“无法写入日志文件”;又或者从 Windows 拷贝过来的启动脚本,ls -l一看是-rw-r--r--,根本没有执行位,直接./start.sh必定失败。这类问题的共同特点是:文件确实存在,路径也确实正确,但操作系统在你伸手的那一刻就把操作拦了下来。
Linux 设计权限机制的根本目的,是让多个用户可以在同一台机器上共存而不互相破坏。它不像单用户的桌面系统那样“谁登录谁就是老大”,而是给每一个文件、每一个目录都标注好“谁能读、谁能写、谁能执行”,每次访问都做一次强制检查。理解这一点之后,你会发现权限报错并不是系统在刁难你,而是一套规则正在生效。把规则读懂了,报错反而变成了最准确的线索。
1.2 Linux 权限到底在管什么
用一句话概括:Linux 权限管理的是“某个用户(或某个组)对某个文件对象,能不能做某类操作”。这里包含三个要素——主体(用户/组)、客体(文件、目录、设备等一切文件系统对象)、操作(读、写、执行)。三者组合起来,就构成了权限判定的基本单元。
需要特别强调的是,Linux 里“一切皆文件”,所以权限机制的作用范围远不止普通文件。目录、设备文件、管道、套接字,全部有权限位。甚至你cd进一个目录、ls列出目录内容、在目录里创建文件,这些看起来和“文件内容”无关的动作,同样受权限控制,只不过目录的r、w、x含义和普通文件不同,这一点后文会专门拆开讲。
从专业角度看,Linux 采用的是经典的UGO + rwx自主访问控制模型(Discretionary Access Control)。文件的所有者可以自行决定把权限授予谁,系统只负责执行这个决定,不负责判断这个决定是否合理。正因为“自主”,权限配置的合理性就完全落在管理员和开发者身上,这也是后面要讲最小权限原则的原因。
1.3 权限管理在整个安全体系中的位置
如果把一台 Linux 主机的安全防护分层来看,权限是第一层,也是最基础的一层。它挡在最前面:账号能不能登录、登录后能看哪些文件、能改哪些配置、能不能执行某个二进制程序,全都由权限决定。再往上才是防火墙、审计日志、入侵检测这些手段。
很多线上事故的根因都可以追溯到权限过宽。比如为了图省事执行chmod -R 777,表面上问题解决了,实际上等于把整个目录对所有用户开放了写权限,任何被攻破的低权限账号都能篡改程序文件;再比如给业务脚本配置了免密sudo且不限制命令范围,一旦脚本本身有命令注入风险,攻击面会被直接放大。所以权限不是“配一次就忘”的环境准备工作,而是需要持续维护的安全基线。
2. 环境准备与示例约定
2.1 操作系统与命令版本说明
本文涉及的命令都是 POSIX 标准工具,在主流 Linux 发行版上行为一致,包括 CentOS / RHEL 系列、Ubuntu / Debian 系列、Rocky Linux、AlmaLinux 等。具体的发行版版本和内核版本不需要严格对齐,本文示例重点在于演示配置思路和权限语义,你完全可以在自己机器上直接复现。
不同发行版之间的差异主要体现在几个局部:useradd与adduser的使用习惯(Debian 系常用交互式的adduser,RHEL 系常用useradd)、默认umask值的配置位置(/etc/profile、/etc/login.defs、/etc/bashrc都可能涉及)、以及是否默认启用 SELinux(RHEL 系通常默认开启)。这些差异在遇到问题时需要结合实际情况排查,后文的常见问题一节会专门提到。如果你用的是较新的发行版,工具行为可能有细微变化,遇到不一致的地方以man手册为准。
2.2 本文用到的命令清单
为了保证后续实操能顺利进行,先把涉及的命令列出来,方便你对照检查系统里是否都有:
| 命令 | 作用 | 所属包 |
|---|---|---|
ls -l | 查看文件详细属性与权限串 | coreutils |
chmod | 修改文件或目录的权限位 | coreutils |
chown | 修改文件或目录的属主、属组 | coreutils |
chgrp | 单独修改属组 | coreutils |
umask | 查看或设置新建文件的默认权限掩码 | bash 内建 |
useradd/usermod/groupadd | 用户与用户组管理 | shadow-utils |
id | 查看用户的 UID、GID 与附加组 | coreutils |
getfacl/setfacl | 查看和设置 ACL 精细权限 | acl |
sudo | 以其他用户身份执行命令 | sudo |
如果setfacl提示命令不存在,在 RHEL 系执行sudo yum install -y acl,Debian 系执行sudo apt-get install -y acl即可。其余命令属于基础系统组件,默认都具备。
2.3 示例目录结构与命名约定
后续实战会创建下面这套目录,先在这里统一说明,避免后面看代码时对不上:
/srv/project/ ├── src/ # 源码目录,开发组成员可读写 ├── logs/ # 日志目录,开发组只读 └── shared/ # 共享目录,多人可写但只能删自己的文件三个用户分别承担不同角色:alice和bob属于开发组devteam,carol属于运维组opteam,只被额外授权访问shared目录。这套设定贴近真实项目里“开发组 + 运维组 + 共享区”的常见结构,演示价值比较高。
2.4 操作前的安全前提
本节的所有命令都会创建用户、修改目录属主、调整权限位,请在测试机、虚拟机或容器中执行,不要直接在承载业务的生产服务器上练手。如果你确实需要在生产环境调整权限,务必遵循三个前提:确认操作范围(用ls -ld先看清现状)、做好备份(目录权限可以先记录到文件)、在变更窗口内操作并准备好回滚方案。权限变更不像改代码可以回滚提交,一旦chmod -R敲错范围,恢复成本可能很高。
3. 权限模型的核心概念拆解
3.1 三类身份:属主、属组、其他人
Linux 为每个文件记录三个身份维度。第一个是属主(owner),也就是文件的创建者或后来被指定的所有者,用u表示;第二个是属组(group),文件所属的用户组,用g表示;第三个是其他人(others),既不是属主、也不在属组里的所有用户,用o表示。
这里最容易踩的坑是“属组”的理解。一个用户可以同时属于多个组(一个主组 + 若干附加组),但一个文件只能有一个属组。判定时系统会先看你是不是属主,是就直接套用属主权限,不再看后面;不是属主再看你属不属于文件的属组;都不匹配才落到“其他人”。这个匹配顺序是短路的,不会取并集。举个例子,某个文件属主是alice、属组是devteam,权限是rwx---rwx,如果bob既属于devteam又是文件属主——这不可能同时成立——但假设alice也在devteam里,她走的仍然是属主那一档,而不会因为组权限是空的就失去访问能力。
3.2 三种基本权限:r、w、x 的真实含义
r(read,读)、w(write,写)、x(execute,执行)是三种基本权限,对应数字 4、2、1。对普通文件来说语义很直白:读就是能看内容,写就是能改内容,执行就是能把它当程序跑起来。但要注意,执行权限只是“允许尝试执行”,文件本身得是合法的可执行格式(二进制或带正确 shebang 的脚本),否则内核会报Exec format error。
对目录来说,这三个字母的含义完全不同,这是权限学习中最关键的一处认知转换:
r:允许列出目录内的文件名(ls能看到东西)。w:允许在目录内创建、删除、重命名条目。注意删除文件看的是目录的写权限,而不是文件本身的写权限。x:允许进入目录(cd)以及访问目录内的条目。没有x,即使有r也只能看到名字,拿不到 inode 信息,ls -l会显示一堆问号。
这三者的组合有实际意义。比如一个目录权限是r--,你能ls看到文件名,但ls -l看不到大小和权限,也cd不进去;如果是--x,你进得去、能访问已知路径的文件,但列不出目录内容,这种配置在需要“隐藏目录内容但允许按路径访问”的场景偶尔会用到。
3.3 读懂 ls -l 输出的十位权限串
ls -l的第一列是最重要的信息,先看一个真实输出:
$ ls -l /srv/project total 12 drwxr-xr-x 2 alice devteam 4096 Oct 12 10:20 src -rw-r--r-- 1 alice devteam 512 Oct 12 10:18 app.conf -rwxr-xr-x 1 root root 1824 Oct 11 09:30 deploy.sh用-rwxr-xr-x这一串来分析,它由 10 个字符组成(部分系统末尾还会多一个.或+,分别表示 SELinux 上下文和存在 ACL):
| 位置 | 字符 | 含义 |
|---|---|---|
| 第 1 位 | - | 文件类型:-普通文件,d目录,l软链接,c字符设备,b块设备,s套接字,p命名管道 |
| 第 2-4 位 | rwx | 属主权限 |
| 第 5-7 位 | r-x | 属组权限 |
| 第 8-10 位 | r-x | 其他人权限 |
后面依次是硬链接数、属主、属组、大小、修改时间、文件名。初学者常犯的错误是把第一位-也当成权限位,看到-rw-r--r--数成 11 位就懵了。记住第一个字符是类型,剩下的 9 位才是权限,正好按 3 位一组切成三段。
3.4 文件权限与目录权限的本质差异
前面已经提到目录权限的r、w、x语义,这里再补一个非常实用的推论:访问一个文件时,路径上每一级目录都必须有x权限。假设/srv/project/src/main.py文件本身权限是777,但/srv/project是700且属主是 root,那么普通用户连cd /srv/project都做不到,更别说读里面的文件了。
这解释了为什么很多“权限明明给了还是不能访问”的问题,最后发现是某一级父目录卡住了。排查时不要只盯着目标文件,要用namei -l /path/to/file一次性把整条路径的权限打印出来,或者在每一级目录上用ls -ld逐层确认。这个技巧在后面的排查章节还会用到。
另外,目录的w权限意味着可以删除目录里的任何文件,不管那个文件属主是谁、权限是几。这是很多人的认知盲区:想保护一个文件,光改文件权限没用,还得控制它所在目录的写权限。共享目录场景下的“粘滞位”就是为解决这个问题而生的,第 4.5 节会详细讲。
4. 权限的表示法与修改命令
4.1 数字表示法:八进制权限
数字法是最常用的权限表示方式,规则非常简洁:r=4、w=2、x=1,每组权限把三个数字相加,得到一个 0 到 7 的八进制数,三组拼起来就是三位权限码。比如rwx= 4+2+1 = 7,rw-= 4+2 = 6,r--= 4,---= 0,r-x= 5。
常用的权限码可以整理成一张对照表:
| 数字 | 权限串 | 典型用途 |
|---|---|---|
| 777 | rwxrwxrwx | 完全不限制,生产环境慎用 |
| 755 | rwxr-xr-x | 目录、可执行脚本的标准权限 |
| 750 | rwxr-x--- | 组内共享目录,其他人无权访问 |
| 700 | rwx------ | 私有目录,如用户家目录 |
| 644 | rw-r--r-- | 普通配置文件、网页文件 |
| 640 | rw-r----- | 敏感配置,仅属主和组可读 |
| 600 | rw------- | 私钥、凭据文件 |
| 444 | r--r--r-- | 只读文件 |
使用数字法修改权限非常简单:
# 把配置文件设为属主可读写、组可读、其他人不可读 chmod 640 /srv/project/app.conf # 递归修改目录及其内部所有内容(谨慎使用) chmod -R 750 /srv/project/src数字法的优点是直观、一次到位,适合在脚本里做确定性配置。缺点是它是“覆盖式”修改,会把原有权限整个替换掉,如果目录里混着可执行文件、普通文件,统一-R 755会把所有文件都加上执行位,这既不规范也不安全。所以递归修改前一定要想清楚目录内容构成。
4.2 符号表示法:精确增删
符号法的格式是[ugoa][+-=][rwx],其中u/g/o/a分别表示属主、属组、其他人、全部,+增加权限、-移除权限、=直接赋值。相比数字法,符号法的优势是“增量修改”,不会影响未提及的部分。
# 给属主增加执行权限 chmod u+x deploy.sh # 移除其他人和其他组的写权限 chmod go-w app.conf # 把属组权限直接设为只读 chmod g=r app.conf # 所有人增加读权限 chmod a+r README.md # 组合操作:属主读写执行,组读执行,其他人无权限 chmod u=rwx,g=rx,o= /srv/project/src实际工作中两种方法经常混用:初始化目录结构时用数字法一次性配好,后期微调时用符号法避免误伤。另外chmod支持一次传多个模式,用逗号分隔,例如chmod u+x,g-w file,这样一条命令就能完成复合调整。
4.3 chown 与 chgrp:属主属组的变更
chmod只改权限位,改属主和属组要用chown。它的基本格式是chown 属主:属组 文件,可以只改属主、只改属组,也可以同时改:
# 同时修改属主和属组 sudo chown alice:devteam /srv/project/src # 只改属主,属组保持不变 sudo chown bob /srv/project/app.conf # 只改属组(冒号前留空) sudo chown :devteam /srv/project/logs # 递归修改整个目录树 sudo chown -R alice:devteam /srv/project/srcchgrp是单独改属组的命令,效果等同于chown :组名,保留它主要是习惯问题。这里要强调一个安全边界:普通用户不能把自己的文件“送”给别人,只有 root 才有能力执行chown到其他用户。这不是实现限制,而是防止有人通过转移文件所有权来绕过磁盘配额或逃避审计。所以上面的命令都加了sudo。
还有一个容易被忽略的点:递归chown -R会连带修改符号链接指向的目标文件属性,如果目录里有指向系统关键文件的软链接,后果可能很严重。较新版本的chown在递归时默认不跟随符号链接(需要显式-L),但仍建议在递归前用find确认范围。
4.4 umask:新建文件的默认权限从哪来
新建文件不会一出生就是777,而是由一个“掩码”决定。umask的值表示要从默认权限里去掉哪些位。计算规则是:
- 普通文件的初始权限 =
666减去 umask - 目录的初始权限 =
777减去 umask
默认umask通常是022,于是新建文件得到644(666 & ~022),新建目录得到755(777 & ~022)。这解释了为什么你刚touch出来的文件是-rw-r--r--,刚mkdir出来的目录是drwxr-xr-x。
# 查看当前 umask $ umask 0022 # 临时修改,仅对当前 shell 会话有效 $ umask 002 $ touch test.txt && ls -l test.txt -rw-rw-r-- 1 alice devteam 0 Oct 12 11:00 test.txt把umask设为002后,新建文件变成664,同组成员也能写,这正是团队协作目录需要的效果。umask是 shell 内建命令,只影响当前会话及其子进程,要持久化必须写进~/.bashrc(仅当前用户)或/etc/profile、/etc/login.defs(全局)。注意umask只影响新建对象的初始权限,对已存在的文件毫无作用,这一点经常被误解。
4.5 特殊权限位:SUID、SGID、Sticky
除了三组基础权限,Linux 还有三个特殊权限位,占据权限码的第四位(也就是常说的“四位权限码”),分别对应数字 4、2、1。
SUID(4)作用于可执行文件,表示“程序运行时以文件属主的身份执行,而不是调用者的身份”。最经典的例子是/usr/bin/passwd,普通用户执行它时,程序实际以 root 身份运行,否则根本没权限改写/etc/shadow。设置方式:
sudo chmod 4755 /path/to/program # 或者 sudo chmod u+s /path/to/program权限串里属主位的x会变成s,显示为-rwsr-xr-x。如果原本没有执行位,则显示大写S,表示这个 SUID 位是“无效”的(因为文件根本不可执行)。SUID 是攻击者最喜欢的提权入口之一,生产环境应严格审计,用find / -perm -4000 -type f 2>/dev/null可以列出系统上所有带 SUID 的文件。
SGID(2)有两种用途。放在可执行文件上,效果与 SUID 类似,只是身份切换为属组;放在目录上,作用完全不同且非常实用——在该目录下新建的文件和子目录会自动继承目录的属组,而不是创建者的主组。这解决了团队共享目录里“每个人建的文件属组都不一样”的混乱问题。权限串里显示为drwxrws---。
Sticky Bit(1)只对目录有效,语义是“目录内的文件只有文件属主、目录属主和 root 才能删除或重命名”。系统自带的/tmp就是典型配置:
$ ls -ld /tmp drwxrwxrwt 12 root root 4096 Oct 12 11:10 /tmp末尾的t就是粘滞位。这样所有人都能在/tmp下创建文件,但删不掉别人的文件。设置方式为chmod 1777 目录或chmod +t 目录。在共享写入目录场景中,粘滞位几乎是必选项,第 5 节的实战会用到。
5. 完整实战:给一个多角色项目目录配权限
5.1 需求梳理与权限设计
假设我们有一台开发测试机,需要划出一个项目目录供两个角色使用。需求可以总结为四条:
- 开发组成员
alice、bob对src目录有完整读写权限,能在里面新建、修改、删除文件。 - 开发组成员对
logs目录只读,可以看日志但不能改日志。 shared目录所有开发成员可写,但不能删除其他人创建的文件。- 运维人员
carol默认不能访问整个项目目录,但需要被单独授权访问shared。
对应的权限设计如下:
| 目录 | 属主:属组 | 权限 | 说明 |
|---|---|---|---|
/srv/project | root:devteam | 750 | 组内可进入,其他人无法进入 |
/srv/project/src | alice:devteam | 2770 | SGID 保证新建文件继承 devteam 组 |
/srv/project/logs | alice:devteam | 750 | 组内可读可进入,不可写 |
/srv/project/shared | root:devteam | 1770 | 粘滞位防止互删 |
这个设计已经能覆盖大部分场景,唯一需要额外处理的是carol的授权——她既不是属主也不在devteam组里,靠基础权限位拿不到访问权。这就是 ACL 要解决的问题,放在 5.6 节。
5.2 创建用户组与用户
先创建组和用户。注意useradd -m会自动创建家目录,-s /bin/bash指定登录 shell,省略的话可能拿到/sbin/nologin导致无法su切换:
# 创建两个用户组 sudo groupadd devteam sudo groupadd opteam # 创建三个用户并指定家目录和 shell sudo useradd -m -s /bin/bash alice sudo useradd -m -s /bin/bash bob sudo useradd -m -s /bin/bash carol # 为三个用户设置密码(按提示输入) sudo passwd alice sudo passwd bob sudo passwd carol # 把用户加入对应的组(-a 表示追加,不能省略) sudo usermod -aG devteam alice sudo usermod -aG devteam bob sudo usermod -aG opteam carol这里有个高频错误必须提醒:usermod -G devteam alice不加-a会覆盖用户原有的附加组,如果alice之前还在别的组里,那些组关系会全部丢失。正确写法永远是usermod -aG。修改完成后用id验证:
$ id alice uid=1001(alice) gid=1001(alice) groups=1001(alice),1002(devteam) $ id carol uid=1003(carol) gid=1003(carol) groups=1003(carol),1003(opteam)如果你看到groups里没有预期的组,先确认是不是忘了-a,再确认修改后是否重新登录——附组信息在登录时写入进程凭证,usermod之后已经在线的会话不会自动生效,需要重新登录或新开一个 shell。
5.3 搭建目录结构并设置属主
目录结构用一条mkdir -p就能建好,关键在于属主设置要逐个指定,不要图省事全局递归:
# 创建目录结构 sudo mkdir -p /srv/project/src /srv/project/logs /srv/project/shared # 顶层目录归 root 所有,属组为 devteam sudo chown root:devteam /srv/project # src 和 logs 归 alice 所有,属组 devteam sudo chown -R alice:devteam /srv/project/src sudo chown -R alice:devteam /srv/project/logs # shared 归 root 所有,属组 devteam(保持 root 属主以便配合粘滞位管理) sudo chown root:devteam /srv/project/shared为什么shared的属主要设成 root 而不是 alice?因为粘滞位保护的是“文件属主”和“目录属主”。目录属主如果设成 alice,那 alice 就获得了删除任意文件的能力,而 bob 依然受粘滞位约束,权限就不对称了。设为 root 后,两个开发成员地位平等,只有 root 能强制清理。
5.4 用数字法批量设置权限
属主设置完成后,用数字法一次性配好权限位。注意src用的是四位数2770,第一位2就是 SGID:
# 顶层:属主 rwx,属组 r-x,其他人无权限 sudo chmod 750 /srv/project # src:属主 rwx,属组 rwx,其他人无权限 + SGID sudo chmod 2770 /srv/project/src # logs:属主 rwx,属组 r-x,其他人无权限 sudo chmod 750 /srv/project/logs # shared:属主 rwx,属组 rwx,其他人无权限 + 粘滞位 sudo chmod 1770 /srv/project/shared执行完用ls -ld逐个确认,输出应该是这样的:
$ ls -ld /srv/project /srv/project/src /srv/project/logs /srv/project/shared drwxr-x--- 4 root devteam 4096 Oct 12 11:20 /srv/project drwxrws--- 2 alice devteam 4096 Oct 12 11:20 /srv/project/src drwxr-x--- 2 alice devteam 4096 Oct 12 11:20 /srv/project/logs drwxrwx--T 2 root devteam 4096 Oct 12 11:20 /srv/project/shared重点看三处:src的属组位显示为rws,说明 SGID 生效;shared的其他人位显示为大写T,说明粘滞位生效但其他人没有执行权限(如果是t则表示其他人也有x)。这两种显示都是正确的,T和t的区别只在于其他人是否有执行位,不影响粘滞功能。
5.5 逐角色验证访问效果
配完不等于对,一定要验证。用sudo -u 用户名模拟不同身份执行操作,比反复su切换干净得多:
# bob 能否进入 src 并创建文件 $ sudo -u bob touch /srv/project/src/bob_test.txt $ ls -l /srv/project/src/bob_test.txt -rw-rw-r-- 1 bob devteam 0 Oct 12 11:30 /srv/project/src/bob_test.txt注意文件的属主是bob,但属组是devteam而不是bob的私有组,这正是 SGID 的作用。不过权限是664而不是666,原因是 bob 的umask是022。要让团队成员互相可写,需要在~/.bashrc或全局配置里设置umask 002,这一点在实际项目中很容易遗漏,导致“明明设了 SGID 还是写不了别人的文件”。
继续验证其他行为:
# bob 在 logs 目录写入应该失败 $ sudo -u bob touch /srv/project/logs/test.log touch: cannot touch '/srv/project/logs/test.log': Permission denied # alice 在 shared 建文件,bob 尝试删除应该失败 $ sudo -u alice touch /srv/project/shared/alice_doc.txt $ sudo -u bob rm /srv/project/shared/alice_doc.txt rm: cannot remove '/srv/project/shared/alice_doc.txt': Permission denied # bob 删除自己在 shared 里的文件应该成功 $ sudo -u bob touch /srv/project/shared/bob_doc.txt $ sudo -u bob rm /srv/project/shared/bob_doc.txt这几条验证覆盖了三个核心语义:目录w与x的配合、目录写权限与粘滞位的冲突处理、SGID 的属组继承。如果你跑出来的结果和预期不一致,先回看第 6 节的排查清单。
5.6 用 ACL 补足“一对多”授权
现在处理carol的需求。基础权限位只有三个槽位,没法表达“额外再来一个人”的授权。硬要把carol塞进devteam组会带来过度授权(她能访问src和logs),不符合最小权限原则。标准做法是使用 ACL(Access Control List)给单个用户单独授权:
# 让 carol 能进入项目目录和 shared 目录 sudo setfacl -m u:carol:x /srv/project sudo setfacl -m u:carol:rwx /srv/project/shared # 查看 ACL 规则 $ getfacl /srv/project/shared # file: srv/project/shared # owner: root # group: devteam # flags: --t user::rwx user:carol:rwx group::rwx mask::rwx other::---设置完成后,ls -ld的权限串末尾会多一个+号,提示该对象存在 ACL。用sudo -u carol ls /srv/project/shared验证可以访问,再确认她进不去src目录,授权边界就完整了。
ACL 有几个细节需要留意。第一,setfacl的-m是修改,-x是删除单条规则,-b是清除全部 ACL;第二,ACL 规则里有mask项,它相当于 ACL 的“权限上限”,新加的规则权限如果超过 mask 会被截断,必要时用setfacl -m m::rwx调整;第三,ACL 依赖文件系统支持,ext4 和 xfs 默认都支持,但如果遇到Operation not supported,需要检查挂载参数里是否有acl(较新的内核通常默认开启)。团队规模较大、授权关系复杂时,ACL 比反复建组要清晰得多,但规则数量一多同样会带来维护成本,建议在文档里记录每条规则的存在原因。
6. 常见问题与排查思路
6.1 高频报错速查表
权限问题虽然表现千变万化,归类之后其实就那么几种。先给一张速查表,遇到问题时先对号入座,再进入后续小节深挖:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
Permission denied但文件权限看起来是rwx | 路径上某一级父目录缺少x权限 | 用namei -l逐级检查整条路径 |
| 文件自己改不了 | 属主不是自己 | chown到正确属主,或用sudo编辑 |
./script.sh: Permission denied | 缺少执行位 | chmod u+x script.sh |
| 新建文件权限和预期不符 | umask影响 | 检查umask并调整配置 |
| 组内成员无法写同组文件 | 文件权限是664但属组不对,或umask为022 | 检查 SGID 与umask |
chmod成功但仍访问不了 | SELinux 上下文不匹配或 ACL 限制 | getenforce、getfacl检查 |
| 挂载的 U 盘文件全是 777 | FAT/exFAT 无 Unix 权限位 | 通过mount -o uid,gid,umask控制 |
/tmp下删不掉别人的文件 | 粘滞位在起作用 | 属于正常设计,需要 root 处理 |
6.2 逐级检查目录的执行权限
“权限明明给了却访问不了”是第一大类问题,根因几乎都是父目录的x位。假设你给/data/app/config.yml设了644,属主也对,但普通用户就是读不到。这时候不要盯着文件看,用namei把整条路径打出来:
$ namei -l /data/app/config.yml f: /data/app/config.yml drwxr-xr-x root root / drwxr-xr-x root root data drwx------ root root app -rw-r--r-- root root config.yml一眼就能看出问题:/data/app是700,其他用户根本没有进入权限,后面的文件权限再宽松也没用。修复方式是把目录调整为755或750,或者把需要访问的用户加入属组。
类似的还有另一种情况:目录有r没有x。这时ls 目录能看到文件名,但ls -l 目录会显示所有元数据为问号,cat 目录/文件名也会失败。这种配置在实际环境中很少是故意的,多半是某次chmod敲错了数字,用ls -ld逐层确认即可定位。
6.3 umask 导致的新文件权限异常
第二类高频问题是“权限配好了,新建的文件又不对了”。这基本可以锁定为umask。典型场景是团队共享目录:管理员精心设置了2770和 SGID,结果成员新建的文件还是644,同组其他人写不了。
排查步骤很直接,先用当前用户执行umask看值,如果是022,那就是它把组的写位掩掉了。验证方法:
$ umask 0022 $ touch /srv/project/src/demo.txt $ ls -l /srv/project/src/demo.txt -rw-r--r-- 1 alice devteam 0 Oct 12 12:00 /srv/project/src/demo.txt属组已经是devteam(SGID 生效),但权限是644而不是664。解决方式是在相应用户的~/.bashrc里加一行umask 002,或者对整个项目组统一在/etc/profile.d/下放一个脚本设置。改完记得让用户重新登录或source一下。
需要提醒的是,umask不是“设置权限”,而是“屏蔽权限”,它的值越大限制越严。把umask改成000会得到666/777的默认权限,这在多用户环境中非常危险,不要为了省事这么干。合理的做法是按目录职责区分:私有目录077,团队共享目录002。
6.4 SELinux 与 ACL 造成的“权限看着没问题却访问不了”
在 RHEL / CentOS 系上,还有一个容易被忽略的因素是 SELinux。它的检查发生在传统权限位之后,也就是说ls -l显示777,SELinux 依然可能拦截。判断方法很简单:
$ getenforce Enforcing如果是Enforcing且传统权限确认无误,去看审计日志:
sudo tail -f /var/log/audit/audit.log # 或者用 sealert 做可读化分析(需安装 setroubleshoot-server)日志里出现denied且带avc:前缀,基本就是 SELinux 拦的。处理方式有两种:一是调整文件的安全上下文(chcon或restorecon),二是用semanage fcontext添加持久化规则。不建议直接setenforce 0关掉 SELinux,那是把整层防护拆了来规避一个配置问题,安全代价太大。
另一类“看着没问题”的情况是 ACL。ls -l权限串末尾有+号就说明存在 ACL 规则,此时显示的rwx只是 ACL 的一个投影,真实规则要用getfacl看。曾经遇到过这样的案例:文件权限644,属主和属组都对,但用户就是写不了,getfacl一看mask::r--,把所有 ACL 里的写权限都截断了。这类问题不看 ACL 是永远查不出来的。
7. 最佳实践与工程建议
7.1 最小权限原则的落地方式
最小权限原则说起来简单——只给完成工作所必需的最小权限——但落地需要具体手段。第一条是按角色建组,不要按人授权。给每个人单独chmod或setfacl看着灵活,实际上一旦人员变动就会留下大量僵尸权限,而且没人说得清某条规则为什么存在。正确的做法是把权限绑定到组,人员进出只改组成员关系。
第二条是能用 750 就不用 777。777意味着任何能登录这台机器的账号(包括将来可能被创建的低权限服务账号)都能写入该目录,这在容器化环境中尤其危险,因为容器内的进程可能以任意 UID 运行。真正需要“所有人可写”的场景,用1777加粘滞位,或者用2770限定组范围,都比777安全得多。
第三条是服务账号不要用 root 运行。给应用创建专用的系统账号,只把必要的目录授权给它,例如chown -R appuser:appuser /opt/myapp/data。这样即使应用被攻破,攻击者拿到的也只是一个权限受限的账号。
7.2 一套可复用的权限基线
如果团队没有明确的权限规范,很容易出现“谁配的谁说了算”的混乱局面。建议在项目里维护一份权限基线,作为 Code Review 的一部分。下面是一份可以直接参考的基线:
| 对象类型 | 属主:属组 | 权限 | 说明 |
|---|---|---|---|
| 应用根目录 | root:appgroup | 750 | 只有 root 能改结构 |
| 可执行脚本 | appuser:appgroup | 750 | 属主可执行,组可执行 |
| 普通配置文件 | appuser:appgroup | 640 | 组内可读,其他人不可读 |
| 含密码的配置 | appuser:appgroup | 600 | 仅属主可读 |
| 日志目录 | appuser:appgroup | 750 | 属主可写,组内可读 |
| 上传/临时目录 | appuser:appgroup | 2770 | SGID 保证属组一致 |
| 多用户共享目录 | root:sharedgroup | 1770 | 粘滞位防误删 |
| 用户家目录 | user:user | 700 | 默认私有 |
把这张表落到部署脚本里,好处是每次发布都能得到一致的权限状态,不会因为某次手工操作让权限悄悄漂移。同时建议在 CI 或巡检脚本里加一条检查,发现权限偏离基线就告警。
7.3 sudo 与 root 的使用边界
日常操作中,很多人习惯直接su -切到 root 干活,这其实是个坏习惯:所有操作都没有审计痕迹,出问题查不到是谁做的。更推荐用sudo,它有三个明显优势——按命令授权、有日志记录、有时间戳缓存。
配置sudo时通过visudo编辑(它会做语法检查,避免配错导致所有人用不了 sudo),并且只授予具体命令,不要图省事写ALL=(ALL) NOPASSWD: ALL:
# 错误示范:权限过大,且无审计价值 devops ALL=(ALL) NOPASSWD: ALL # 推荐示范:只允许重启指定服务,且仍需输入密码 devops ALL=(root) /bin/systemctl restart nginx, /bin/systemctl status nginx如果确实有脚本需要 sudo 执行,也要评估脚本本身是否可写。曾经出现过这样的问题:给某个运维脚本配了免密 sudo,但脚本文件所在目录对普通用户可写,结果任何能改脚本的人都能借助 sudo 提权。被 sudo 调用的脚本,其路径上每一级目录都必须只有 root 可写,这个检查很容易被漏掉。
7.4 权限变更的审计与回滚
权限变更最怕两件事:一是改错范围,二是改完忘了。针对第一点,chmod -R和chown -R是最容易出事的命令,执行前先用find确认范围:
# 先看会影响到哪些文件,确认无误再执行 find /srv/project -type f | head -20 find /srv/project -type f | wc -l如果数量远超预期,说明路径写错了,赶紧停手。针对第二点,建议在批量变更前先做一次权限快照:
# 记录当前权限,便于出问题时对照恢复 find /srv/project -printf '%M %u:%g %p\n' > /tmp/perm_backup_$(date +%F).txt这个快照不像数据库备份那样严谨,但足以在误操作后手工回滚,成本极低,值得养成习惯。另外,权限变更应该走变更流程:评估影响面、在测试环境验证、选择低峰窗口执行、变更后验证关键功能,这四步和发布代码没有本质区别。
8. 延伸学习路线与动手建议
8.1 下一步该学什么
基础权限搞清楚之后,可以按下面这条线继续深入。第一步是把用户与用户组体系吃透,包括/etc/passwd、/etc/shadow、/etc/group三个文件的字段含义,useradd的默认配置文件/etc/default/useradd与/etc/login.defs,以及su与sudo在凭证传递上的差异。很多权限问题的根源其实在账号体系而不是权限位。
第二步是特殊权限与文件属性。本文讲了 SUID、SGID、Sticky,还可以继续了解chattr的不可变属性(+i),它能在 root 权限之上再加一层保护,适合用在关键配置文件和审计日志上。第三步是ACL 与 SELinux,前者解决复杂授权,后者是强制访问控制,两者在日常运维中都会遇到。第四步可以接触文件系统挂载选项,像noexec、nosuid、nodev这些参数在安全加固中使用频率很高,比如给/tmp加noexec就能防止有人往临时目录扔可执行程序。
8.2 建议马上动手验证的三个实验
光看不练记不住,建议在测试机上把下面三个实验过一遍,每个都能带来新的认知。
实验一:验证目录权限的短路特性。创建一个目录设为700,在里面放一个777的文件,然后用另一个用户尝试读取。你会发现文件权限完全没问题,但依然失败。这个实验能让你彻底记住“父目录x位”这条规则。
实验二:验证删除文件看的是目录写权限。让两个用户共同操作一个2770的目录,其中 A 创建文件并chmod 444,然后让 B 尝试删除。你会发现 B 删得掉,因为 B 对目录有写权限。接着给目录加上粘滞位再试一次,这次就删不掉了。两次对比能把“文件权限”和“目录权限”的边界分得很清楚。
实验三:观察 umask 的实际效果。分别在umask 022和umask 002的会话里执行touch file和mkdir dir,然后ls -l对比结果。这个实验只需要两分钟,但能让你以后看到“新建文件权限不对”时立刻想到该查哪里。
权限这东西,配的时候多花五分钟想清楚,比出问题后排查两小时划算得多。把基线写进部署脚本,把检查加进巡检任务,剩下的就交给系统去执行——它比你想象中守规矩。