一、问题背景
“为什么明明有文件却访问不了”这类权限工单,几乎是 Linux 运维里最常见、也最容易被新人低估的一个领域。它的常见表现:
Permission denied,文件就在那里但 cat 不了
Bash: /usr/local/bin/xxx: Permission denied,明明已经加了 chmod +x
- 同一个目录,root 能进,普通用户进不去
- 服务启动成功,但读不到配置文件,日志里偶尔出现
Read-only file system
- 容器内
ls 能看到文件,但应用报 EACCES
- 应用前几分钟能跑,几分钟后突然报错(NFS / 文件被改为
chmod 000 等)
它的根因往往不在“chmod 一个数字 644”,而在四个层面之间的相互作用:
如果只看一层,常常查不出真正的根因。本文用一条完整的排查主线,逐步把这四层都串起来,让读者遇到类似工单时,能按照这个流程稳定收敛并定位。
二、适用场景
下面这些场景明确在本文覆盖范围内:
cat / vi / less / cp / mv / bash / sh 报 Permission denied。
- Web / 数据库等服务不能读取配置文件、写入日志目录、创建 socket 文件。
- 容器内应用
EACCES、permission denied,宿主机上看着正常。
- SELinux 触发
avc: denied 日志。
- AppArmor 触发
DENIED 操作。
- NFS、SMB 挂载点写入报错,能读不能写。
setuid 程序在某些环境运行不正常。
- 二进制无法执行,典型错误:
cannot enable executable stack、缺少 x 权限、挂载了 noexec。
- systemd 服务启动后拿到错误的 uid / gid。
Operation not permitted,特别是 mount、chroot、capability 类。
不适用场景(不在本文深挖):
- 业务代码层的鉴权(OAuth / RBAC 角色绑定等),不在系统权限讨论范围。
- 文件系统损坏、磁盘 IO 错误、ACL 模块支持未编译等更偏“文件系统 / kernel 模块”的故障。
三、核心知识点
下面分模块讲权限相关的关键概念。每一块都是后面排查命令的依据,看命令时可以回查这些知识点。
1. UNIX 经典权限三元组
每个文件有三组权限位:owner / group / other。ls -la 看到的 rwxr-xr-- 就是这三组。
数字表示:
例如 rwxr-xr-- 即 754。
文件与目录的 rwx 含义差异:
| 权限 |
文件含义 |
目录含义 |
| r |
能读文件内容 |
能列出目录内容(ls) |
| w |
能修改文件内容 |
能在目录里增删改文件 |
| x |
能作为可执行文件运行 |
能 cd 进入目录 / 访问目录内文件 inode |
关键点:
- 目录必须同时有
r 和 x,只有 r 看不到内容但能拿 inode 信息(某些系统行为不同)。
- 缺
x,cd 进不去;缺 r,ls 列不出但能 cd。
- 父目录没有
x,即使文件本身是 0777 也打不开。
例:
drwxr-x--- root www /var/www/html
/var/www/html 下属文件如果是 www 用户能改,但其他用户连 cd 都做不到。
2. 所有者与属组
文件有 owner 与 group,分别对应一个用户 / 组。访问权限匹配逻辑:
- 如果当前进程 uid 与文件 owner 相同:套
owner 位的权限。
- 否则如果进程的 primary group 或 supplementary groups 命中文件 group:套
group 位。
- 否则套
other 位。
关键点:
- 进程的 supplementary group 列表很长,
id 显示的全部都可能参与匹配,不仅仅是 primary。
chgrp 只改 group,不动 owner;chown 改 owner 后,group 通常不变。
chown user:group file 是常用写法;如果 group 留空(chown user: file),group 会保持不变(取决于实现)。
3. setuid / setgid / sticky bit
三个特殊位,影响权限匹配:
| 位 |
ls 显示位 |
含义 |
| setuid |
-rws------ 文件 owner 位的 s |
执行时进程 euid 取文件 owner |
| setgid |
-rwxrws--- 文件 group 位的 s |
执行时进程 egid 取文件 group |
| sticky |
drwxrwxrwt 目录 other 位的 t |
目录内只有 owner / root 能删自己文件 |
典型场景:
/usr/bin/passwd 是 setuid root,能写 /etc/shadow。
/tmp 是 sticky,普通用户不能删别人的 tmp 文件。
ssh-agent 等会用 setgid 切换到特定组。
关键点:
- 挂载了
nosuid 时,setuid / setgid 会被忽略。
- 没有
x 位时 s 显示为大写 S,表示“设了特殊位但无效”。
- 一些发行版默认屏蔽
setuid 程序(如 mount -o nosuid)。
4. 文件系统 ACL 与扩展属性(POSIX ACL)
Linux 标准的 ugo 权限之外,文件系统还支持 ACL 与扩展属性。挂载 acl 选项后,可以用 getfacl / setfacl 设置更多访问控制。
例如:
# user::rw-
# user:lisi:rw-
# group::r--
# mask::rw-
# other::---
user:lisi:rw- 给了 lisi 特殊权限。
mask 是“有效 mask”,用于限制所有 named user 与 group 的最高权限。
关键点:
- 修改文件后没 chmod 改变 mask,会发现
chmod 600 后,named user 访问也不通了,因为 mask 被锁死。
setfacl -b 清空所有扩展 ACL,不只是改 mask。
- 备份工具(
tar、rsync)要带 --acls 才能正确保留 ACL。
- 一些场景(比如 NFS)在不支持 ACL 时静默失败,需要
mount -o noacl 或 -o acl。
5. Linux 安全模块(SELinux / AppArmor)
SELinux 是 LSM 框架(Linux Security Module)的一种,基于策略对每个操作做额外校验。即使 rwx 全部通过,仍然可能因为 SELinux 拒绝。
基本概念:
enforcing:拒绝并审计。
permissive:只审计不拒绝(适合排错)。
disabled:彻底关闭(重启生效,不能直接切到 disabled)。
核心字段:
- 进程的 domain(
httpd_t、sshd_t、unconfined_t 等)。
- 文件的 type(
httpd_sys_content_t、tmp_t、default_t 等)。
- 通过 te rules 规则允许特定 domain → type 访问。
常见错误关键字:
avc: denied { read } for pid=1234 comm="nginx" ... 在 audit.log 或 /var/log/messages。
- 应用报
Permission denied,但 ls -Z 看上下文是合理的,例如 /var/www/html 被标成 admin_home_t。
排错策略:
getenforce 看模式。
ausearch -m AVC -ts recent / journalctl -t setroubleshoot 找拒绝记录。
audit2why < /var/log/audit/audit.log 看 SELinux 为何拒绝。
audit2allow 自动生成允许规则(生产慎用)。
- 长期解:调整目录 context(
semanage fcontext + restorecon)。
6. AppArmor(Ubuntu / SUSE 默认)
AppArmor 是另一种 LSM,机制与 SELinux 不同:用路径而不是 ino 安全上下文来定义策略。
排错命令:
aa-status:当前加载的策略。
cat /var/log/audit/audit.log | grep -i apparmor。
aa-logprof:基于日志自动生成建议规则。
apparmor_parser -R /etc/apparmor.d/<profile>:临时禁用某 profile。
7. 文件系统挂载选项
挂载选项对权限影响很大:
| 选项 |
影响 |
ro |
只读,任何写都会 Read-only file system |
noexec |
文件不能执行(如 /tmp 默认) |
nosuid |
setuid / setgid 失效 |
nodev |
不解释设备文件 |
relatime / atime |
与权限无关,性能问题 |
acl / noacl |
是否启用 POSIX ACL |
usrquota / grpquota |
启用磁盘配额 |
xattr |
是否启用扩展属性 |
排查时 mount 命令直接看,再看 /proc/mounts、/etc/fstab、/proc/self/mountinfo。
8. 进程身份:uid / gid / groups / euid / egid
进程跑起来时有一组身份字段:
uid、gid:进程启动时真实身份。
euid、egid:实际权限判断的身份(setuid 后会变)。
groups:supplementary group 列表。
cap:能力集,用于精细控制 root 能力。
工具:
id:当前 shell 身份。
cat /proc/<pid>/status | grep -E '^(Uid|Gid|Groups|Cap)':进程全部身份信息。
sudo -u user id:以 user 身份校验。
9. Linux Capability(root 能力分片)
CAP_DAC_OVERRIDE:绕过文件读 / 写 / 执行权限检查。
CAP_CHOWN:绕过 chown 检查。
CAP_NET_BIND_SERVICE:绑定 < 1024 端口。
CAP_SYS_ADMIN:一堆管理员能力。
排查 setuid 程序或服务时,getcap /usr/bin/xxx 看授予了哪些能力。
10. 容器场景:UID 映射与 namespace
容器内的 root(UID 0)与宿主机的 root 不一定是同一身份。常见做法:
- 默认映射:容器内
0 ↔ 宿主机大 UID(实际安全策略下)。
user_namespaces:把容器内 UID 1 映射到宿主机普通用户。
容器内文件出现在宿主机时,权限会显示成宿主机 UID。例如容器内 chmod 666 /data/file,宿主机看到的是宿主机 UID chmod 666。
排查容器文件权限时:
docker exec -u <uid> <container> ls -la /path
- 镜像构建时是否通过
USER 切了普通用户
- 是否使用了
read_only: true / tmpfs 挂载
四、整体排查思路
按从最外层到最内层的顺序排查:
- 明确报错的具体命令、用户、文件、上下文。
- 看文件和父目录的标准权限位、owner、group。
- 检查进程实际用户和 supplementary groups。
- 检查 ACL、扩展属性。
- 检查挂载选项,特别是
ro / nosuid / noexec / acl。
- 检查 SELinux(如果启用)。
- 检查 AppArmor(如果启用)。
- 用
strace 取内核证据。
- 拿 audit 日志查被拒绝的系统调用。
- 验证修复 + 准备回滚。
具体执行每一步时都有明确判断逻辑,进步骤章节会展开。
五、实战步骤
下面从“现场拿到一个报错”开始,每一步给出命令、判断、动作。
步骤 0:明确报错上下文
目的:把问题描述压到一句最小可复现的句子:who: 用户; what: 操作; where: 路径; what happens: 报错。
收集的命令:
whoami
id
ls -la /path/to/file
ls -la /path/to/dir
# 触发报错
sudo -u user cat /path/to/file
sudo -u user bash -lc 'cd /dir && ls'
预期输出:得到报错原文(错误信息、错误码、关联错误号)。
异常表现:
- 错信息中没有“Permission denied”,可能是不同层的问题(路径不存在、磁盘只读、capability 缺失)。
- 多个用户报错,可能不是同一根因,需要样本分流。
下一步动作:进入步骤 1。
步骤 1:基础属性三件套
目的:用最少命令看清 owner、group、permission、type、modification、size。
命令:
ls -la <path>
stat <path>
file <path> # 文本 / 二进制 / 软链 / 块设备
判断逻辑:
- 文件 owner 是
root,用户不是 root:other 位决定。
- 是软链接:
ls -la 看链指哪里,stat 看的是目标文件。
mount 卷下文件权限位看起来对但仍然 Permission denied:考虑 ACL、mask、SELinux。
- 文件类型是 block / char device:可能是
nodev 阻止。
下一步动作:进入步骤 2。
步骤 2:查看完整权限数字与特殊位
目的:避免被 ls 显示迷惑,比如误把大写的 S 当 s,一定要看准 setuid / setgid / sticky。
命令:
stat -c '%a %A %n' <path>
# %a 八进制权限
# %A 字符权限(含特殊位)
判断逻辑:
0777 -rwxrwxrwx 没有特殊位,问题不在 setuid。
4755 -rwsr-xr-x 是 setuid 程序,执行时拿到 owner 身份。
- 看到大写
S:特殊位有但 x 没设,特意去 chmod u+x 才会让 s 有效。
- 看到
drwxrwxrwt:是 sticky,目录里只有 owner 能删自己的文件。
下一步动作:进入步骤 3。
步骤 3:判断进程身份与组的匹配
目的:搞清楚“谁去访问这个文件”。同样的 7xx 权限,进程身份不一样结果不一样。
命令:
# 当前 shell
id
# 服务进程身份
ps -eo pid,user,group,comm | grep -E 'nginx|mysql|java'
cat /proc/<pid>/status | grep -E '^(Uid|Gid|Groups)'
# 复现:以服务用户去访问
sudo -u <user> -- bash -lc 'id && cat /path/to/file'
判断逻辑:
- 如果
id 看到的 uid 不等于文件 owner,且 group 不在 supplementary groups 里,就只能按 other 匹配。
supplementary groups 一般从 /etc/group 派生,可查 getent group <group>。
- 某进程以 root 跑,看似什么都能做,但 SELinux 会拦截,这就是“root 也会被拒绝”的常见原因。
下一步动作:进入步骤 4。
步骤 4:ACL 与扩展属性
目的:标准 ugo 权限之外,ACL 与 xattr 会影响访问。
命令:
getfacl /path/to/file
getfattr -d /path/to/file
# 看看 mask 是否锁住了有效权限
getfacl /path/to/file | grep -E '^# (effective|mask)'
判断逻辑:
- 用户在 ACL 中单独给了权限,但 mask 是只读:实际权限被截断。
setfacl -m u:user:rw 后用 chmod 600,会把 mask 同步成 600,原来的 named entry 实际可用权限变成最小值。
noacl 挂载下,setfacl 会报“Operation not supported”,getfacl 只显示 classic ugo。
下一步动作:进入步骤 5。
步骤 5:挂载选项检查
目的:避开“权限位看上去对,但因为挂载选项而被拦截”的陷阱。
命令:
mount | grep $(df <path> | tail -1 | awk '{print $1}')
cat /proc/self/mounts
cat /etc/fstab | grep -E '^[^#]'
cat /proc/self/mountinfo | grep <path>
判断逻辑:
- 文件落在
ro 挂载卷上:写操作必失败(Read-only file system),权限位无关。
noexec 阻止执行 / sh 脚本(脚本 #!/bin/bash 第一行也无法直接 #!)。
- 容器内的
/tmp 是 nosuid,nodev,noexec:在 /tmp 里放脚本并 chmod +x 不能直接执行。
- 用户空间 mount 信息受
mount_namespaces 影响,容器场景下用 cat /proc/1/mountinfo 而不是宿主。
下一步动作:进入步骤 6。
步骤 6:SELinux 检查
目的:确认问题不是 LSM 拦截。
命令:
getenforce
sestatus
ls -Z /path/to/file
ps -Z -p <pid> | head
# 触发报错
sudo -u user cat /path/to/file
# 看 log
journalctl -t setroubleshoot
ausearch -m AVC -ts recent
判断逻辑:
- 模式是
enforcing,且 audit.log 有 avc: denied { read } 相关记录,根因就在 SELinux。
- 模式是
permissive,日志依然提示 denied:表示策略没让这个访问通过,但没真正阻止,临时把策略改成 enforcing 后才会影响业务。
- 文件 context 是
admin_home_t、tmp_t 等非预期值:context 不对,进程 domain 没权限读。
下一步动作:进入步骤 7。
步骤 7:AppArmor 检查
目的:Ubuntu / 部分发行版默认开启 AppArmor。
命令:
aa-status
cat /var/log/audit/audit.log | grep 'apparmor.*DENIED'
判断逻辑:
DENIED 操作出现,profile 是 enforce 模式:它就是被拒的原因。
- profile 是
complain:仅记录不阻止,可以切到 enforce 后看是否影响。
下一步动作:进入步骤 8。
步骤 8:strace 抓内核证据
目的:所有上层检查都无明确结论时,用 strace 拿到“具体哪个 syscall 返回什么错误”。
命令:
# -f 跟踪 fork 出来的子进程
# -e 只看特定系统调用,避免刷屏
# -tt 显示时间戳相对时间
strace -f -tt -e trace=openat,access,faccessat,stat,read,write,execve -p <pid> 2>&1 | head -200
# 单次执行:
strace -f -tt -e trace=openat,access cat /path/to/file 2>&1 | tail -20
判断逻辑:
openat("/path/to/file", O_RDONLY) = -1 EACCES (Permission denied):标准权限位被拒。
openat(...) = -1 ENOENT (No such file or directory):路径不存在或父目录 x 位缺失。
openat(...) = -1 EROFS (Read-only file system):挂载卷只读。
openat(...) = -1 EACCES ... 且 audit.log 有 AVC:SELinux 拦截,strace 也会标注 SELinux 相关 errno。
stat(...) = -1 EACCES,并且 parent dir 没有 x 位,进程甚至没法 stat 子条目。
下一步动作:进入步骤 9。
步骤 9:审计日志
目的:用 auditd 记录系统调用级事件,能查到业务应用自身不知道的内核拒绝。
命令:
# 启动 auditd,并 audit 文件访问
auditctl -w /path/to/file -p rwxa -k watch-file
auditctl -w /path/to/dir -p rwx -k watch-dir
# 等复现,再查
ausearch -k watch-file
ausearch -k watch-dir
# 通用:最近 AVC
ausearch -m AVC -ts recent
判断逻辑:
- audit log 中带
auid= 是登录用户 ID,便于做用户画像。
- audit log 中带
uid= 是执行进程的实际 UID。
- 看到
path=... 与 objtype=...(如 file、socket),确认拒绝对象。
下一步动作:进入步骤 10。
步骤 10:容器内权限问题
目的:容器内外 UID 不一致、namespace 不共享时,要进容器按容器视角看。
命令:
docker exec -it <container> bash
docker exec -u <uid> <container> bash
cat /proc/1/status | grep -E '^(Uid|Gid|Groups)'
mount | grep -E '/tmp|/var'
ls -laZ <path>
判断逻辑:
docker exec -u 33 <container> bash 失败:Passwd: u:33 is unknown to this system,容器里没这个 UID。
- 容器里
getcap 显示有 capability 而宿主机看不到:capability 在 user namespace 被映射。
- 容器内看到目录
drwxr-xr-x,但写入报错:可能是 docker 的 --read-only 或者上层卷是 ro。
- 容器使用
user: "1000:1000",宿主机需要 chown -R 1000:1000 才能在绑定挂载里写入。
下一步动作:步骤 11 给出修复 + 验证。
步骤 11:修复与回滚前的决定点
走到这一步通常已经定位到根因,根据不同根因有不同的修复动作。重点是“只改一个变量再验证”,不要一次性把多种修复混合。常见修复动作:
chmod 644 /path/to/file:权限位
chown user:group /path/to/file:owner/group
usermod -aG group user:补充组
setfacl -m u:user:r /path/to/file:ACL
mount -o remount,rw /mount_point:可写挂载
restorecon -Rv /path/to/dir:SELinux context 修复
setenforce 0 临时切 permissive 验证
- AppArmor:
aa-complain /usr/sbin/nginx
每修一次都重复步骤 11 的验证:复现报错动作,确认不再是 Permission denied。
风险提醒:
chmod -R 777:会让任何用户都能改,包括不该动的人。
setenforce 0:临时禁用 LSM,可能让攻击面扩大,验证完务必恢复。
setfacl -R -b:清空 ACL 是不可逆的(不备份的前提下)。
restorecon 大范围执行:可能误改其他相关目录 context,先测试再批量。
六、常用命令
按用途归档常用命令。
1. 看权限和属性
# 看权限
ls -la
stat
stat -c '%a %A %n'
# 看 owner
id
getent passwd user
getent group group
# 整目录递归
ls -laR
tree -pug
2. 改权限 / owner / group
chmod 644 file
chmod u=rw,g=r,o= file
chown user:group file
chown -R user:group dir
chgrp group file
3. 特殊位
chmod u+s file # setuid
chmod g+s file # setgid
chmod +t dir # sticky
4. ACL
getfacl file
setfacl -m u:lisi:rw file
setfacl -m g:dev:r file
setfacl -m d:u:lisi:r dir # default ACL,新文件继承
setfacl -x u:lisi file # 移除指定 ACL
setfacl -b file # 清空扩展 ACL
5. Capability
getcap /usr/bin/xxx
setcap cap_net_bind_service=+ep /usr/bin/xxx
getpcaps <pid>
6. SELinux
getenforce
setenforce 0 # 0 = permissive, 1 = enforcing
sestatus
ls -Z file
ps -Z -p pid
sealert -a /var/log/audit/audit.log
audit2why < /var/log/audit/audit.log
audit2allow -M mynginx < /var/log/audit/audit.log
# 长期修复
semanage fcontext -a -t httpd_sys_content_t '/var/www(/.*)?'
restorecon -Rv /var/www
7. AppArmor
aa-status
aa-complain /usr/sbin/nginx
aa-enforce /usr/sbin/nginx
aa-disable /usr/sbin/nginx
aa-logprof
# 强制重新加载
apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
8. 进程身份与 strace
id
ps -eo pid,user,group,command
cat /proc/<pid>/status | grep -E '^(Uid|Gid|Groups|Cap)'
strace -f -e trace=openat,access,read,write cat /path/to/file
# ltrace
ltrace -e 'getenv+puts+...+fopen' /usr/bin/xxx 2>&1
9. 挂载 / 文件系统
mount
cat /proc/self/mounts
cat /proc/self/mountinfo
findmnt /path
findmnt -t btrfs,ext4,xfs
tune2fs -l /dev/sda1 # 单独格式化属性
xfs_info /dev/sda1
10. 容器与 audit
docker exec -u <uid> <container> bash
nsenter -t <pid> -m -u -i -n -p -- /bin/bash
auditctl -w /path/to/file -p rwxa -k mykey
ausearch -k mykey
aureport --summary
七、配置示例
下面给几组典型场景的配置示例。
1. sudoers 标准片段
文件:/etc/sudoers.d/01-ops
# 不要直接改 /etc/sudoers,避免被包更新覆盖
Defaults env_keep += "PATH HOME LANG LC_ALL"
root ALL=(ALL:ALL) ALL
%admin ALL=(ALL) ALL
%sudo ALL=(ALL:ALL) ALL
# 特殊授权:希望 www-data 能 mount / umount 某个文件
%sysops ALL=(root) NOPASSWD: /bin/mount -o remount,rw /data
注意:/etc/sudoers 用 visudo 编辑(visudo -f /etc/sudoers.d/01-ops)。
2. ACL 在 NFS 上的 config
文件:/etc/exports(注意 NFS 默认 noacl)
/data *(rw,sync,no_subtree_check,no_root_squash,crossmnt)
挂载时启用 ACL:
mount -t nfs <server>:/data /mnt -o 'acl,vers=4.1'
NFS v4 内置 ACL,不需要 noacl,但 NFSv3 默认不支持。
3. SELinux 长期修复
文件:自定义 policy 模块(用 audit2allow 生成)
# mynginx.te
module mynginx 1.0;
require {
type httpd_t;
type var_log_t;
class file { open read };
}
allow httpd_t var_log_t:file { open read };
checkmodule -M -m -o mynginx.mod mynginx.te
semodule_package -o mynginx.pp -m mynginx.mod
semodule -i mynginx.pp
在确认根因之前,不要直接导入新策略模块。生产环境里先 setenforce 0 把模式切到 permissive,再观察一段时间,确认没有其他被拒绝的请求,最后用 audit2allow 把业务相关的都纳入同一模块。
4. systemd 服务以特定用户运行
文件:/etc/systemd/system/my-app.service
[Unit]
Description=My App
After=network.target
[Service]
ExecStart=/opt/myapp/run.sh
WorkingDirectory=/opt/myapp
User=app
Group=app
UMask=0002
# 如需要绑定 < 1024 端口,使用 capability 而不是 root
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# 资源限制
LimitNOFILE=65535
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
应用:
systemctl daemon-reload
systemctl start my-app
5. AppArmor 给一个脚本写 profile
文件:/etc/apparmor.d/usr.local.bin.myscript
#include <tunables/global>
/usr/local/bin/myscript {
#include <abstractions/base>
#include <abstractions/python>
/usr/local/bin/myscript r,
/etc/myapp/ r,
/etc/myapp/** r,
/var/log/myapp/ w,
/var/lib/myapp/ rw,
/run/myapp/ rw,
}
加载:
apparmor_parser -r /etc/apparmor.d/usr.local.bin.myscript
6. udev 规则:给特定设备固定 ownership
文件:/etc/udev/rules.d/90-usb-key.rules
SUBSYSTEM=="block", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", OWNER="ops", GROUP="ops", MODE="0660"
加载:
udevadm control --reload
udevadm trigger
7. PAM 强化
文件:/etc/pam.d/sshd 或 /etc/pam.d/system-auth
auth required pam_faillock.so preauth deny=5 unlock_time=300
auth required pam_faillock.so authfail deny=5 unlock_time=300
account required pam_faillock.so
文件:/etc/security/pwquality.conf
minlen = 12
minclass = 3
dcredit = -1
ucredit = -1
lcredit = -1
ocredit = -1
八、日志与指标观察方法
权限问题不像性能问题,没法靠单一指标直接收敛到根因,仍以日志为主。
1. 系统日志
journalctl -t kernel --since '15 minutes ago' | tail -100
journalctl -t setroubleshoot --since '15 minutes ago' | tail -50
dmesg -T | tail -50
2. SELinux 日志
ausearch -m AVC,USER_ROLE_CHANGE -ts today
sealert -a /var/log/audit/audit.log
grep -E 'avc:|denied' /var/log/audit/audit.log | tail -50
3. AppArmor 日志
grep -E 'apparmor.*DENIED' /var/log/audit/audit.log | tail -50
dmesg | grep -i 'apparmor'
4. 鉴权 / 登录日志
journalctl -u sshd --since '15 minutes ago'
grep -E 'Failed password' /var/log/secure
last -f /var/log/btmp | head
5. 业务侧日志
业务侧日志会直接给出关键字:
java.io.FileNotFoundException: ... (Permission denied)
nginx: ... (13: Permission denied) while reading upstream
EACCES: permission denied, open '/var/log/myapp.log'
nginx: connect() failed (111: Connection refused):注意这不是权限问题,可别混淆
6. strace 日志(手动抓)
把报错进程的 strace 输出保存下来:
strace -f -e trace=openat,access,read,write,fstat -p <pid> -o /tmp/strace.log
# 复现后用:
grep -E 'EACCES|EPERM|EROFS' /tmp/strace.log
7. audit 监控告警建议
如果要接 audit 日志到告警:
- 同一文件短时间内出现 > N 次 EACCES,触发告警。
- 同一 SELinux denied 记录高频出现,意味着业务有违规访问,建议排查程序是否异常启动。
# 一次性打 snapshot
ausearch -m AVC -ts today | grep count | tail -10
九、排查路径
把整篇文章的命令汇总成一个可执行路径树。
现象:业务 / 用户 / 服务报 "Permission denied"
│
├─ 步骤 0:明确报错上下文(who/what/where/报错原文)
│ └─ 报错原文是别的(如 EROFS / ENOENT / EACCES)
│ → 不一定是权限问题
│
├─ 步骤 1-2:基础属性 + 特殊位
│ ├─ 权限位对 → 步骤 3
│ └─ 权限位错 / 特殊位错 → chmod 修,复现
│
├─ 步骤 3:进程身份匹配
│ ├─ 用户在 owner / group 中 → 步骤 4
│ └─ 用户在 other → 是否该业务就该是 other?→ 步骤 4
│
├─ 步骤 4:ACL
│ ├─ getfacl 列表正常 → 步骤 5
│ └─ 有 mask / named user 异常 → setfacl 修
│
├─ 步骤 5:挂载选项
│ ├─ ro / noexec / nosuid 阻挡 → remount 或配置变更
│ └─ 挂载正常 → 步骤 6
│
├─ 步骤 6:SELinux
│ ├─ enforcing 且 audit 报 AVC → restorecon / policy 修
│ ├─ enforcing 但 audit 没有 → 步骤 7
│ └─ disabled → 步骤 7
│
├─ 步骤 7:AppArmor
│ ├─ DENIED → profile 调整
│ └─ 没有 DENIED → 步骤 8
│
├─ 步骤 8-9:strace + audit
│ ├─ 拿到明确 EACCES / EPERM + 现场调用栈 → 回到步骤 4/5/6 二次取证
│ └─ strace 无显著拒绝 → 继续排查
│
└─ 步骤 10:容器内权限
├─ 容器视角权限正常 → 宿主机 / 挂载问题
└─ 容器视角异常 → namespace / uid mapping 调整
十、风险提醒
排查与修复时,下面这些是高风险动作,必须结合备份 / 灰度 / 回滚。
chmod -R 777。表面解决了 A 问题,但同时让任何用户都可以随便改内容。如果是金融系统、密钥目录、配置文件目录、业务日志目录,千万别用。
setenforce 0 + setenforce 1 之后没复验。一旦忘记切回 enforcing,下次重启 LSM 状态会回归 disabled 或维持 permissive,攻击面扩大。一定要有“开 permissive、定时窗关回 enforcing”的 SOP。
restorecon -Rv /。会重置全盘 context,可能影响很大。务必限定到具体目录并提前 semanage fcontext -l 看现状。
setfacl -b -R dir。清空整个目录 ACL,在备份好之前不要执行。
usermod -aG group user。-a 不要漏,否则会替换用户的 supplementary group 列表,把生效的组清理掉。
mount -o remount,rw /。一旦业务层有 ro 挂载是因为一致性或 snapshot 原因,强行 remount 会带来数据不一致风险。
apt remove apparmor / 升级后 policy 默认 deny。在不熟的版本上禁用 LSM,可能导致登录失败、SSH 失败、内核模块装载失败。
chown -R 7777 这种 UID:把不属于业务的 ID 改到 root 才能识别的 UID 范围,会让一些服务找不到对应用户。
auditctl -a never,task。审计策略选错,可能耗尽磁盘,要确认 rule 的 key 和 path。
- 容器内
docker exec 改文件。docker exec 编辑的文件改动可能不会持久(取决于 bind-mount)。改完要确认。
十一、验证方式
不论做什么变更,验证要给出“通过 / 失败”的标准,避免“我感觉好了”。
验证 1:复现脚本
sudo -u <user> bash -lc 'cmd'
# 预期:不再有 Permission denied
验证 2:业务侧
- 通过业务接口验证:例如之前不能读文件,现在能读。
- 错误日志中“Permission denied”关键字频率降到 0。
验证 3:审计 / LSM 日志无新拒绝
ausearch -m AVC -ts recent
aa-status
journalctl -t setroubleshoot --since '1 hour ago'
验证 4:保持前后对照
# 变更前
ls -la /path/to/file
getfacl /path/to/file
ls -Z /path/to/file
mount | grep <path>
# 变更后
ls -la /path/to/file
getfacl /path/to/file
ls -Z /path/to/file
mount | grep <path>
确认是按预期变化,而不是其他副作用。
验证 5:strace 验证
strace -e trace=openat -p <pid> 2>&1 | grep -E 'EACCES|EPERM'
如果没有任何拒绝,且打开路径与预期一致,验证通过。
验证 6:自动化测试
如果要长期避免,回退前至少有一条端到端测试:
sudo -u app bash -lc 'cd /var/app && ./run-stest'
十二、回滚方案
准备每个修复动作对应的回滚。
回滚 chmod / chown / setfacl
# 写入备份
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
( cd /etc && find dir -printf '%p %u:%g %m\n' > "/var/backup/perm.${TIMESTAMP}.txt" )
# 回滚
xargs -a "/var/backup/perm.${TIMESTAMP}.txt" chmod --args
# 注意:上面的命令只是示意,恢复 owner 要先腾出元数据
实践更稳的做法:把权限位打到一个文件里,恢复时按文件遍历回设。
回滚 SELinux
setenforce 1
restorecon -Rv /path/to/dir
# 或:
semodule -r mynginx
回滚 AppArmor
apparmor_parser -R /etc/apparmor.d/usr.sbin.nginx
回滚 group / user
gpasswd -d user group
# 或
usermod -G <known-good-list> user
回滚 systemd unit
systemctl revert my-app
# 或
cp -a /etc/systemd/system/my-app.service.bak.* /etc/systemd/system/my-app.service
systemctl daemon-reload
systemctl restart my-app
回滚 mount / fstab
mount -o remount,ro /mount_point
sed -i 's/^UUID=.*\s\/\s.*$/原来行/' /etc/fstab
十三、生产环境注意事项
下面是权限问题的生产注意事项,把它沉淀成 checklist。
- 任何改变权限的命令都要先记录现状:用
stat -c 打快照到一个 /var/backup/perm/。
setenforce 0 是一次性开关,重启会失效。如果要做长期调整,走 sed -i 's/SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config。
restorecon 不带 -R 默认只作用于当前目录及其下文件。-R 递归,但小心范围。
- NFS / Samba 挂载点要明确
acl、vers。线上默认选项都不一定带 ACL。
- setuid 程序要做白名单。
/etc/audit/rules.d/audit.rules 里加上 -a always,exit -F path=/usr/bin/* -F perm=x -F auid>=1000 -F auid!=-1 -k privileged。
/tmp 不应该写敏感数据。/tmp 默认 sticky,但敏感文件仍可能泄漏。
- 任何 umask 调整:进程级 umask 会影响新文件默认权限,要在 systemd unit 里用
UMask=0007 等显式指定。
- 日志目录归属:日志目录多服务共用时谨慎混用 owner,建议每服务一个目录并赋予 specific uid。
- 不要用 ACL 滥用:ACL 多了排查困难,能用 ugo 权限解决的不要上 ACL。
- 关键路径同步策略:多台主机的
/etc/passwd、/etc/group、/etc/shadow 用统一配置管理(Ansible / Salt),不要散乱。
- 容器镜像构建时尽早切非 root。
USER app 写在 Dockerfile 下游,避免生产跑 root。
- 千万别让
ssh-keygen 的私钥所在目录可被同机其他用户读取。chmod 700 ~/.ssh,chmod 600 ~/.ssh/id_*。
/var/log/audit/ 占满磁盘会导致系统拒绝记录,从而丢审计日志。配合 max_log_file_action 配置 rotate。
- *`cap_
用 capsh 验证**:测试setcap cap_net_bind_service=+ep binary时用capsh --decode=<...>` 看是否正确。
十四、总结
把这次主题的关键点归纳成几句:
- 从出错的具体用户 / 命令 / 文件出发,先查标准 rwx,再查 owner / group,再查 ACL,再查 LSM。每一步都有可观察证据。
ls -la 看着对,未必真能访问。权限位只是第一道关;ACL、SELinux、AppArmor、挂载选项都可能阻挠。
- 修复要分步验证。
chmod → 测 → 测下一个;不要一次性多个变量一起改。
- 回滚路径必须先演练。线上工单里最容易出现的二次伤害是:乱配了 LSM 想恢复却恢复不回。
把这一套路写在团队 wiki 上,配合 auditd watch 关键文件、SELinux/AppArmor 状态监控,能把权限类工单从散乱的“凭直觉”变成“流程化处置”。
附录 A:内核 sysctl / procfs 关键开关
权限问题往往伴随系统行为边界,下面几个 procfs 路径值得知道,能避免反复被“隐藏配置”坑。
# proc 的特殊目录
/sys/kernel/security/lsm
/proc/sys/kernel/cap_last_cap
/proc/sys/fs/protected_*
/proc/sys/fs/protected_regular
/proc/sys/fs/Protected_hardlinks
/proc/sys/fs/Protected_pipe
/proc/sys/fs/Protected_symlinks
# 用户空间进程默认权限
cat /proc/self/loginuid
cat /proc/self/uid_map
protected_* 是 hardlink / symlink 保护开关,开启后会在硬链接 / 软链接做防御:
kernel.yama.protected_sticky_symlinks = 1:限制用户跟随 world-writable 目录里的符号链接。
kernel.yama.protected_hardlinks = 1:限制非所有者创建硬链接。
这些不影响直接权限判断,但影响业务场景里“应用跟随软链执行”类逻辑。
/proc/self/uid_map 是 user namespace 的映射关系,做容器时常常要修改:
echo "0 100000 65536" > /proc/<pid>/uid_map
附录 B:常见 pid / namespace debug
权限问题在多进程协同(systemd + 容器 + 主机)下,需要看清进程跑的 namespace:
# 进程所在的 namespace
ls -la /proc/<pid>/ns
# 进入指定进程的 namespace
nsenter -t <pid> -m -u -i -n -p -- /bin/bash
nsenter 是处理容器内外权限差异的关键工具,能让运维临时获得容器视角的 id / mount。
附录 C:典型工单:5 个真实场景闭环
下面是我和生产团队这几年的真实工单,每条都按闭环组织。
工单 C.1:nginx 报 Permission denied while reading upstream
现象:nginx 反代 upstream 报错,error.log 出现 Permission denied 在和 upstream 通信之后,紧接着 502。
初步判断:多数人会猜 SELinux。少数情况是反向代理接口落到了本地 socket。
命令检查:
ls -la /var/run/upstream.sock
ls -Z /var/run/upstream.sock
ps -Z -p $(pgrep -f nginx) | head
audit2why < /var/log/audit/audit.log | head
关键指标:
/var/run/upstream.sock 是 root:root 666
- nginx domain 是
httpd_t
- upstream 服务 domain 是
unconfined_t 或 initrc_t
根因定位:SELinux policy 不允许 httpd_t 访问 unconfined_t 的 socket。audit2why 输出大致类似 allow httpd_t unconfined_t:unix_dgram_socket read/write;,说明需要给 httpd_t 一个允许规则。
修复方案:
semanage fcontext -a -t 'httpd_sys_rw_t' /var/run/upstream.sock
restorecon -v /var/run/upstream.sock
# 或者改上游 socket 路径到 nginx 默认允许的 /var/cache/nginx/...
验证:
sudo -u nginx -- bash -lc 'cat /var/run/upstream.sock && echo ok' || true
curl --unix-socket /var/run/upstream.sock http://localhost/healthz
回滚预案:
semanage fcontext -d /var/run/upstream.sock
restorecon -v /var/run/upstream.sock
复盘:要在自己服务里建通用 socket 路径时,要确认 SELinux 是否允许当前进程 domain 反代。
工单 C.2:Docker 内应用 touch: cannot touch '/data/flag': Permission denied
现象:容器内应用尝试写入 /data/flag,提示 Permission denied;host 上 /data 目录是 777。
命令检查:
docker exec <container> ls -la /data
docker exec <container> id
docker exec <container> cat /proc/1/status | grep -E '^(Uid|Gid)'
关键指标:host 上 /data owner 是 1000;container USER 不是 1000,是 0 或 33。
根因:UID 不对应,容器内进程不能写宿主机 1000 用户拥有的目录。
修复:docker-compose.yml 里加入 user: "1000:1000",或在 docker run 用 --user 1000:1000。
验证:
docker exec -u 1000 <container> bash -lc 'touch /data/flag && echo ok'
回滚:docker run --user 0 ... 临时恢复回 root,但不要久用。
工单 C.3:业务报 bash: ./run.sh: Permission denied
现象:shell 脚本首行 #!/bin/bash,但报 Permission denied。
命令检查:
ls -la run.sh
mount | grep $(pwd)
file run.sh
关键指标:文件 mode 可能是 644(缺 x);卷挂载可能包含 noexec。
根因:通常两者之一:chmod +x run.sh 即可;临时绕过:bash run.sh。
修复:chmod +x run.sh,如果挂在 noexec,编辑 fstab 去掉。
验证:
./run.sh --ok-test
回滚:不重要,必要时 chmod 回原来权限。
工单 C.4:sudo 提示 unable to resolve host
现象:sudo 在容器 / 临时主机里报 unable to resolve host xxx。
命令检查:
cat /etc/hosts
hostname
cat /etc/resolv.conf
关键指标:/etc/hosts 缺少主机名到 127.0.0.1 行映射。
修复:
echo "127.0.0.1 $(hostname)" >> /etc/hosts
验证:sudo id 不再报错。
工单 C.5:git pull 在 NFS 卷上 Permission denied
现象:git 共享卷是 NFSv3,挂载选项默认 noacl;git 拉取时无法修改某些 metadata。
命令检查:
mount | grep <nfs-mount>
getfacl <file>
lsattr <file>
关键指标:NFSv3 不支持 ACL,但 setfacl 又在很多机器上默认启用了。
修复:改用 NFSv4:mount 时带 vers=4.1,acl。
附录 D:调试权限时的“安全命令”小抄
下面是一些安全的命令,适合贴工位上。
# 看进程特权
getpcaps <pid>
ps -o pid,uid,gid,euid,egid,suid,sgid,command -p <pid>
# 看进程当前 cap
cat /proc/<pid>/status | grep ^Cap
# 看文件 owner 与历史 owner
ls -ln file
find dir -printf '%h/%f uid=%u gid=%g mode=%m type=%y size=%s\n'
# 看补充组
id user
groups user
# 看 secure_path
sudo -l | grep secure_path
# 看 service unit 限制
systemctl cat my-app | grep -E 'Limit|PID|UMask|Capabilit'
附录 E:常用 ACL 模式速查
下面是我自己常用的 ACL 配方。
# 给特定用户加只读
setfacl -m u:user:r file
# 给特定用户加读写
setfacl -m u:user:rw file
# 给特定组加写
setfacl -m g:dev:rw file
# 给现有文件加 default ACL(让新建文件继承)
setfacl -m d:g::rw -d /path
# 删除特定 ACL
setfacl -x u:user file
# 备份 ACL
getfacl -R dir > /tmp/acl.bak
# 恢复 ACL
setfacl --restore=/tmp/acl.bak
附录 F:SELinux policy 三类常见模块
semodule -l 可以看到已装模块,挑三个常见的:
selinux-policy:基础 policy。
semanage:管理工具。
policycoreutils-python-utils:包含 audit2allow、sealert。
# 列出 enabled 模块
semodule -l | head -20
# 看模块细节
semodule --info=selinux-policy
# 装载自定义模块
semodule -i mynginx.pp
# 移除
semodule -r mynginx
附录 G:profile / policy 文件结构简介
/etc/apparmor.d/ 下是 profile。/etc/selinux/targeted/policy/ 下是二进制 policy。
AppArmor profile 常见关键字:
network inet stream,
file,
capability,
owner,
dbus,
mount,
ptrace,
signal,
unix,
SELinux type enforcement(.te)文件常见字段:
module mypol 1.0;
require {
class file { open read };
role source_r;
type src_t;
type dst_t;
}
allow src_t dst_t:file { open read };
不要在生产上手写 .te,通常先用 audit2allow 生成。
附录 H:开源工具生态
下面几个工具是我日常会用到的:
setpriv:以指定 uid/gid/capability 跑命令,比 su 轻量。
runuser:与 su 类似,用于强制以指定 uid 启动脚本。
getcap / setcap:能力位管理。
nsenter:进入指定进程 namespace。
chroot / bwrap:轻量级 chroot/sandbox 工具。
smack 系列:另一套 LSM,但生产用得少。
efivars 相关:与 UEFI SecureBoot 相关权限。
注意:任何卸载 LSM 的操作都建议先在 dev 环境模拟,避免在生产做不可逆变更。
附录 I:跨主机一致性检查
线上多机环境里,同一台主机有时会因为 /etc/passwd、/etc/group、/etc/shadow、/etc/sudoers、/etc/dconf 等文件不一致出现“明明一台机能用,另一台不行”的情形。
用 ansible 或 puppet 统一收口是个办法:
ansible all -m copy -a 'src=/etc/passwd dest=/etc/passwd'
ansible all -m copy -a 'src=/etc/group dest=/etc/group'
ansible all -m copy -a 'src=/etc/shadow dest=/etc/shadow mode=0600'
ansible all -m shell -a 'getent passwd {1000..1010}'
发现差异立刻拉齐。aide 或 tripwire 监控关键文件变更也很有用。
附录 J:何时考虑“重新走 LDAP / SSSD 集中账号”
如果维护多套 /etc/passwd、/etc/group 已经非常痛苦,可以把账号统一到 LDAP / FreeIPA,并把客户端切到 sssd + nslcd。
sssd 提供本地缓存,业务离线时仍然可用。
nslcd 是 LDAP / nssswitch / pam 的标准组合。
部署时注意:
- 先在测试域打通
getent passwd <username>
- 再迁移生产主机。
附录 K:key 文件和 SSH 权限清单
SSH 关联文件,权限是“严格”要求的:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_rsa
chmod 644 ~/.ssh/id_rsa.pub
chmod 644 ~/.ssh/known_hosts
chmod 600 ~/.ssh/config
chmod 644 ~/.ssh/authorized_keys
排查 SSH 连接被拒
# 服务端
ssh -vvv user@host
ls -ld ~/.ssh ~/.ssh/authorized_keys
stat -c '%a %U:%G' ~/.ssh ~/.ssh/authorized_keys
有时 Linux Permission 仅是 SSH 拒绝的原因之一,更多是 AuthorizedKeysFile、StrictModes、PermitRootLogin、PubkeyAuthentication 配置。下文给一个最小化清单:
# /etc/ssh/sshd_config
PubkeyAuthentication yes
PermitRootLogin prohibit-password
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes
AllowGroups ssh-users
附录 L:日志文件清理与权限
清理日志是有风险的:
truncate /var/log/xxx.log 会让 logrotate 判时点错,建议用 > /var/log/xxx.log,并配合日志服务(如 rsyslog)reopen。
rm /var/log/xxx.log 会让打开它的进程继续写(继续写入 unlinked inode)。
- 用
logrotate 才是标准化方式:postrotate 触发 kill -HUP。
权限经常被忽略:
/var/log/ 是 root:root 0755,但部分日志需要 adm 组可读,如 /var/log/auth.log。
附录 M:socket / pipe / FIFO 权限
文件 socket 是一种特殊的文件,权限要按 socket 本身的 mode 来设置,客户端启动时用 unix socket 路径连。
M.1 MySQL socket
ls -la /var/lib/mysql/mysql.sock
# srwxrwxr-x 1 mysql mysql 0 ... mysql.sock
如果有其他用户要连(一般是错的),可调 mode,但不要 777。
M.2 Redis socket
- 配置
unixsocket /tmp/redis.sock 和 unixsocketperm 770。
- 在外网/容器中走 TCP,而不是 socket。
M.3 Postfix socket
/var/spool/postfix/private/*:socket 共享队列。
附录 N:Capabilities 实战
N.1 最小权限运行 Web 服务
要让一个非 root 用户绑定 80 端口:
setcap cap_net_bind_service=+ep /usr/bin/python3.11
sudo -u app python3.11 -m http.server 80
N.2 dump cap 列表
capsh --decode=<HEX_MASK>
# 或
getpcaps <pid>
N.3 限制服务 cap
# systemd unit
[Service]
User=app
Group=app
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
LockPersonality=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectHome=yes
ProtectSystem=full
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
附录 O:容器权限矩阵
容器内权限可用下面 5 个维度评估:
| 维度 |
选项 |
影响 |
| USER |
root / non-root |
默认是 root;改 non-root 时主机文件要 chown |
| capability |
一组 |
默认是 Docker 默认 cap;做 K8s 时按需求关闭 |
| seccomp |
system / custom |
默认 system;可以自定义 syscall 白名单 |
| selinux / apparmor |
label / profile |
默认为 unconfined;启用后按策略 |
| readonly rootfs |
true / false |
true 时容器内 / 没法写;做 secret mount 必要 |
附录 P:错误码速查
| err code |
errno |
含义 |
| EACCES |
13 |
一般权限位被拒 |
| EPERM |
1 |
操作不允许(不是 rwx,是策略性) |
| EROFS |
30 |
只读文件系统 |
| ELOOP |
40 |
软链循环 / 路径深度超限 |
| ENOENT |
2 |
路径不存在;可能父目录缺 x 位 |
| ESTALE |
116 |
NFS 文件过期 |
| ETXTBSY |
26 |
写正在执行的可执行 |
| EFAULT |
14 |
缓冲区越界 / 段错误 |
| EIO |
5 |
IO 错误,但不代表权限(注意) |
| ENOMEM |
12 |
内存不足 |
| ESRCH |
3 |
进程不存在 |
附录 Q:相关 Linux 文档与 man
查 man 能找到更详细的字段:
man 1 chmod
man 1 chown
man 1 getfacl
man 5 acl
man 5 capabilities
man 8 setcap
man 8 getcap
man 8 setenforce
man 8 semanage
man 8 auditctl
man 8 nsenter
man 5 apparmor.d
man 5 sudoers
附录 R:写在最后
权限问题不复杂但很碎。最好用的工具依然是 流程化 + 最小变更:
- 不要一上来就
chmod 777,先查再改。
- 改了任何权限,立刻写一份
/var/backup/perm.YYYYMMDD-HHMMSS 快照。
- 修了 LSM(SELinux/AppArmor)相关的,遇到不确定时先
permissive / complain 而不是直接关闭。
- 任何
rm、setfacl -b -R、整卷 chown -R 永远要二次确认。
按上面这个节奏,工单处理时间通常从“折腾两小时”降到“5 分钟内找到根因”。
附录 S:典型 bash / sh 脚本失败的 6 个坑
下面 6 个坑是写权限相关脚本时最容易踩的。
坑 1:变量没加引号,路径带空格就跪
BACKUP="/var/log/my app.log"
ls -la $BACKUP
# 实际执行的是:ls -la /var/log/my app.log
# 正确:ls -la "$BACKUP"
解决:所有路径变量都用 "$BACKUP"。
坑 2:路径与目录不同步
cd "$DIR"
# 注意:cd 之后如果 DIR 不存在,下面的命令行 $PWD 不会更新,除非重新读。
解决:每条命令检查 cd $DIR && ...。
坑 3:批量删除没二次确认
find /var/log -type f -name "*.log" -mtime +7 -delete
# 高风险操作
解决:加 -print 或 -ok。
坑 4:目录递归 chmod 时没用 -X
chmod -R u=rwX,go=rX .
# X = 只给可执行文件 / 目录加成可执行
# 不加 X 会导致普通文件变成 rw,但非文件不期望;
解决:明确 -X 或 -x。
坑 5:脚本退出码没处理
mount /dev/sda1 /mnt
# 挂在失败,但 echo 没输出,下面的程序以为成功了
解决:用 set -euo pipefail 或每条命令检查 $?。
坑 6:硬编码路径在不同发行版上移位
# CentOS
ls /etc/sysconfig/iptables
# Ubuntu
ls /etc/iptables/rules.v4
解决:判断 /etc/os-release,脚本里区分发行版。
附录 T:常见 bad case 组合 + 修复
T.1 setuid + nosuid
mount -o remount,suid /tmp
# 临时解除 nosuid,或编辑 /etc/fstab
T.2 read-only 上写
# 重 mount
mount -o remount,rw /path
# 然后看 /proc/mounts 是否生效
T.3 setcap 看不到
# 容器里 setcap 不会被保留
# K8s 里,做法:
# securityContext.capabilities.add: [NET_BIND_SERVICE]
# securityContext.capabilities.drop: ["*"]
T.4 AppArmor + Docker
docker run --security-opt apparmor=my-profile ...
T.5 redis 不能 bind 0.0.0.0
bind 127.0.0.1 ::1 或 protected-mode yes 时,仅允许本地。生产开 bind 0.0.0.0 要把 protected-mode no,并且防火墙开放。注意:bind 接 IPv6 前缀 - 的写法仅在部分版本支持,建议直接 bind 0.0.0.0。
附录 U:监控项(用 Prometheus exporter)
权限类故障在业务指标里体现不像性能问题那样直观,但可以做以下监控:
# 审计日志:单位时间 AVC 计数
audit_status="ausearch -m AVC -ts today | wc -l"
# /etc/lsb-release 中 SELinux 模式监控
getenforce
# AppArmor 监控
aa-status
# strace 类监控一般不上线
更重要的是把业务侧的 Permission denied 类错误做关键字告警:
grep -E 'Permission denied|EACCES|EPERM' /var/log/business.log 频率
附录 V:相关参考资源
- Kernel
Documentation/admin-guide/cgroups-v2.rst
- Kernel
Documentation/admin-guide/LSM/
systemd.exec 手册:详细描述所有 LimitNOFILE、User、Group、Capabilities、NoNewPrivileges 等配置项
- Linux 文档项目
http://tldp.org
- Red Hat Enterprise Linux SELinux 文档
- Ubuntu AppArmor 文档
- Man pages:上面已经列出,直接
man 即可
附录 W:练习题
为了让读者练习,列 5 个 mini 工单出来:
W.1 调试题:服务 db.service 无法访问 /var/lib/mysql/data
- 写出 5 个排查步骤。
- 答案:步骤 1 看 owner、步骤 2 看 mode、步骤 3 看 SELinux context、步骤 4 看 AppArmor、步骤 5 走 strace。
W.2 看图题:drwxrwx--T 4 root web 4096 /var/www/uploads
- 问:其他人能删除
/var/www/uploads/x.png 吗?
- 答:不能,sticky 阻止。这是“其他人只能删自己”的语义。
W.3 排错题:/opt/app/run.sh 有 x,但运行 bash: /opt/app/run.sh: No such file or directory
- 答:通常是第一行
#!/bin/bash 指向不存在的解释器,或 #! 后必须紧跟解释器路径。
W.4 排错题:应用 EACCES 在容器内,但 host 上正常
- 答:UID 映射不匹配、容器内进程非运行用户、host 上目录为只读。
W.5 设计题:实现一个最小化的 my-cron 服务,通过 systemd 部署且以非 root 运行
- 答:参考附录 N 中的 systemd unit。注意:cron 调度通常用
crond 而不是自写。
附录 X:一种典型的权限问题操作流程图
[Receive ticket]
|
v
[Collect facts: who/what/where/报错原文]
|
v
[step 1-2: file info & mode]
|
v
[step 3: identity match?]
|
v
[step 4: ACL?]
|
v
[step 5: mount options?]
|
v
[step 6: SELinux?]
|
v
[step 7: AppArmor?]
|
v
[step 8: strace?]
|
v
[step 9: audit log?]
|
v
[step 10: container?]
|
v
[Identify root cause]
|
v
[Apply fix + verify]
|
v
[Record snapshot + rollback path ready]
|
v
[Close ticket with evidence]
附录 Y:生产环境变更 Checklist
[ ] 备份当前 permission: stat / getfacl / ls -lR / find -printf
[ ] 备份当前 owner: getent passwd / getent group
[ ] 备份当前 LSM 状态: getenforce / aa-status / sestatus
[ ] 备份 mount: mount / fstab
[ ] 备份 capability: getcap -r /
[ ] 备份 SELinux: semanage fcontext -l -C
[ ] 应用变更
[ ] 复现命令验证不再 Permission denied
[ ] 业务侧验证
[ ] audit 日志确认无新拒绝
[ ] 通知变更窗口
[ ] 文档记录更新