1. 先搞清楚“我是谁”:用户身份与UID/GID
1.1 为什么Linux用数字记人:UID/GID与passwd文件
很多人在Windows下用习惯了图形界面,刚转Linux时遇到权限报错,第一反应是“我是不是命令敲错了”。其实大部分权限问题都出在一个更基础的环节:Linux系统默认不认用户名,只认数字ID,这个数字就是UID(用户ID)和GID(组ID)。你在终端里输入
ls -l
看到的
root root
,系统真正拿去做判断的,是文件记录的UID 0和GID 0。
打开
/etc/passwd
,每一行就是一个用户账户,字段用冒号分隔。举个实际的例子:
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
第一列是用户名,第二列是密码占位符(真正的密码存在
/etc/shadow
里),第三列UID,第四列GID,后面是用户描述、家目录、登录Shell。你平时用
id
命令看到的,就是这些数字背后的映射关系:
id
uid=1000(zhang) gid=1000(zhang) groups=1000(zhang),4(adm),27(sudo)
这里面有个关键信息:用户所属的组分“主组”(GID列)和“附加组”(groups里除了主组之外的那些组)。判断权限时,系统除了看主组,还会把附加组全部拿进去比对。所以一个用户能不能读某个组权限的文件,不完全取决于他初始分配到哪个组,后补加的附加组同样有效。
1.2 root与普通用户:权限差距到底有多大
root的UID是0,在Linux里UID 0拥有最高特权。用一句话概括root和其他用户的差距:所有传统权限检查在UID 0面前都直接放行。文件是000权限?root照样能读;目录没有写权限?root照样能建文件;进程是别人的?root一样能kill。之所以设计成“UID 0特判”,是因为系统启动、修复、挂载、调整内核参数这些操作必须有一个不受普通权限约束的入口。
但这不意味着你应该整天用root干活。我见过不少部署环境,一上来就是
root@server
操作一切,结果真正出问题时反而更难排查,因为你根本不知道正常用户会遇到什么权限错误。最典型的例子:你用root把网站目录的属主全改成root、权限改成777,网站能跑起来,可一旦哪个php-fpm进程以普通用户运行,它就无法写日志、无法生成缓存文件,排查半天发现所有问题都是root“帮忙”绕过去的。
实际工作中,root只做两类事情:一是系统级维护(装软件、改系统配置、管理服务);二是收尾修复(普通用户搞不定,你切root看一眼然后归还环境)。日常操作能用普通用户就用普通用户,能用sudo就用sudo,这个习惯越早养成越好。
1.3 进程的身份:真正执行操作的不是你,是进程
这是权限问题里最容易被忽略的一点。你在终端里执行
rm file
,看起来是“你在删文件”,实际上真正发起删除操作的是bash这个进程,而bash进程的身份才是内核做权限判断的依据。如果你通过
sudo su
切换到root再执行操作,那么bash进程的UID是0;如果你用
sudo -u www-data
执行命令,那么该命令的属主身份就是www-data。
为了把这个问题讲透,需要引入两组概念:真实用户ID(real UID)和有效用户ID(effective UID)。正常情况下两者相同,都是你登录时的UID。但一旦涉及SUID权限(后面第4节会细说),有效UID就会临时变成文件属主的UID。内核在访问文件时,看的几乎都是有效UID。
所以排查权限问题时,第一步永远不是看“我是谁”,而是看“我当前这个进程是以什么身份在跑”。比如Docker容器里的进程,经常在宿主机看来是root,但容器内部的权限映射又是另一套规则,这就是为什么容器挂载卷时报Permission denied时,第一反应应该是看容器内进程的UID,而不是看宿主机的当前用户。
2. “我能干啥”的底层规则:权限位怎么读怎么算
2.1 rwx对文件和目录的含义完全不同
大部分新手在权限上踩的第一个坑,就是把对“文件”的理解直接套到“目录”上。
ls -l
看到
-rw-r--r--
,知道这是所有者可读可写、其他人只读;可看到
drwxr-xr-x
这种目录权限时,很多人就蒙了——目录的r、w、x到底各代表什么?
文件层面的r/w/x很直观:r代表可读取内容,w代表可修改内容,x代表可作为程序执行。目录是个完全不同的概念,它的r是“可列出目录内容”,w是“可在目录内创建、删除、重命名条目”,x是“可进入这个目录或穿越它去访问更深的路径”。
这里有个非常关键的实操结论:删除文件看的是“文件所在目录”的权限,不是文件本身的权限。只要你对某个目录拥有w和x权限,那么即使目录里的文件是
-r--r--r--
(只读),你也可以直接把它删掉或改名。这就是为什么很多人困惑“这个文件我明明没有写权限,为什么能删?”因为删除操作的判断对象是目录,不是文件。
同理,一个目录只有r权限没有x权限,你能
ls
看到文件名列表,但
cd
进不去,也无法
ls -l
查看详细属性。目录只有x权限没有r权限,你不知道里面有什么,但如果你恰好知道某个文件名,可以直接访问它。所以给网站/代码目录设置权限时,至少要有
r-x
,否则web服务连文件都读不到。
2.2 权限检查的顺序:属主、属组、其他人三选一
ls -l
输出最开始那10个字符,第一个是文件类型,后面9个分成三组,每组三位:文件属主(user)、文件属组(group)、其他所有人(other)。很多人以为权限是叠加的,比如自己是属主,就同时享有属主权限加上属组权限加上其他权限。大错特错。Linux的权限检查是“三选一”:进程的有效UID与文件属主UID相同,就只使用user位权限;否则看进程的有效GID是否匹配文件属组GID或附加组,匹配则只使用group位;都不匹配才落到other。
我举个例子:文件属主是root,属组是www-data,权限是
-rw-r-----
。你是www-data组成员,但不是root,那么你能做的操作就是读,因为group位是
r--
,而root那三位
rw-
跟你没有关系。反过来,如果这个文件group位是空的
---
,但other位是
r--
,你作为www-data组成员反而一点都读不了,因为匹配了group组之后就不会再落到other了。这个“只取一组、命中即止”的机制,让很多“以为叠加”的人栽过跟头。
所以在设计权限时,要提前想清楚三类身份的边界:属主是谁、属组是谁、第三方是谁。最常见的做法是把需要协作的用户都拉进同一个附加组,然后把文件属组设置成这个组,给group位开权限。
2.3 chmod 755/644的数值怎么来
你肯定见过
chmod 755
、
chmod 644
这种写法,这三个数字和rwx位的对应关系其实非常数学。r对应4,w对应2,x对应1,一个数字就是这个用户类别下三个值的和。比如要给属主“可读可写可执行”,就是4+2+1=7;要给属组“可读可执行”,就是4+1=5;要给别人“可读可执行”,同样5,于是
chmod 755
。
反过来,看到
-rw-r--r--
也能心算出数字:属主
rw-
=4+2=6,属组
r--
=4,other
r--
=4,对应644。这个换算练熟了,以后看文件权限几乎不用过脑子。常用组合我列一下:
| 数值 | rwx位 | 典型用途 |
|---|---|---|
| 777 | rwxrwxrwx | 所有人可写可执行,基本只用于临时共享,生产环境要慎用 |
| 755 | rwxr-xr-x | 可执行文件、目录,属主可写,其他人可读可执行 |
| 750 | rwxr-x--- | 目录或脚本,组内可读执行,外部不可见 |
| 644 | rw-r--r-- | 普通文件默认位,属主可写,其他人只读 |
| 640 | rw-r----- | 配置类文件,属主可写,组内可读 |
| 600 | rw------- | 私密文件,如ssh私钥,只允许属主读写 |
| 400 | r-------- | 只读文件,比如某些证书文件 |
注意一个细节:
chmod
不只支持数字,还支持符号模式,比如
chmod u+x file
给属主加执行位,
chmod g-w file
去掉属组的写位。生产环境要批量修改时,符号模式更直观,不容易因为记错数字而误改其他位。
3. 实操上手:用户、组、文件权限一站式配置
3.1 ls -l、chmod、chown、umask这几个命令的组合用法
先看
ls -l
的一个完整输出:
-rw-r--r-- 1 zhang dev 1024 Feb 20 10:30 report.pdf
drwxr-xr-x 2 zhang dev 4096 Feb 20 10:31 uploads
第一列是类型加权限,第二列是硬链接数(目录通常是子目录数加2),第三列是属主,第四列是属组,之后是大小、修改时间、文件名。看权限前要习惯性地用
ls -ld 目录名
查看目录自身的权限,因为
ls -l
直接看一个目录时,列出来的是目录里的内容,不是目录本身的权限。
日常调整权限最少会用到三个命令:
chown
改属主,
chgrp
改属组,
chmod
改权限。一条命令可以把前两个合并完成:
chown zhang:dev report.pdf # 同时把属主设为zhang,属组设为dev
chown -R zhang:dev /data/upload # -R递归修改目录下所有文件
chmod -R g+w /data/upload # 递归给属组加写权限
还有两个容易被忽略的小技巧:
chmod --reference=模板文件 目标文件
可以把目标权限直接改成和模板一样,适合批量统一;
chown --from=旧属主:旧属组 新属主:新属组 文件
可以只修改符合条件的文件,避免误改。这些命令的组合能应付九成以上日常需求。
再讲
umask
。你每次创建文件,系统都会用默认最大权限套上一个“权限掩码”。普通文件默认666,目录默认777,然后用
umask
值做按位取反再相与。示例:
umask 022
时,文件是
666 & ~022 = 644
,目录是
777 & ~022 = 755
。这就是为什么你新建的文件天然是
rw-r--r--
,新建的目录天然是
rwxr-xr-x
。
想要组内成员默认可以写?把
umask
改成002,文件就是664,目录就是775。在团队协作的服务器上,设置一个合适的
umask
往往比事后不断
chmod
更省心。修改方式是在
/etc/profile
或
~/.bashrc
里加一行
umask 002
,然后重新登录。
3.2 新建用户和用户组的最少踩坑配置
创建一个新用户的命令,很多人只敲一句
useradd zhangsan
,然后发现新用户登录后没有家目录,连命令提示符都怪怪的。这是因为在不同发行版上,
useradd
的默认行为差异很大。稳妥的做法是显式指定:
useradd -m -s /bin/bash zhangsan
passwd zhangsan
-m
表示创建家目录,
-s /bin/bash
指定登录Shell。之后把用户加进需要的组,比如加进sudo管理组(Ubuntu/Debian)或wheel组(CentOS/RHEL):
usermod -aG sudo zhangsan
# 或者
gpasswd -a zhangsan docker # 加入docker组,以便普通用户运行docker命令
注意
usermod -aG
里的
-a
表示追加,千万别漏掉,否则会把用户从其他附加组里踢出来。改完之后用户需要退出重新登录,或执行
newgrp
刷新组关系,否则当前会话里
id
看不到新增的组。
新建了用户之后,常见的下一步是规划目录。假设你有一个
/data/www
项目目录,想让zhangsan和另一个同事李四都可以编辑,但不想让第三方看,典型的做法是:
groupadd dev
usermod -aG dev zhangsan
usermod -aG dev lisi
chown -R root:dev /data/www
chmod -R 770 /data/www
# 也可以加setgid位,让目录里新建文件自动继承dev组
chmod g+s /data/www
这样不用给任何人root权限,也能实现多人协作,而且
/data/www
对外完全封闭。相比一上来
chmod 777
,这个方案安全太多。
3.3 用sudo做精细化授权,别再轻易切root
sudo的本质是:管理员定义“哪些用户、以哪些身份、能执行哪些命令”,然后在日志里留下审计记录。配置文件在
/etc/sudoers
,但强烈建议不要直接编辑这个文件,而是用
visudo
或在
/etc/sudoers.d/
下放一个独立文件。这样既避免语法错误导致sudo整体不可用,也方便按团队维护。
最常见的授权形式:
zhangsan ALL=(ALL:ALL) ALL
这一行的意思是:zhangsan可以在任何主机(第一个ALL)上,以任何用户身份(括号里的ALL:ALL)执行任何命令(最后的ALL)。如果要限制只能管理某个服务:
zhangsan ALL=(root) /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginx
如果不希望每次执行都输密码,可以加上
NOPASSWD
:
zhangsan ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
这里有个很容易踩的坑:sudo配置里如果命令不是绝对路径,会直接报command not found,因为sudo不会自动搜索当前用户的PATH。写配置时务必用
which systemctl
查一下绝对路径再写。
我个人的建议是:除非是单机自用的服务器,否则不要给普通用户配置“完全root权限”。更合理的方式是按运维场景分成几个组,比如docker组管容器、deploy组管发布目录、log组管日志查看,然后在sudoers里用
%组名
批量授权。权限粒度越细,出问题后能追溯和止损的余地就越大。
4. 进阶:特殊权限、ACL与Linux隐藏的“权限锁”
4.1 SUID/SGID/Sticky Bit三种特殊权限
普通rwx之外,Linux还有三个特殊权限位,分别用s、s、t表示,对应的数字位置是4、2、1,放在普通权限的前面或对应位置。
SUID(4)只对可执行二进制文件有意义。它让普通用户执行该程序时,进程的有效UID临时变成文件属主的UID。最经典的例子是
/usr/bin/passwd
:
ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 ...
普通用户需要修改自己的密码,但
/etc/shadow
这个密码文件只有root能写。机制就是passwd程序带SUID,执行过程中临时以root身份修改shadow文件。你可以把这个概念理解成:你进了一间只有管理员能进的门,但门禁卡上写的是管理员的编号,出门后还是你本人。SUID一旦配错,比如给
bash
、
vim
这些程序加了SUID,任何普通用户执行它们都能拿到root权限,这是非常严重的安全漏洞。
SGID(2)对目录有特殊含义:目录设置了SGID后,在目录里新建的文件或子目录会继承该目录的属组,而不是创建者自己的主组。这个特性在做团队共享目录时非常实用,前面3.2里的
chmod g+s /data/www
就是这个思路。这样无论谁在里面新建文件,文件的属组都是dev,组内其他人就能继续协作,不会出现“我建的文件别人改不了”的尴尬。
Sticky Bit(1)最典型的应用是
/tmp
目录:
ls -ld /tmp
drwxrwxrwt 20 root root ...
目录权限末尾那个
t
表示粘滞位。在/tmp这种目录里,任何人都有写权限,可以创建目录项,但只有文件属主、目录属主或root才能删除/改名别人的文件。这防止了“别人建了个临时文件被你顺手删掉”的问题。设置方式:
chmod +t /tmp
或数字方式
chmod 1777 /tmp
。
4.2 用ACL把权限精确到具体某个用户
传统权限只有owner、group、other三档,想单独给某个用户授权就非常别扭:要么把用户加进组里(但会影响所有组内成员),要么再用sudo(但粒度不够)。ACL(访问控制列表)就是来解决这个问题的,它能给任意用户或任意组单独设置权限。
查看ACL用
getfacl
,设置用
setfacl
。比如有一个
/data/shared
目录,想让alice单独拥有读写执行权限,其他人保持原样:
setfacl -m u:alice:rwx /data/shared
setfacl -R -m u:alice:rwx /data/shared # 递归
如果要让某个文件新建后默认继承ACL,可以设置默认ACL:
setfacl -d -m u:alice:rwx /data/shared
设置完ACL后,
ls -l
末尾会多出一个
+
:
drwxrwx---+ 2 root root 4096 Feb 20 10:31 shared
这里要提醒一个ACL和chmod的交互坑:当文件有ACL时,传统group位的含义会变成ACL mask,也就是“被ACL授权的用户和组的最高可达权限”。如果你之后执行
chmod g-w
,可能无意中把所有ACL用户的写权限也砍掉了,因为mask被改小了。碰到“我明明setfacl授权了,怎么还是没权限”的问题,先执行
getfacl
看mask,再调整。
4.3 root却删不掉文件?chattr与SELinux在作怪
有一种情况会让几乎所有Linux新手抓狂:我都切到root了,执行
rm -rf
竟然提示
Operation not permitted
。这不是普通权限问题,十有八九是文件被加了不可变属性。
查看属性用
lsattr
,解锁用
chattr
:
lsattr /path/to/file
----i---------e-- /path/to/file
chattr -i /path/to/file # 去掉不可变属性
rm -f /path/to/file
chattr +i
设置不可变属性后,即使是root也无法修改、删除、重命名该文件。有些加固过的系统会对关键文件(比如
/etc/passwd
、日志目录)加
+i
属性,防止恶意篡改。如果你在清理临时文件时遇到
Operation not permitted
,先
lsattr
看一眼,别急着怪系统。
另一个“隐藏权限锁”是SELinux。CentOS/RHEL这类系统默认开启SELinux时,权限判断除了传统的rwx位,还要检查文件的安全上下文(type)。典型场景:nginx报403,文件明明有644权限、属主也对,但就是读不了。用
ls -Z
查看上下文:
ls -Z /var/www/html/index.html
-rw-r--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0 index.html
web服务能正常读取的上下文一般是
httpd_sys_content_t
或
httpd_sys_rw_content_t
。如果你把文件从别的地方拷进来,上下文变成了
home_t
或
user_tmp_t
,nginx就会拒绝访问。修复方式:
restorecon -Rv /var/www/html
或者临时调整到目标类型:
chcon -t httpd_sys_content_t /var/www/html/index.html
restorecon
是按SELinux策略数据库恢复默认上下文,
chcon
是临时直接修改,更推荐前者,因为下次
restorecon
还会保持一致。遇到莫名其妙的权限问题,用
setenforce 0
临时关一下SELinux试试,如果问题消失,基本就能断定是SELinux策略在拦截。不过关掉SELinux只适合排查,生产环境还是要找到正确的上下文或布尔值设置。
5. 实战:权限不足、无法删除、docker/mount场景排查实录
5.1 一套通用排查流程:从id到namei再到lsattr
权限问题千奇百怪,但排查路径完全可以套路化。我遇到任何“Permission denied”或“无法删除/修改”,都会按这个顺序走:
第一步,确认当前身份。执行
id
看使用者的UID/GID和附加组,执行
whoami
看有效用户。这一步能排除一半的“我以root登录但网站没权限”这类基础误区。
第二步,确认目标文件的权限和属主。
ls -ld /path/to/file
看文件或目录本身的权限。注意路径每一层都要看,因为任何一个中间目录缺少x权限,都会导致最终无法访问。这里有个非常好用的命令:
namei -l /var/www/html/index.html
它会一层一层列出路径里每个目录/文件的属主和权限,哪一段权限不对一目了然。很多“路径越长越容易出问题”的疑难杂症就是靠这一步定位的。
第三步,检查是否是特殊属性或SELinux。执行
lsattr -d /path
看是否被加了
+i
,执行
ls -Z /path
看SELinux上下文。如果这两个都正常,再看ACL:
getfacl /path
说实话,走到这一步,九成五的权限问题都能定位了。剩下极少数可能是挂载选项导致的,比如
mount
时加了
noexec
或
nosuid
,这些用
mount | grep 挂载点
就能看到。
5.2 文件权限修复实操:chown -R后用find按需修正
权限修复最常见的错误,就是“不管三七二十一,整个目录chmod 777”。这样虽然立竿见影,但等于把校验门全部打开,后患无穷。我更推荐有节奏地修复。
场景:某个web项目迁移到新机器后,页面能打开但上传图片报“目录不可写”,日志显示
Permission denied
。
第一步,搞清楚web进程运行用户。常见的是
www-data
(Debian系)或
nginx
(RHEL系):
ps aux | grep -E 'nginx|php-fpm' | grep -v grep
进程输出第一列就是运行用户。我遇到过最迷惑的情况是php-fpm和nginx分别用了不同用户,导致nginx能读、php-fpm写不了。这种时候先统一它们要么同一个用户,要么明确各自职责。
第二步,把项目目录整体归属到运行用户,并保留组权限:
chown -R www-data:www-data /var/www/myapp
chmod -R 755 /var/www/myapp
第三步,单独给“可写目录”加写权限,而不是全站可写:
find /var/www/myapp -type d -name uploads -exec chmod 775 {} \;
find /var/www/myapp -type d -name cache -exec chmod 775 {} \;
如果目录里还需要缓存文件、日志文件,可以进一步区分:目录用775,文件用664。这里最关键的思路是“最小可写范围”,只有确实需要动态写入的目录才给写权限,其他一律只读。
5.3 docker挂载卷、网站上传、普通用户mount三个高发场景
场景一:Docker挂载卷Permission denied
这是用Docker最常碰到的坑。你在宿主机创建了
/data/mysql
目录并chown成自己,然后启动MySQL容器:
docker run -v /data/mysql:/var/lib/mysql mysql:8.0
容器内MySQL进程默认以mysql用户运行,uid通常是999,而宿主机
/data/mysql
目录属主是你的uid 1000,容器内进程对这个目录就没有写权限。常规解法有三个:一是直接把宿主目录的属主改成容器内进程的uid:
chown -R 999:999 /data/mysql
二是用
docker run --user
参数指定以宿主机uid运行,比如
--user 1000:1000
,但这要求镜像里的进程允许切换用户。三是在
docker-compose.yml
里设置
user: "1000:1000"
。我一般优先用第三种,因为它把配置固化下来了,别人拉下来也能复现,不会“这个机器能跑换台机器就挂”。
场景二:网站上传文件后显示403
上传功能正常,但访问上传后的图片404或403。用
ls -l
看上传文件,发现属主是www-data,权限是644,目录是755,乍看没毛病,但拿
namei -l
一查,发现上传目录某个父级只有
r-x
,中间链断裂了。修复思路很简单:给路径上每一层都补上执行权限。另一个常见原因是上传后文件带有SELinux上下文,需要
restorecon -R
一下。
场景三:普通用户无法mount挂载
传统
mount
命令要求root权限,普通用户直接执行会报权限不足。桌面环境可以用
udisksctl mount -b /dev/sdb1
,服务端更稳妥的方式是在
/etc/sudoers.d/
里做白名单授权:
zhangsan ALL=(root) NOPASSWD: /usr/bin/mount /dev/sdb1, /usr/bin/umount /dev/sdb1
注意白名单要精确到具体设备挂载路径,不要写裸的
/usr/bin/mount
,否则用户可以用
mount --bind
做各种奇奇怪怪的事情,授权面太宽。
5.4 常见权限问题速查表与最小权限原则
为了方便快速对照,我把高频问题整理成一张速查表:
| 现象 | 可能原因 | 排查命令 | 解决办法 |
|---|---|---|---|
| 能列出文件名但cd不进去 | 目录缺x权限 |
namei -l
|
chmod +x 目录
|
| 文件明明有写权限还是改不了 | 父级目录缺w权限 |
ls -ld 父目录
| 给父目录加w权限 |
| root也删不掉文件 | 文件被加了不可变属性 |
lsattr 文件
|
chattr -i 文件
后删除
|
| 网站403且权限/属主都对 | SELinux上下文不对 |
ls -Z
|
restorecon -Rv 目录
|
| Docker容器读写宿主机目录报错 | 容器内uid与宿主机属主不匹配 |
ps aux
看容器内用户
|
chown -R 999:999
或compose指定user
|
| 普通用户sudo报command not found | sudoers里没写绝对路径 |
which 命令
| 写完整路径重配sudo |
| 同一个组内用户互相改不了文件 | 目录没有SGID继承组权限 |
ls -ld 目录
|
chown -R :组名 目录 && chmod g+s 目录
|
说到底,Linux权限设计其实不复杂,核心就三件事:身份(我是谁)、权限位(我能干啥)、特殊机制(谁能绕过规则)。只要坚持“最小权限”原则,给每个用户只开够用的权限,给每个服务建立独立的运行用户,绝大多数权限问题都不会发展到让你焦头烂额的地步。
在我实际维护服务器的经验里,权限故障七成是“目录权限没搞对”或“属主/属组没设对”,两成是“临时图方便chmod 777埋下的雷”,只有一成是SELinux、ACL这些进阶机制。排查时先看目录,再看属主,最后才考虑特殊机制,顺序对了,问题就少了一大半。另外,每次改权限前,用
ls -l
和
namei -l
各看一眼,改完后第一时间用实际操作验证,别改完就以为完事——权限这东西,验证过才算数。
250



被折叠的 条评论
为什么被折叠?



