从一条 PostgreSQL 锁文件报错说起:权限问题的本质
前段时间公司测试环境一台 PostgreSQL 突然连不上了,日志里反复出现一行报错:
text复制无法创建锁文件 "/var/run/postgresql/.s.pgsql.5432.lock": 权限不够
当时我第一反应是磁盘满了,结果 df -h 一看,空间还很充裕。又怀疑是不是 PostgreSQL 进程被杀、socket 目录被误删,后来一查才发现,问题根本不在磁盘,而是 /var/run/postgresql 这个目录的写权限不对。这个目录在开机时由系统自动创建,默认属主是 postgres 用户,但测试环境重启后目录权限被某个初始化脚本改成了 755,非属主用户根本没有写权限,PostgreSQL 自然没法在这个目录里创建 Unix socket 和锁文件。
其实这类问题在 Linux 系统运维里太常见了。“我没有权限”和“这个用户的权限配错了”是两种完全不同的排查路径,但绝大多数人一看到 Permission denied 就跑去改文件权限,能蒙对一次,下一次换个文件又卡住。真正的问题在于,很多人对 Linux 文件权限和用户管理这套体系只停留在“chmod 777 能解决”的层面,从来没有系统地搞明白权限位、属主属组、默认权限、特殊权限位、sudo 提权这些概念之间的逻辑关系。
这一篇我准备把 Linux 文件权限与用户管理这条线完整拆开讲一遍,从 rwx 权限位怎么编码,到用户和组的增删改查,再到 ACL、SUID/SGID/Sticky Bit、sudo 配置这些容易被忽略但实际工作里经常踩坑的点。适合刚接触 Linux 运维的新人,也适合那些已经写了好几年命令、却一直没搞懂“为什么要这样配”的开发者。
1.1 锁文件和 socket 文件到底卡在了哪一步
先回到开头那个报错。PostgreSQL 启动时,默认会在 /var/run/postgresql 目录下创建一个 Unix socket 文件和一个锁文件。锁文件的作用是防止同一个数据目录被多个 PostgreSQL 实例重复启动,socket 文件则是让本地客户端能通过 Unix domain socket 连上来。
这两个文件的创建逻辑都依赖一句话:进程对所在目录必须拥有写权限。注意,这里的关键是“目录的写权限”,不是文件本身的权限——因为锁文件此刻还不存在。Unix 文件系统的设计里,目录本质上也是一份“文件”,里面记录着目录条目的名字和 inode 对应关系。你对一个目录有写权限,才允许在里面创建、删除、重命名文件;这个规则和文件本身的权限互不影响。
PostgreSQL 是以 postgres 用户身份运行的,所以真正需要验证的是:postgres 用户对 /var/run/postgresql 这个目录,是否具备 w 权限。
看一下目录当前状态:
bash复制$ ls -ld /var/run/postgresql
drwxr-xr-x 2 postgres postgres 60 Jan 12 09:30 /var/run/postgresql
权限位是 755,属主是 postgres 用户,这对 postgres 用户本人来说有读有写有执行,看起来问题不大。但如果目录属主不是 postgres,而是被人改成了 root,或者目录权限被收成了 750,那 postgres 用户就落入“其他用户”分类,写权限直接没了。
这也就解释了一个很常见的现象:同样一套配置,有的机器上 PostgreSQL 能正常启动,有的机器上却报 socket 目录权限不够。差别往往就在于某个初始化脚本、某个监控脚本“顺手”对目录执行了 chmod,把权限位改掉了。
这类问题最简单的验证方式是:
bash复制$ sudo -u postgres touch /var/run/postgresql/.test
能创建成功,就说明目录写权限没问题;创建失败,报错信息里会直接告诉你返回的是 EACCES 还是 EROFS,两者原因完全不同。
1.2 权限模型的最小单元:属主、属组、其他人
Linux 文件的权限模型可以浓缩成一句话:每个文件都有且仅有一个属主用户、一个属组,系统再根据请求者身份,把访问者归入三类中的某一类,最终决定放行还是拒绝。
三类身份是:
u(user):文件的属主用户,通常是创建者g(group):文件的属组,表示文件归属的那个用户组里的所有用户o(other):除上面两类外的所有其他用户
这个模型的关键在于“归属判断是互斥的”。请求访问文件时,内核先看“你是不是属主本人”,如果是,就只按属主权限判断;如果不是,再看“你是不是属组成员”,是,就只按属组权限判断;两者都不满足,才落到“其他用户”权限。不存在“属主权限不够,再把属组权限加上”这种叠加逻辑。
理解了这个模型,再回头看权限配置,很多问题就豁然开朗了:
- 一个文件属主是
root,属组是www,权限是640,那么www组的成员有读权限,非组成员只能干瞪眼,就算他也在服务器上有账号也没用。 - 一个用户加入了十几个组,访问某个文件时,系统会依次检查
uid匹配和所有从属gid匹配,只要有任意一个gid匹配上,就按对应属组权限处理。
这也顺带解释了为什么“把一个用户加到组里之后,他看到的权限可能反而变了”——因为他的身份分类从“其他用户”变成了“属组成员”,生效的权限位从 others 这一档切换到了 group 这一档。
2.1 十个字符到底代表什么
很多人看 ls -l 的输出,只知道前面一串 -rw-r--r--,却说不清每个字符的含义。这串字符实际上由两部分组成:第一个字符表示文件类型,后面九个字符分成三组,每组三个字符,依次对应 u/g/o 三类身份的读、写、执行权限。
text复制-rw-r--r-- 1 root root 1024 Jan 12 10:00 test.txt
把它拆开看:
| 位置 | 字符 | 含义 |
|---|---|---|
| 第1位 | - |
普通文件(d 目录,l 符号链接,c 字符设备,b 块设备,s socket,p 管道) |
| 第2-4位 | rw- |
属主:可读、可写、不可执行 |
| 第5-7位 | r-- |
属组:只读 |
| 第8-10位 | r-- |
其他:只读 |
那 rwx 分别代表什么?日常理解起来可以这么记:
r:读取文件内容的权限;对目录而言,是列出目录条目的权限w:修改、截断、删除文件内容的权限;对目录而言,是在目录里创建、删除、重命名条目的权限x:执行文件(如果它是程序或脚本)的权限;对目录而言,是“穿过”目录进入其子目录的权限
我一直强调目录的 x 权限,因为这是新手最常忽略的点。在 Linux 里,目录的 r 和 x 是拆开的:只有 r 没有 x,你能用 ls 看到目录里有什么文件,但无法 cd 进入,也无法访问其中任何文件的 inode 信息;只有 x 没有 r,你能 cd 进去,但 ls 看不到文件名,只能凭已知的完整路径去访问具体文件。
这种设计实用性很强。比如一个共享目录,你希望别人能上传文件到某个固定子目录,又不希望他们直接看到根目录下有哪些文件,就可以对根目录只给 x 权限、不给 r 权限。很多 Web 服务的上传目录就是这么做的。
2.2 r=4、w=2、x=1 是怎么设计出来的
chmod 用数字表示权限时,采用的是八进制编码:r 占权重 4,w 占权重 2,x 占权重 1,三个值相加得到一位数字。比如 rwx 就是 4+2+1=7,rw- 是 4+2=6,r-- 是 4,--- 是 0。
这个编码方式的底层逻辑其实是一组二进制位。把每个权限位当成一个开关,有权限记为 1,没有记为 0,那么 rwx 就是二进制的 111,转成十进制就是 7;rw- 是 110,转成十进制是 6。三位一组,天然适合用一位八进制数表示。
[
rwx = 111_2 = 7_8
]
[
rw- = 110_2 = 6_8
]
[
r-- = 100_2 = 4_8
]
所以 chmod 754 file,拆开读就是:属主 7(rwx)、属组 5(r-x)、其他 4(r--)。每次看到这种数字权限,我建议你心里默念一句:读是4、写是2、执行是1,三个加起来算一档,依次看三类身份。
符号法(u+x、g-w、o=r)和数字法是等价的,只是表达角度不同。符号法的好处是“只改某个身份、某个权限位”,不用重写整串权限。比如一条 chmod -R o-w /data/website 的意思就是对整个目录树去掉其他人(o)的写权限,不会动到其他权限位。这种操作在加固服务器时非常常用。
2.3 目录权限和文件权限的记忆陷阱
文件权限和目录权限用的都是 rwx 三位,但语义差异很大。我见过不少运维新手在改目录权限时踩坑,最常见的一个是:为了让人能进目录,给了目录 r--,以为可读就能进去,结果死活进不了。
原因前面说了:进入目录依赖 x 权限,而不是 r。如果你想让人能进入目录但看不到里面的文件名,给 --x 就行;想让人能列出文件名但又进不去,给 r--;想让人既能进又能看,就给 r-x。
另外还要注意“穿透”问题。访问某个深层路径时,比如 /data/app/logs/access.log,你不仅需要 access.log 文件本身的权限,还需要 /data、/app、/logs 这三层目录各自具备 x 权限,否则路径走到一半就会被拦截。很多人排查权限问题时只看最后一级文件权限,忽略了路径上每一层目录,结果反复试不出来。
用 namei -l 就能一次性看到完整路径上每个节点的权限:
bash复制$ namei -l /data/app/logs/access.log
f: /data/app/logs/access.log
dr-xr-xr-x root root /
drwxr-xr-x root root data
drwxrwxr-x root app app
drwxrwxr-x app app logs
-rw-r----- app app access.log
这么一列,哪一层目录权限不够一目了然。
3.1 用户和组的信息都存在哪里
Linux 用户的常规信息存放在 /etc/passwd,但密码哈希本身并不在这里,而是存放在 /etc/shadow。为什么这么设计?因为 /etc/passwd 是全局可读的,如果密码哈希也放在这里,任何人都能把它拉下来做离线暴力破解;而 /etc/shadow 默认只允许 root 和 shadow 组读取,安全性明显好得多。
/etc/passwd 每行是一条用户记录,冒号分隔 7 个字段:
text复制postgres:x:26:26:PostgreSQL Server:/var/lib/postgresql:/bin/bash
| 字段 | 值 | 含义 |
|---|---|---|
| 用户名 | postgres | 登录名 |
| 密码占位 | x | 真正密码在 /etc/shadow |
| UID | 26 | 用户 ID,内核按它识别用户 |
| GID | 26 | 主组 ID |
| 注释 | PostgreSQL Server | 通常是用户全名或说明文字 |
| 家目录 | /var/lib/postgresql | 登录时初始所在目录 |
| 登录 shell | /bin/bash | 登录后启动的 shell |
/etc/shadow 里则记录密码哈希、最近修改时间、最短/最长有效天数、过期提醒天数、禁用天数等。密码策略相关的参数全在这里,做等保、做账号合规时经常要动到它。
用户组记录在 /etc/group,格式和 passwd 类似:
text复制sudo:x:27:zhang,san,liubei
最后一个字段列出的是把该组作为“附加组”的用户清单。注意,passwd 里的 GID 是“主组”,group 文件里的用户列表是“附加组”,一个用户有且只有一个主组,但可以同时加入多个附加组。
3.2 useradd 常用选项与默认值
创建一个用户,表面上一行命令,背后其实做了很多事:写入 passwd/shadow/group 三个文件、创建家目录、复制 skeleton 目录(/etc/skel)下的默认配置、设置 shell 等。默认行为由 /etc/login.defs 和 /etc/default/useradd 控制。如果你不想用发行版默认值,建议显式指定参数:
bash复制$ sudo useradd -m -d /home/zhang -s /bin/bash -G docker,sudo -u 2201 zhang
-m:创建家目录-d:指定家目录位置-s:指定登录 shell-G:指定附加组(多个组用逗号分隔)-u:指定 UID-c:添加注释信息,比如真实姓名或工号
如果不加 -m,新用户可能没有家目录,这在执行 usermod、ssh-keygen 或者登陆后写配置时会出现各种诡异问题。
创建之后别忘了设置密码:
bash复制$ sudo passwd zhang
useradd 创建的用户默认是锁定状态,只有执行过 passwd 设置密码才能登录。很多新手建完用户后直接 su zhang,发现密码不对,其实是因为根本没设置过。
3.3 usermod 与 userdel:改错一步,后患无穷
用户管理里最容易被忽略的是修改操作的正确姿势。usermod 的常用场景是把用户加入某个组:
bash复制$ sudo usermod -aG docker zhang
这里的 -a(append)极其关键。如果不加 -a,-G 会直接把用户的附加组列表重置为指定的组,也就是把用户之前加入的其他附加组全部清掉。我见过有人在给用户补加 docker 组时,把用户跟 sudo 组的关联清掉了,导致该用户突然失去提权能力,半天排查不出原因。
如果只想锁定、解锁用户,不需要动密码,可以用:
bash复制$ sudo usermod -L zhang # 锁定
$ sudo usermod -U zhang # 解锁
删除用户时,另一个常踩的坑是:直接执行 userdel zhang,用户的 UID 被删除,但他曾经创建的文件还是以这个 UID 的形式留在文件系统里。此时如果你新建一个用户,系统重新分配了一个相同 UID(特别是你手动指定 -u 时容易撞上),那这个新用户会“继承”之前用户留下的所有文件所有权。
所以生产环境删用户前,建议先把他的文件找出来,决定归属:
bash复制$ sudo find / -user zhang -ls
然后要么把文件 chown 给别人,要么用 userdel -r zhang 连家目录和邮件目录一起删掉。但注意 -r 只删除家目录和 mail spool,并不能清理用户散落在 /tmp、/var/tmp、NFS 挂载点等位置的文件。
3.4 一个实战场景:gitea 里管理 SSH 密钥,实际上是在管文件权限
热搜词里有一条“gitea 中怎么管理用户的 ssh 密钥”,这里简单说一嘴:Gitea 本身通过 Web 界面管理密钥,但底层原理是 SSH daemon 读取用户家目录下 ~/.ssh/authorized_keys 文件来认证。Gitea 是作为某个系统用户(比如 git)来运行的,所以所有通过 Gitea 推送代码的用户的公钥,最终都会追加到 git 用户的 ~/.ssh/authorized_keys 里。
这里权限有个硬性要求:~/.ssh 目录权限必须是 700,authorized_keys 文件权限必须是 600。如果权限过宽,OpenSSH 出于安全策略会直接忽略这个文件,表现就是“明明公钥配好了,SSH 还是要求输密码”。
bash复制$ sudo -u git chmod 700 /home/git/.ssh
$ sudo -u git chmod 600 /home/git/.ssh/authorized_keys
遇到 SSH 密钥失配问题,先别急着重新生成密钥,检查权限方向往往更快。
4.1 chmod 的两种操作姿势
chmod 是整个权限体系里使用最频繁的命令。数字法适合“知道最终权限应该是什么”的情况,符号法适合“只改某一个身份上的某一项权限”的情况。两种姿势必须都熟练掌握,不要只会一种。
数字法一次设置全量权限:
bash复制$ chmod 644 index.html
$ chmod 750 /data/app
$ chmod 600 id_rsa
符号法则可以精准操作某个身份:
bash复制$ chmod u+x script.sh # 给属主加执行权限
$ chmod g-w config.ini # 去掉属组的写权限
$ chmod o= readme.txt # 其他人的权限置空
$ chmod a+r public.log # 所有人加读权限
$ chmod -R u+rwX /data/www # 递归添加权限,X 只对目录或已有执行权限的文件生效
其中 X(大写)是一个很实用的符号:它表示“如果目标是目录,或者文件已有任一位执行权限,才加执行权限”。这样你就不必担心一个递归操作把一堆文本文件全部变成可执行文件。
4.2 chown、chgrp 与常见误用
chown 用于修改属主和属组,支持一次写完:
bash复制$ sudo chown zhang:devops /data/project
这行命令把 /data/project 的属主改成 zhang,属组改成 devops。如果只想改属组,也可以写成 chown :devops /data/project,或者用 chgrp devops /data/project。
常见误用有两个:
第一个是忘了加 -R,导致只改了目录本身,目录里成千上万的文件还是原来的属主属组。拷代码上线时新版目录往往就是这么变成一堆 root 文件的。
第二个是把 chown 当成“解决所有权限问题的万能药”,不看业务场景就直接把目录 chown 到运行用户。比如 Nginx 的 web 根目录,如果你把整个 /var/www/html 都 chown 成 nginx 用户,那后续开发者写入文件时反而可能踩到“对目录有写权限,对文件没有写权限”的边界问题。
更稳妥的做法是:
bash复制$ sudo chown -R zhang:devops /data/project
$ sudo find /data/project -type d -exec chmod 750 {} \;
$ sudo find /data/project -type f -exec chmod 640 {} \;
目录和文件分别设置权限,而不是一刀切 777 或 755。
4.3 注意复制文件时的权限处理
很多人在服务器之间同步文件时,会在意内容,却不在意权限。cp 默认复制文件时,新文件的权限取决于 umask,而不是源文件权限。如果你想保留原权限位,必须加 -p:
bash复制$ cp -p config.conf /data/backup/
用 rsync 同步时,保留权限、属主、时间戳等元信息是常规操作:
bash复制$ rsync -avz --owner --group --perms /source/ /dest/
热搜词里有一条“ansible 复制文件到所有节点并授权 777 权限”,这在 ansible 里是 copy 模块的常规操作:
yaml复制- name: 分发配置文件
ansible.builtin.copy:
src: files/app.conf
dest: /etc/app/app.conf
owner: app
group: app
mode: '0640'
我建议在生产环境尽量少用 0777 这种全开放权限,除非确实有多个互相不信任的用户需要同时读写同一批文件。这类场景通常可以用目录属组、ACL 或粘滞位解决,而不是粗暴地 777。给文件 777 意味着任何能在本机登录的用户都能改它,一旦这台机器上有低权限账号被攻破,那个文件就跟着沦陷。
5.1 为什么新文件默认是 644,而不是 666
这是一个很多人没深究过的问题:为什么新创建的普通文件默认权限是 644,新创建的目录却是 755?
底层逻辑是:Linux 对“新文件”有一个预设的基准权限——普通文件 666,目录 777。为什么文件是 666?因为文件不预设执行权限,所有的新文件默认都应该是“不可执行”的;而目录必须带 x,否则没法进入。基准权限再与 umask 做一次“取反后按位与”的运算,得到最终权限。
umask 的值通常显示为四位八进制,比如 0022。计算规则是:
[
\text{新文件最终权限} = 0666 ;&; \sim(umask)
]
[
\text{新目录最终权限} = 0777 ;&; \sim(umask)
]
以 umask 为 022 为例:
text复制0666 & ~0022
= 0666 & 0755
= 0644
目录:
text复制0777 & ~0022
= 0777 & 0755
= 0755
所以新文件是 644、新目录是 755。如果你把 umask 改成 002,新文件就变成 664,新目录变成 775。这也是很多协同开发团队推荐设置:同一用户组内的成员可以互相修改文件,组外用户只有读权限。
5.2 按用户设置 umask 与常见误区
umask 可以按用户定制,写在 ~/.bashrc 或 ~/.profile 里。但要注意,非交互式 shell(比如计划任务、systemd 服务)不会读取用户的 ~/.bashrc,这时候你得在 systemd service 文件里显式设置:
ini复制[Service]
UMask=0027
或者是写进 /etc/profile、/etc/profile.d/*.sh 做全局设置。
需要特别提醒的是,很多人在计算 umask 时容易把“最终权限”和“umask 值”搞反。umask 表示的是“要屏蔽掉的权限”,不是“要赋予的权限”。所以 umask 077 并不代表新文件权限是 077,而是屏蔽掉属组和其他人的所有权限,最终新文件是 600,新目录是 700。
一个快速心算法:文件最终权限 = 666 减去 umask 中对应位的值;目录最终权限 = 777 减去 umask 中对应位的值。比如 umask 027,文件就是 666-027=640(注意按位减,不是普通十进制减法),目录就是 777-027=750。这个方法虽然不严谨,但日常心算很快。
6.1 常规权限不够用时,ACL 是第一个该想到的扩展
现实场景里,经常会遇到一类需求:某个共享目录,业务组所有成员可以读写,但是审计员只需要只读,而临时外包人员连看都不能看。用常规 ugo 模型解决这个问题会很尴尬——你需要在同一个组模型里塞进三种不同档次的权限,普通的 rwx 根本没有这个维度。
ACL(Access Control List)就是为此设计的。它允许你在 ugo 模型之上,为任意单个用户或单个组单独配置权限,而不影响其他条目的权限。
查看 ACL:
bash复制$ getfacl /data/project
给指定用户 auditor 添加只读权限:
bash复制$ sudo setfacl -m u:auditor:rx /data/project
给指定组 temp_workers 添加完全权限,并递归到子目录:
bash复制$ sudo setfacl -Rm g:temp_workers:rwx /data/project
设置默认 ACL,让以后新建的子文件和子目录自动继承:
bash复制$ sudo setfacl -m d:g:devops:rwx /data/project
文件系统收到 ACL 后,传统权限模型里那个“三类身份互斥判断”的规则就变成了:先看有没有 ACL 条目匹配请求者,有就按 ACL 走;没有,再回落到 ugo 模型。
需要注意的是,ACL 是会改变 ls -l 输出格式的。当文件设置了额外 ACL 条目,权限位后面会出现一个 +:
text复制drwxrwx---+ 3 root root 4096 Jan 12 12:00 project
此时用数字法执行 chmod 可能不会像你预期那样生效,因为 chmod 只改传统权限位,ACL mask 会约束 ACL 条目的最大权限。遇到设置了 ACL 的目录,建议先 getfacl 看全貌再做修改。
6.2 SUID、SGID、Sticky Bit:三种特殊权限位的风险与用途
除了 rwx 三位,Linux 还支持三种特殊权限位,它们分别作用在可执行文件和目录上,行为差异很大。
SUID(Set User ID)出现在属主执行权限位上,表现为 s。它的效果是:可执行程序运行时,进程的有效用户 ID 切换为文件属主,而不是当前执行者。最经典的例子是 /usr/bin/passwd:
bash复制$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 May 30 2024 /usr/bin/passwd
普通用户执行 passwd 修改密码时,需要写 /etc/shadow,而 shadow 文件只有 root 能写。有了 SUID 位,passwd 程序运行时就以 root 身份操作,普通用户才能顺利修改自己的密码。
但 SUID 也正是权限提升攻击的重点关注对象。如果你在某个自己可控的脚本上设置了 SUID root,或者某个程序存在漏洞,一旦被利用就等于获得了 root 权限。因此生产环境发现属主是 root 的 s 位程序时,一定要先搞清楚它是否真的有这个必要。很多临时给某个二进制文件加的 SUID 位,事后没人清理,就成了安全隐患。
查看系统中所有带 SUID 的文件,用这句:
bash复制$ sudo find / -perm -4000 -type f -ls
SGID 显示在属组执行权限位上,行为分为两种:作用于可执行文件时,进程的有效组 ID 切换为文件属组;作用于目录时,目录下新建文件的属组不再归创建者的主组,而是自动继承目录的属组。第二种行为在团队共享目录里非常有用。
bash复制$ sudo chgrp devops /data/shared
$ sudo chmod 2775 /data/shared
此后只要 devops 组成员在该目录下新建文件,文件属组自动是 devops,不用每个人手动 chgrp。配合 setfacl 默认 ACL,基本能覆盖绝大多数团队协作场景。
Sticky Bit 显示在“其他用户”的执行权限位上,表现为 t。它只对目录有意义:目录设置了粘滞位之后,只有文件属主、目录属主或 root 才能删除、重命名目录内的文件,其他有写权限的用户不能互相删文件。/tmp 就是典型代表:
bash复制$ ls -ld /tmp
drwxrwxrwt 10 root root 4096 Jan 12 12:00 /tmp
权限是 1777,所有人都能往里写,但普通用户删不了别人的临时文件。
排查权限问题时,注意
s和t的大小写:如果对应位置本来有x,显示为小写s/t;如果没有x,则显示为大写S/T。大写说明特殊位生效但对应的执行权限并没有开,行为上往往达不到你预期。
7.1 用户报“没权限”,先按这个顺序查
权限类故障排查最忌讳一上来就 chmod、chown 乱试。我自己的排查链路固定是四步:
第一步,确认“我是谁”:
bash复制$ id username
输出会同时列出 uid、主组 gid 和所有附加组。这一步能快速判断用户到底属于哪些组,以及他访问文件时会被归入哪一类身份。很多时候用户反馈“我是 devops 组的,为什么没权限”,执行 id 一看,他根本不在 devops 组里。
第二步,确认目标文件当前权限:
bash复制$ ls -ld /path/to/file
如果文件后面带了 +,说明还有 ACL,继续跑 getfacl。
第三步,确认路径上每一层目录都能穿过:
bash复制$ namei -l /path/to/file
第四步,如果上面全是常规权限,还是被拒绝,那就用 strace 看实际系统调用返回的错误码:
bash复制$ strace -e trace=file cat /path/to/file
注意最后一行附近会出现 EACCES(Permission denied)还是 ENOENT(No such file or directory),这两个错误在普通用户看来可能一样,但排查方向完全不同。EACCES 是权限问题,ENOENT 往往指向路径解析过程中某一层目录无法访问或者文件本身不存在。
7.2 实战复盘:“用户拒绝访问内存文件权限”到底卡在哪
热搜词里有条“用户拒绝访问内存文件权限怎么办”,这个说法很模糊,但大概率指两类情况。一类是 /dev/shm 或 tmpfs 挂载的目录,用户没有写权限;另一类是内核的 memfd_create 等机制被 seccomp 或 AppArmor 拦截。
如果是 /dev/shm,先看挂载选项:
bash复制$ mount | grep /dev/shm
tmpfs 默认权限可能是 1777,任何用户都能读写。如果被改成了 755,普通用户自然无法在里面创建文件,很多依赖共享内存的中间件(比如某些数据库、消息队列)直接启动报错。解决办法是把权限改回来,或者调整挂载参数。
如果确认目录权限没问题,但应用程序访问“内存文件”还是被拒,那就要考虑是不是 SELinux 上下文的问题。查看文件上下文:
bash复制$ ls -Z /path/to/file
再对比进程的域标签:
bash复制$ ps -eZ | grep appname
如果上下文不匹配,即使传统权限位是 777,SELinux 也会返回 Permission denied。这种情况下看 /var/log/audit/audit.log,用 ausearch 过滤:
bash复制$ sudo ausearch -m avc -ts recent
凡是遇到“权限明明是对的,就是访问不了”的诡异故障,都建议检查一下 SELinux 或 AppArmor,这层安全机制独立于文件权限系统,极易被忽视。
8.1 sudo 和 su 的本质区别
用户管理的最后一块拼图是权限提升。很多开发者把 su 和 sudo 当成同一种东西,其实两者的行为模型差异很大。
su 是切换用户(switch user),它会打开一个新的登录会话,默认情况下你还得知道目标用户的密码。运维人员常用的 sudo su - 实际上是用 sudo 执行 su 命令,绕过 root 密码、直接切换到 root 身份。
sudo 则不是“切换用户”,而是“以指定身份执行单条命令”。它默认需要当前用户自己的密码,验证通过后,按 /etc/sudoers 里配置的规则,决定你能不能执行某条命令、以什么身份执行。
为什么要区分?因为安全边界完全不同。su 一旦切换成 root,你就是完整的 root,所有权利和责任都是你的;sudo 可以精确到命令级别,比如只允许某个用户执行 systemctl 管理某个服务,不允许他执行 vim /etc/shadow。前者是“把钥匙给你”,后者是“只给你开一扇窗户”。
8.2 visudo 配置与最小权限原则
sudo 的配置集中在 /etc/sudoers,强烈建议用 visudo 编辑,不要直接用 vim 改。原因是 visudo 在保存时会做语法检查,写错规则会直接提示错误,避免你把 sudoers 改坏导致所有用户无法提权。
一行典型规则:
text复制zhang ALL=(ALL:ALL) /usr/bin/systemctl restart nginx
结构拆解:
zhang:授权给哪个用户(或%组名表示授权给组)- 第一个
ALL:允许在哪些主机上执行,多主机环境下用于区分 (ALL:ALL):可以以哪个用户、哪个组身份执行- 后面的命令路径:允许执行的具体命令
生产环境建议遵循最小权限原则:能用命令路径限制的就绝不给 ALL。比如:
text复制%devops ALL=(ALL) /usr/bin/systemctl restart *
比给 %devops ALL=(ALL) ALL 安全得多。
另外两个实际踩坑点:
Defaults requiretty在某些发行版默认开启,会导致非交互会话(比如通过 ansible、脚本调用 sudo)报 “sudo: no tty present and no askpass program specified” 之类的错误。按需注释掉或改为Defaults !requiretty。Defaults secure_path决定 sudo 环境下的 PATH,如果你在交互 shell 里能用某个命令,但 sudo 一执行就 command not found,大概率是这个变量没包含对应目录,而不是命令没装。
脚本里使用 sudo 时,建议用:
bash复制$ sudo -n true && echo "sudo available"
-n 表示非交互模式,如果当前用户没有缓存 sudo 凭证或需要密码验证,会直接失败而不是卡住等输入。配合 ci 流水线或计划任务,可以避免脚本被“挂起”在密码输入上。
8.3 sudo 授权不等于给了 root 密码
最后聊一个管理视角的问题。很多团队把“能不能 sudo”当成“是不是管理员”的标志,这在小型服务器上问题不大,但用户一多就会失控——因为 sudo ALL=(ALL:ALL) ALL 本质上等于给了 root 权限,和直接知道 root 密码没有区别。用户一旦有了这个权限,他可以随时 sudo passwd root 修改 root 密码,或者 sudo -i 进入 root shell,后续所有 sudo 日志审计都形同虚设。
因此我在实际项目里更倾向于这样设计:
- 有完整机器管理权限的人:极少,一般 1-2 个,直接加入
wheel或sudo组 - 需要重启服务、看日志的人:给具体命令限制
- 临时协助的人:通过 sudoers 里的
Timeout_Start、Timeout_Stop或者在任务完成后立刻从授权组移除
文件权限与用户管理这套知识,初看只是几条命令,真正吃透之后,几乎每一个“权限不够”的报错都能用一套统一的模型去解释。排查时先确认身份、再逐层看路径权限、接着看 ACL 和特殊权限位、最后补查 SELinux,大概率都能在十分钟内定位问题。我自己每接手一台新服务器,也习惯先把关键目录权限、sudoers 规则、用户组成员清单捋一遍再交付,这种“提前花五分钟”,往往能省下后续几个小时的故障排查时间。
