1. 从一次深夜报错说起:用户与权限为什么是绕不过去的坎
刚接手 Linux 服务器的人,十个里有八个第一次被卡住的地方不是命令记不住,而是屏幕上那句冷冰冰的Permission denied。我当时的第一反应是"这文件明明在我面前,怎么就动不了",然后习惯性地开个 root 一把梭,问题当场消失,隐患也就此埋下。后来项目做大,多人共用一台机器,有人误删了别人的数据,有人部署的服务因为属主错乱起不来,才回过头认真补上Linux 用户、用户组和文件权限这一课。
这篇内容我想把这条线一次讲透:Linux 里用户是怎么被定义的、权限位那九个字符到底在说什么、chmod和chown的数字是怎么算出来的,以及最关键的——遇到Permission denied时应该按什么顺序去查。它适合刚接触 Linux 的同学,也适合已经能敲命令但总在权限上翻车的开发者。我不会只给你一堆命令清单,而是把每个参数背后的理由讲清楚,让你下次见到报错能自己定位,而不是靠搜索引擎撞运气。
2. 用户、用户组与权限的底层模型
2.1 Linux 为什么一定要做成多用户系统
很多人把 Linux 当成"单机系统"来用,装了桌面环境,开机就进自己的账号,感觉和 Windows 差不多。但 Linux 从设计的第一天起就是为多人共用一台机器准备的:上世纪的大型机时代,几十个终端接到同一台主机上,凭什么保证 A 的操作不会把 B 的数据搞坏?答案就是用户身份 + 权限校验这套机制。
具体来说,内核在做任何文件操作前,都会先问自己三个问题:当前发起操作的进程是谁(uid 是多少)、它属于哪些组(gid 和附加组)、目标文件允许谁做什么(权限位)。三个问题答完,才决定放行还是返回EACCES,也就是我们看到的Permission denied。这就是为什么同一个文件,你用 A 账号能改,用 B 账号就改不动——不是文件有问题,是身份不匹配。
理解了这一层,你就不会再把"权限"当成一个配置项,而会把它当成系统安全的第一道闸门。这也是为什么生产环境严禁所有人共用 root,以及为什么容器、CI 流水线都要专门建一个非特权用户跑服务。
2.2 三个关键文件:passwd、shadow、group
用户信息不是存在内存里的,而是落在三个纯文本文件里,它们的分工很明确:
| 文件 | 存什么 | 关键字段 | 权限 |
|---|---|---|---|
/etc/passwd | 用户基本档案 | 用户名:x:UID:GID:描述:家目录:登录Shell | 644,所有人可读 |
/etc/shadow | 密码哈希与过期策略 | 用户名:哈希:最后一次改密天数:最小间隔:最大有效:警告天数 | 640 或 000,仅 root 可读 |
/etc/group | 组与组成员 | 组名:x:GID:成员列表 | 644 |
/etc/passwd里第二字段那个x是个历史遗留的占位符,真正的密码哈希早就搬到/etc/shadow里去了。原因很直白:早期密码哈希就放在 passwd 里,而这个文件必须全局可读(很多程序要靠它做 uid 到用户名的翻译),等于把哈希暴露给所有人做离线爆破。分家之后,普通程序读 passwd 拿身份映射,只有 root 能读 shadow 校验密码,安全性立刻提升一个档次。
这里有个新手常踩的点:UID 为 0 的账号就是 root,不管它叫什么名字。有些加固规范会建议把真正的 root 改名,再建一个假的 root 账号(UID 非 0)当诱饵,原理就在这——内核只认数字,不认字符串。
2.3 用户组的两副面孔:主组和附加组
每个用户有且仅有一个主组(primary group),记录在/etc/passwd的第四字段;同时可以有零到多个附加组(supplementary groups),记录在/etc/group的成员列表里。判断权限时,内核会把主组和所有附加组一起拿来比对。
这个设计解决的是"一个人要同时参与多个项目"的场景。比如你既在dev组,又在ops组,那两个组共享的目录你都能进,不需要来回切换账号。反过来,如果你只给某人加错了组,他就会莫名其妙地被某个目录拒之门外——这是排查Permission denied时第一个要确认的地方。
id命令可以一次性把这几层关系摊开给你看:
$ id alice uid=1000(alice) gid=1000(alice) groups=1000(alice),27(sudo),1001(dev)再配合groups、whoami,基本就能确认"当前这个进程到底以什么身份在跑"。注意,附加组的变更是要重新登录(或者新开会话)才生效的,因为进程启动时组列表就已经固定下来了。我见过太多人usermod -aG加完组,在当前终端里怎么试都是Permission denied,新开一个 ssh 会话就好了——不是命令没生效,是当前 shell 还没更新组信息。
3. 把那串-rwxr-xr-x彻底读懂
3.1 九个字符的三段结构与八进制换算
ls -l输出里最长的那一串,其实是十位:第一位是文件类型,后面九位才是权限。
$ ls -l deploy.sh -rwxr-xr-x 1 alice dev 2183 Aug 12 09:14 deploy.sh第一位:-普通文件,d目录,l符号链接,c字符设备,b块设备,ssocket,p命名管道。后面九位分成三组,分别对应属主(user)、属组(group)、其他人(other),每组三位依次是r(读,4)、w(写,2)、x(执行,1)。没有权限的位置用-占位。
于是rwxr-xr-x换算成八进制就是:属主4+2+1=7,属组4+0+1=5,其他人4+0+1=5,合起来755。常用组合其实就那么几个:
| 八进制 | 符号形式 | 典型用途 |
|---|---|---|
| 644 | rw-r--r-- | 普通文本、配置、网页文件 |
| 600 | rw------- | 私钥、~/.ssh/id_rsa、含密码的配置 |
| 755 | rwxr-xr-x | 目录、可执行脚本 |
| 700 | rwx------ | 个人私有目录、.ssh目录本身 |
| 750 | rwxr-x--- | 组内共享、对外封闭的目录 |
| 775 | rwxrwxr-x | 团队协作目录,同组可写 |
| 777 | rwxrwxrwx | 除非做实验,否则不要出现 |
换算的逻辑值得记牢而不是背表:把读、写、执行分别当成 4、2、1 三个砝码,需要哪个就加哪个。这样你看到任意一个三位数都能反推出权限,反之亦然。
3.2 chmod 的两种写法,什么时候用哪种
chmod支持符号模式和数字模式两种写法。
符号模式的结构是[对象][操作符][权限],对象取u/g/o/a,操作符取+/-/=:
chmod u+x deploy.sh # 给属主加执行权 chmod go-w notes.txt # 去掉属组和其他人的写权 chmod g=rx shared.log # 属组精确设为读+执行 chmod a+r public.html # 所有人可读数字模式则是直接覆盖全部九位:
chmod 644 app.conf chmod 755 /opt/scripts chmod 600 ~/.ssh/id_rsa我的使用习惯是:知道目标状态就用数字,只做增量微调就用符号。比如给一个脚本加执行权,chmod +x最直观;但给一堆配置文件统一收紧,chmod 644更不容易漏。要注意chmod 644 dir这种写法作用在目录上是灾难性的——目录没了x位就进不去,等于把自己锁在门外,这个坑后面还会细说。
3.3 chown 与 chgrp:改属主才是治本的手段
很多Permission denied的根因不是权限位不够,而是文件的属主压根就不是你。这时候chmod加权限只是治标,正确做法是改属主。
chown alice app.log # 只改属主 chown alice:dev app.log # 属主和属组一起改 chown :dev app.log # 只改属组 chown -R www-data:www-data /var/www/html # 递归改整个目录树-R这个参数要格外小心。它会把子目录、子文件全部改成同一个属主和属组,包括那些本来不该动的(比如用户上传目录里的文件、.git目录)。另外递归改属组时,如果希望新文件能继承目录的组,更稳妥的方案是给目录加SGID 位(见下一节),而不是反复手动chown -R。
顺带说一个容易混淆的点:chown -R user:group dir之后再往里新建文件,属组默认是创建者的主组,不是目录的属组。想让新文件自动归到目录的组,就必须给目录设 SGID。
3.4 特殊权限位:suid、sgid、sticky
除了九位常规权限,还有三个特殊位藏在同一个字段里,分别占八进制的高位 4、2、1。
SUID(4):作用在可执行文件上,任何用户执行它时,进程的有效 UID 会临时变成文件属主。最典型的就是passwd命令——它是 root 所有,但因为设了 SUID,普通用户也能通过它去改自己的密码、间接写/etc/shadow。
$ ls -l /usr/bin/passwd -rwsr-xr-x 1 root root 68208 ... /usr/bin/passwd注意属主那组的x变成了s。SUID 是提权漏洞的高发区,自己写脚本时不要随便加,尤其是 shell 脚本——绝大多数内核会直接忽略脚本上的 SUID 位,这本身就是一道安全防护。
SGID(2):作用在文件上时,进程有效 GID 变为文件属组,用法和 SUID 类似但更少见;作用在目录上时,它会让该目录下新建的文件和子目录自动继承目录的属组。这个特性在团队协作目录里非常实用:
chmod 2775 /srv/project # 目录设 SGID设完之后,无论谁往里建文件,属组都固定是project组,同组的人都能读写,省掉了无数次chown :project。
Sticky(1):只对目录有意义,效果是目录里的文件只有属主本人(或 root)才能删除或重命名。/tmp就是标配:
$ ls -ld /tmp drwxrwxrwt 12 root root 4096 ... /tmp其他人那组的x变成了t。没有它,任何人只要对/tmp有写权限,就能删掉别人正在用的临时文件——这就是一个共享目录从"能用"到"敢用"的分界线。
4. Permission denied 的分类排查手册
4.1 先分清是文件权限还是目录权限
报错信息通常长这样:
$ cat /var/log/app/error.log cat: /var/log/app/error.log: Permission denied新手会直接chmod 644 /var/log/app/error.log,改完发现还是不行。原因是访问一个文件要经过它的每一级父目录,任何一级不让你通过,后面的权限再宽松也没用。这时候namei -l是最省事的工具,它会把整条路径上每一层的权限列出来:
$ namei -l /var/log/app/error.log f: /var/log/app/error.log drwxr-xr-x root root / drwxr-xr-x root root var drwxr-xr-x root root log drwx------ root root app <-- 问题在这里 -rw-r--r-- root root error.log一眼就能看出是/var/log/app这个目录把其他人挡在外面了。没有namei的环境,用ls -ld一层层往上敲也能达到同样效果,只是慢一点。
4.2 目录的 r、w、x 到底各自管什么
目录权限和文件权限的语义完全不同,这个差异是绝大多数误判的来源:
- r(读):允许列出目录内容,也就是
ls能看到文件名。 - w(写):允许在目录内创建、删除、重命名文件。
- x(执行):允许"进入"目录,也就是能访问目录内的文件、能
cd进去。
关键在于:删除一个文件靠的是目录的 w 权限,而不是文件本身的 w 权限。这就是为什么你在共享目录里可以删掉别人的文件(如果目录给了你写权限),却改不动它的内容。理解了这点,/tmp要加 sticky 位、共享上传目录要小心设计,就都顺理成章了。
还有一个反直觉的组合:只有 x 没有 r 的目录。你可以直接cd进去、可以访问已知文件名的文件,但ls会报Permission denied。这在某些只允许按固定路径取文件的场景里是有意为之的设计。
4.3 权限位之外的那些坑
权限位对上了还是报Permission denied?那就要往更外层找。以下是几个我实际踩过的方向:
SELinux / AppArmor。CentOS、RHEL 系默认开 SELinux,文件权限完全正确,安全模块照样拦你。判断方式是看/var/log/audit/audit.log有没有 AVC 记录,或者临时用getenforce看状态。修复手段通常是restorecon -Rv /path恢复正确的上下文,而不是直接把 SELinux 关掉。
挂载选项。findmnt -T /srv/data能看到挂载参数,如果带ro就是只读挂载,怎么chmod都没用;noexec会让脚本即使有x位也执行不了;nosuid会让 SUID 位失效。
文件系统属性。lsattr能看到i(不可变)和a(只追加)这类标志,被chattr +i锁过的文件,root 都改不动,必须先chattr -i。
POSIX ACL。getfacl能看到比ls -l更细的授权规则,有些目录的权限是通过 ACL 而不是传统三位组给的,只看ls -l会漏判。对应的命令是setfacl -m u:alice:rwx dir。
磁盘满或配额超限。写入时磁盘空间耗尽,也可能以权限类错误的形式表现出来,配合df -h和df -i(inode 用尽)一起看。
远程登录的Permission denied, please try again。这个和文件权限完全是两码事,它是 SSH 认证失败——密码错了、PermitRootLogin被禁、AllowUsers白名单没包含你,或者密钥没被authorized_keys接受。排查方向应该是/etc/ssh/sshd_config和认证日志,而不是去改文件权限。
4.4 一张报错速查表
把常见现象和对应动作整理成表,下次直接对号入座:
| 现象 | 最可能的原因 | 首选动作 |
|---|---|---|
| 新建文件同级目录下报无法创建 | 目录缺 w 或 x | ls -ld看目录权限 |
cd进不去目录 | 目录缺 x | chmod +x或chmod 755 |
ls列不出目录内容 | 目录缺 r | chmod +r |
| 能读不能写文件 | 文件缺 w,或属主不是你 | 先确认id,再决定chown还是chmod |
| 删不掉别人的文件 | 目录有 w 但被 sticky 保护 | 用属主账号删,或由管理员处理 |
| 脚本有 x 位仍执行失败 | 挂载了noexec,或 shebang 路径错 | findmnt -T检查挂载参数 |
| 改完权限依然拒绝 | SELinux 拦截 | 看 audit 日志,restorecon |
| 服务启动报无法写日志 | 服务账号对日志目录无权限 | 建专用用户并chown日志目录 |
SSH 登录报Permission denied | 认证失败,非文件权限 | 查 sshd 配置和认证日志 |
5. 从零搭一套可用的用户与权限方案
5.1 建用户:几行命令背后的取舍
sudo useradd -m -s /bin/bash -c "Deploy account" deploy sudo passwd deploy sudo usermod -aG www-data deploy-m会创建家目录并拷贝/etc/skel的骨架文件(.bashrc、.profile等),不写它的话家目录不会自动建,登录后一堆工具会报找不到配置文件。-s指定登录 shell,如果是纯服务账号,可以设成/usr/sbin/nologin,从源头堵死登录可能。-c是备注信息,写清楚用途,半年后回来看还能认出来。
usermod -aG里的-a是 append,漏掉它会覆盖掉用户原有的附加组,把一个还在sudo组里的人踢出去,这种事故我亲眼见过。改完组记得让用户重新登录,或者用newgrp临时切一下。
想确认结果,用id deploy和groups deploy各看一眼,比翻三个文件快得多。删除用户时,userdel默认保留家目录,要连家目录一起清掉得加-r,但生产环境我建议先保留几天再手工清理,免得误删数据。
5.2 权威原则:最小权限与专用账号
有一条原则值得刻在脑子里:每个长期运行的服务,都用独立的、权限刚好够用的非特权账号跑。数据库一个账号,Web 服务一个账号,部署脚本一个账号,彼此不共享。这样做的好处是万一某个服务被攻破,攻击者拿到的是一个几乎什么都干不了的 shell,而不是整台机器。
落到具体操作上,一个 Web 服务的典型权限布局是这样的:
sudo useradd -r -s /usr/sbin/nologin -d /var/www/app www-app sudo chown -R www-app:www-app /var/www/app sudo find /var/www/app -type d -exec chmod 750 {} \; sudo find /var/www/app -type f -exec chmod 640 {} \;注意这里对目录和文件用了不同的权限:目录 750,文件 640。原因是目录需要 x 才能进入,而文件不需要(脚本类的可执行文件单独处理)。一刀切chmod 755 -R会让所有配置文件都变成其他人可读,把数据库密码暴露给任何能登录机器的账号。
5.3 umask:新文件的默认权限从哪来
每次新建文件,权限都不是凭空来的,而是基准权限减去 umask。文件基准是 666,目录基准是 777。默认 umask 022 时,新文件是 644,新目录是 755。
日常最需要改 umask 的场景是团队协作目录:希望新文件同组可写,就得把 umask 调成 002。
umask 002 # 当前会话生效 echo 'umask 002' >> ~/.bashrc # 持久化在/etc/profile或/etc/login.defs里统一配置可以让所有用户生效。另外,目录上加 SGID 位配合 umask 002,是团队目录最稳的组合拳:组继承有 SGID 保证,组内可写有 umask 保证,两个都做对了,基本不用再事后补救。
5.4 sudo:把 root 权限关在笼子里
生产环境直接用 root 登录是禁忌,正确姿势是给普通账号配 sudo,并且只授权必要的命令。
sudo visudo -f /etc/sudoers.d/deploy在里面写:
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart app %ops ALL=(ALL) ALL第一行只允许 deploy 用户免密重启指定服务,其他什么都干不了;第二行让 ops 组拥有完整 sudo,行首的%表示这是组名。用visudo而不是直接编辑文件,是因为它会做语法校验,写错了会拦下来——直接编辑/etc/sudoers写崩了,你就失去了唯一的提权入口。
排查 sudo 相关问题时,sudo -l能列出当前用户被授权执行哪些命令,比翻配置文件快得多,也是"我明明配了为什么还是被拒"这类问题的第一站。
6. 踩过的坑与长期维护经验
6.1 chmod -R 777:看起来最省事,代价最大
chmod -R 777是新手遇到权限问题的万能解药,也是我见过最贵的一行命令。它把所有文件的权限位全部打开,任何能登录机器的账号都能改你的代码、替换你的配置文件、往脚本里塞一行反向 shell。更糟的是,它破坏了"权限能反映意图"这个信息——三个月后你再看到这个目录,完全不知道哪些文件本该是可执行的,哪些本该是只读的。
如果已经这么干过,补救办法不是拍脑袋改回去,而是从另一个正常环境或备份里拷贝一份ls -lR的输出做对比,逐条恢复。系统目录的默认权限一般能通过重装对应的软件包来找回(包管理器会带上正确的权限信息)。
6.2 umask 与 SGID 的配合,能省掉大量重复劳动
前面提过,这里再强调一次,因为它是真正一劳永逸的做法。团队目录的正确开局是三步:
sudo chown -R :dev /srv/project sudo chmod 2775 /srv/project echo 'umask 002' | sudo tee -a /etc/profile.d/dev-umask.sh这样一来,只管把文件往目录里丢,属组和权限自动就是对的,不需要每次部署后跑一条chown -R。反过来,如果先建了一堆文件再补 SGID,那些老文件是不会自动改属组的,还得手工修一遍。
6.3 权限修复的常规流程
线上遇到权限问题,我一般按这个顺序走,避免越修越乱:
第一步,id确认当前身份和组列表,同时确认会话是不是最新的(必要时重开终端)。 第二步,namei -l或逐级ls -ld看完整路径上每一层的权限和属主。 第三步,stat看文件的精确权限、属主、属组和三个时间戳。 第四步,如果权限看着没问题,getfacl看 ACL,lsattr看扩展属性,findmnt -T看挂载选项,getenforce看安全模块。 第五步,定位清楚后再动手,改完立刻复测,并把改动记录到变更单里。
这个流程的意义在于先诊断后治疗。直接动手改权限,往往会掩盖真实原因,过两天换个场景又犯。
6.4 面试里关于用户和权限的高频考法
这类题目在基础面试里出现频率很高,我整理了几个常见问法和自己习惯的回答角度:
| 问题 | 回答的要点 |
|---|---|
chmod 755和chmod u=rwx,go=rx等价吗 | 等价,但后者更明确表达意图 |
| SUID 有什么用、有什么风险 | 临时提升有效 UID,是提权漏洞高发点,脚本上通常无效 |
为什么/tmp是 1777 | sticky 位防止用户互删文件 |
| 用户加了组为什么不生效 | 进程组列表在会话启动时固定,需重新登录 |
| 怎么让新文件自动继承目录属组 | 目录设 SGID(2775) |
| 删文件看文件权限还是目录权限 | 看目录的 w 权限 |
| 怎么查一个路径上哪一级卡住了 | namei -l |
回答时如果能顺带说清楚"为什么",而不是只报命令,通常能加分不少。
7. 我个人的几条长期习惯
说几条自己这些年养成的小习惯,都不是什么高深技巧,但确实省了很多事。
先看再改。任何时候动手改权限之前,先把ls -l和id的输出贴出来看一眼。养成这个反射之后,绝大部分问题在敲下chmod之前就已经定位清楚了。
给关键目录写注释。在/etc/sudoers.d/的自定义文件和/srv目录下放一个README,写清楚谁应该是什么权限、为什么这么设。权限设计一旦隔了半年,连写它的人都会忘。
密钥文件永远 600,.ssh目录永远 700。SSH 对这两个权限非常挑剔,权限太松会直接拒绝使用密钥,而这个报错往往含糊其辞,容易让人往错误方向排查。
生产环境的权限变更走记录。用getfacl -R /path > acl.bak在变更前存一份快照,出问题能一键setfacl --restore回滚。这个操作成本极低,但真的救过我一次。
尽量不用 root 做日常操作。需要提权时用 sudo,让每一条高权限命令都留下日志。这既是安全要求,也是出事之后唯一能还原现场的依据。
权限这件事,本质上是在回答"谁、能在什么范围内、做什么"。把这个问题想清楚了,Permission denied就不再是一个让人烦躁的报错,而是一条准确的提示——它在告诉你,你的身份和这个操作之间,还差一个明确的授权关系,找到它、补上它,问题就结束了。