Linux 权限问题排查:为什么明明有文件却访问不了?
一、问题背景
“为什么明明有文件却访问不了”这类权限工单,几乎是 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”,而在四个层面之间的相互作用:
-
标准 UNIX 权限位(owner/group/other + rwx) -
文件系统 ACL 与扩展属性 -
Linux 安全模块(SELinux / AppArmor) -
进程身份与命名空间(uid/gid、capability、容器 namespace)
如果只看一层,常常查不出真正的根因。本文用一条完整的排查主线,逐步把这四层都串起来。让读者遇到类似工单时,能按照这个流程稳定收敛并定位。
二、适用场景
下面这些场景明确在本文覆盖范围内:
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-- 就是这三组。
数字表示:
r = 4w = 2x = 1
例如 rwxr-xr-- 即 754。
文件 vs 目录的 rwx 含义差异:
|
|
|
|
|---|---|---|
|
|
|
ls)
|
|
|
|
|
|
|
|
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
三个特殊位,影响权限匹配:
|
|
|
|
|---|---|---|
|
|
-rws------
s
|
|
|
|
-rwxrws---
s
|
|
|
|
drwxrwxrwt
t
|
|
典型场景:
/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 选项后,用 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. SELinux(CentOS / RHEL / Fedora 默认)
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 |
|
nodev |
|
relatime
atime
|
|
acl
noacl
|
|
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: 报错。
收集的命令:
bash 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。
命令:
bash 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。
命令:
bash 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 权限,进程身份不一样结果不一样。
命令:
bash # 当前 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 会影响访问。
命令:
bash 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:挂载选项检查
目的:避开“权限位看上去对,但因为挂载选项而被拦截”的陷阱。
命令:
bash 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 拦截。
命令:
bash 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。
命令:
bash aa-status
cat /var/log/audit/audit.log | grep 'apparmor.*DENIED'
判断逻辑:
DENIED操作出现,profile 是 enforce模式:它就是被拒的原因。-
profile 是 complain:仅记录不阻止,可以切到 enforce 后看是否影响。
下一步动作:进入步骤 8。
步骤 8:strace 抓内核证据
目的:所有上层检查都无明确结论时,用 strace 拿到“具体哪个 syscall 返回什么错误”。
命令:
bash # -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 记录系统调用级事件,能查到业务应用自身不知道的内核拒绝。
命令:
bash # 启动 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 不共享时,要进容器按容器视角看。
命令:
bash 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. 看权限和属性
bash # 看权限
ls -la
stat
stat -c '%a %A %n'
# 看 owner
id
getent passwd user
getent group group
# 整目录递归
ls -laR
tree -pug
2. 改权限 / owner / group
bash chmod 644 file
chmod u=rw,g=r,o= file
chown user:group file
chown -R user:group dir
chgrp group file
3. 特殊位
bash chmod u+s file # setuid
chmod g+s file # setgid
chmod +t dir # sticky
4. ACL
bash 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
bash getcap /usr/bin/xxx
setcap cap_net_bind_service=+ep /usr/bin/xxx
getpcaps <pid>
6. SELinux
bash 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
bash 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
bash 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. 挂载 / 文件系统
bash 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
bash 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:
bash mount -t nfs <server>:/data /mnt -o 'acl,vers=4.1'
NFS v4 内置 ACL,不需要 noacl,但 NFSv3 默认不支持。
3. SELinux 长期修复
文件:自定义 policy 模块(用 audit2allow 生成)
te # 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 };
bash 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
ini [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
应用:
bash 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,
}
加载:
bash 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"
加载:
bash 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. 系统日志
bash journalctl -t kernel --since '15 minutes ago' | tail -100
journalctl -t setroubleshoot --since '15 minutes ago' | tail -50
dmesg -T | tail -50
2. SELinux 日志
bash 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 日志
bash grep -E 'apparmor.*DENIED' /var/log/audit/audit.log | tail -50
dmesg | grep -i 'apparmor'
4. 鉴权 / 登录日志
bash 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 upstreamEACCES: permission denied, open '/var/log/myapp.log'nginx: connect() failed (111: Connection refused):注意这不是权限问题,可别混淆
6. strace 日志(手动抓)
把报错进程的 strace 输出保存下来:
bash 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 记录高频出现,意味着业务有违规访问,建议排查程序是否异常启动。
bash # 一次性打 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 状态会回归 disbled 或维持 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 范围,会让一些服务(比如 ssh-agent、cron)找不到对应用户。 auditctl -a never,task。审计策略选错,可能耗尽磁盘,要确认 rule 的 key 和 path。 - 容器内
docker exec改文件。 docker exec编辑的文件改动可能不会持久(取决于 bind-mount)。改完要确认。
十一、验证方式
不论做什么变更,验证要给出“通过 / 失败”的标准,避免“我感觉好了”。
验证 1:复现脚本
bash sudo -u <user> bash -lc 'cmd'
# 预期:不再有 Permission denied
验证 2:业务侧
-
通过业务接口验证:例如之前不能读文件,现在能读。 -
错误日志中“Permission denied”关键字频率降到 0。
验证 3:审计 / LSM 日志无新拒绝
bash ausearch -m AVC -ts recent
aa-status
journalctl -t setroubleshoot --since '1 hour ago'
验证 4:保持前后对照
bash # 变更前
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 验证
bash strace -e trace=openat -p <pid> 2>&1 | grep -E 'EACCES|EPERM'
如果没有任何拒绝,且打开路径与预期一致,验证通过。
验证 6:自动化测试
如果要长期避免,回退前至少有一条端到端测试:
bash sudo -u app bash -lc 'cd /var/app && ./run-stest'
十二、回滚方案
准备每个修复动作对应的回滚。
回滚 chmod / chown / setfacl
bash # 写入备份
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
bash setenforce 1
restorecon -Rv /path/to/dir
# 或:
semodule -r mynginx
回滚 AppArmor
bash apparmor_parser -R /etc/apparmor.d/usr.sbin.nginx
回滚 group / user
bash gpasswd -d user group
# 或
usermod -G <known-good-list> user
回滚 systemd unit
bash 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
bash 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 路径值得知道,能避免反复被“隐藏配置”坑。
bash # 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 的映射关系,做容器时常常要修改:
bash echo"0 100000 65536" > /proc/<pid>/uid_map
附录 B:常见 pid / namespace debug
权限问题在多进程协同(systemd + 容器 + 主机)下,需要看清进程跑的 namespace:
bash # 进程所在的 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
C.1.1 现象
-
nginx 反代 upstream 报错。 -
error.log 出现 Permission denied在和 upstream 通信之后,紧接着 502。
C.1.2 初步判断
多数人会猜 SELinux。少数情况是反向代理接口落到了本地 socket。
C.1.3 命令检查
bash 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
C.1.4 关键指标
/var/run/upstream.sock是 root:root 666-
nginx domain 是 httpd_t -
upstream 服务 domain 是 unconfined_t或initrc_t
C.1.5 根因定位
SELinux policy 不允许 httpd_t 访问 unconfined_t 的 socket。audit2why 输出大致类似 allow httpd_t unconfined_t:unix_dgram_socket read/write;,说明需要给 httpd_t 一个允许规则。
C.1.6 修复方案
bash semanage fcontext -a -t 'httpd_sys_rw_t' /var/run/upstream.sock
restorecon -v /var/run/upstream.sock
# 或者改上游 socket 路径到 nginx 默认允许的 /var/cache/nginx/...
C.1.7 验证
bash sudo -u nginx -- bash -lc 'cat /var/run/upstream.sock && echo ok' || true
curl --unix-socket /var/run/upstream.sock http://localhost/healthz
C.1.8 回滚预案
bash semanage fcontext -d /var/run/upstream.sock
restorecon -v /var/run/upstream.sock
C.1.9 复盘
要在自己服务里建通用 socket 路径时,要确认 SELinux 是否允许当前进程 domain 反代。
工单 C.2:Docker 内应用 touch: cannot touch '/data/flag': Permission denied
C.2.1 现象
-
容器内应用尝试写入 /data/flag,提示Permission denied。 -
host 上 /data目录是777。
C.2.2 命令检查
bash docker exec <container> ls -la /data
docker exec <container> id
docker exec <container> cat /proc/1/status | grep -E '^(Uid|Gid)'
C.2.3 关键指标
-
host 上 /dataowner 是 1000。 -
container USER 不是 1000,是 0 或 33。
C.2.4 根因
UID 不对应,容器内进程不能写宿主机 1000 用户拥有的目录。
C.2.5 修复
docker-compose.yml 里加入 user: "1000:1000",或在 docker run 用 --user 1000:1000。
C.2.6 验证
bash docker exec -u 1000 <container> bash -lc 'touch /data/flag && echo ok'
C.2.7 回滚
docker run --user 0 ... 临时恢复回 root,但不要久用。
工单 C.3:业务报 bash: ./run.sh: Permission denied
C.3.1 现象
-
shell 脚本首行 #!/bin/bash,但报Permission denied。
C.3.2 命令检查
bash ls -la run.sh
mount | grep $(pwd)
file run.sh
C.3.3 关键指标
-
文件 mode 可能是 644(缺 x) -
卷挂载可能包含 noexec
C.3.4 根因
通常两者之一:
chmod +x run.sh即可 -
临时绕过: bash run.sh
C.3.5 修复
chmod +x run.sh,如果挂在 noexec,编辑 fstab 去掉。
C.3.6 验证
bash ./run.sh --ok-test
C.3.7 回滚
不重要,必要时 chmod 回原来权限。
工单 C.4:sudo 提示 unable to resolve host
C.4.1 现象
sudo在容器 / 临时主机里报 unable to resolve host xxx。
C.4.2 命令检查
bash cat /etc/hosts
hostname
cat /etc/resolv.conf
C.4.3 关键指标
/etc/hosts缺少主机名 → 127.0.0.1 行映射。
C.4.4 修复
bash echo"127.0.0.1 $(hostname)" >> /etc/hosts
C.4.5 验证
sudo id 不再报错。
工单 C.5:git pull 在 NFS 卷上 Permission denied
C.5.1 现象
-
git 共享卷是 NFSv3,挂载选项默认 noacl。 -
git 拉取时无法修改某些 metadata。
C.5.2 命令检查
bash mount | grep <nfs-mount>
getfacl <file>
lsattr <file>
C.5.3 关键指标
-
NFSv3 不支持 ACL,但 setfacl又在很多机器上默认启用了。
C.5.4 修复
-
改用 NFSv4:mount 时带 vers=4.1,acl。
附录 D:调试权限时的“安全命令”小抄
下面是一些安全的命令,适合贴工位上。
bash # 看进程特权
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 配方。
bash # 给特定用户加只读
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。
bash # 列出 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)文件常见字段:
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 统一收口是个办法:
bash 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 连接被拒
bash # 服务端
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 端口:
bash setcap cap_net_bind_service=+ep /usr/bin/python3.11
sudo -u app python3.11 -m http.server 80
N.2 dump cap 列表
bash 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 个维度评估:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
附录 P:错误码速查
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
附录 Q:相关 Linux 文档与 man
查 man 能找到更详细的字段:
man 1 chmodman 1 chownman 1 getfaclman 5 aclman 5 capabilitiesman 8 setcapman 8 getcapman 8 setenforceman 8 semanageman 8 auditctlman 8 nsenterman 5 apparmor.dman 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 个坑是写权限相关脚本时最容易踩的,做一些通用 case:
坑 1:变量没加引号,路径带空格就跪
bash BACKUP="/var/log/my app.log"
ls -la $BACKUP
# 实际执行的是:ls -la /var/log/my app.log
# 正确:ls -la "$BACKUP"
解决:所有路径变量都 "$BACKUP"。
坑 2:路径与目录不同步
bash cd"$DIR"
# 注意:cd 之后如果 DIR 不存在,下面的命令行 $PWD 不会更新,除非重新读。
解决:每条命令检查 cd $DIR && ...。
坑 3:批量删除没二次确认
bash find /var/log -type f -name "*.log" -mtime +7 -delete
# 高风险操作
解决:加 -print 或 -ok。
坑 4:目录递归 chmod 时没用 -X
bash chmod -R u=rwX,go=rX .
# X = 只给可执行文件 / 目录加成可执行
# 不加 X 会导致普通文件变成 rw,但非文件不期望;
解决:明确 -X 或 -x。
坑 5:脚本退出码没处理
bash mount /dev/sda1 /mnt
# 挂在失败,但 echo 没输出,下面的程序以为成功了
解决:用 set -euo pipefail 或每条命令检查 $?。
坑 6:硬编码路径在不同发行版上移位
bash # CentOS
ls /etc/sysconfig/iptables
# Ubuntu
ls /etc/iptables/rules.v4
解决:判断 /etc/os-release,脚本里区分发行版。
附录 T:常见 bad case 组合 + 修复
T.1 setuid + nosuid
bash mount -o remount,suid /tmp
# 临时解除 nosuid,或编辑 /etc/fstab
T.2 read-only 上写
bash # 重 mount
mount -o remount,rw /path
# 然后看 /proc/mounts 是否生效
T.3 setcap 看不到
bash # 容器里 setcap 不会被保留
# K8s 里,做法:
# securityContext.capabilities.add: [NET_BIND_SERVICE]
# securityContext.capabilities.drop: ["*"]
T.4 AppArmor + Docker
bash 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)
权限类故障在业务指标里体现不像性能问题那样直观,但可以做以下监控:
bash # 审计日志:单位时间 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:一种典型的权限问题操作流程图
text [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
text [ ] 备份当前 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 日志确认无新拒绝
[ ] 通知变更窗口
[ ] 文档记录更新

