“为什么明明有文件却访问不了”这类权限工单,几乎是 Linux 运维里最常见、也最容易被新人低估的一个领域。它的常见表现:
Permission deniedBash: /usr/local/bin/xxx: Permission deniedRead-only file systemls 看到文件,但应用报 EACCESchmod 000 等)它的根因往往不在“chmod 一个数字 644”,而在四个层面之间的相互作用:
如果只看一层,常常查不出真正的根因。本文用一条完整的排查主线,逐步把这四层都串起来。让读者遇到类似工单时,能按照这个流程稳定收敛并定位。
下面这些场景明确在本文覆盖范围内:
cat / vi / less / cp / mv / bash / shPermission denied。EACCES、permission denied,宿主机上看着正常。avc: denied 日志。DENIED 操作。setuidcannot enable executable stack、缺少 x 权限、挂载了 noexec)。Operation not permitted不适用场景(不在本文深挖):
下面分模块讲权限相关的关键概念。每一块都是后面排查命令的依据,看命令时可以回查这些知识点。
每个文件有三组权限位: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 都做不到。
文件有 owner 与 group,分别对应一个用户 / 组。访问权限匹配逻辑:
owner 位的权限。group 位。other 位。关键点:
id 显示的全部都可能参与匹配,不仅仅是 primary。chgrpchown 改 owner 后,group 通常不变。chown user:group filechown user: file),group 会保持不变(取决于实现)。三个特殊位,影响权限匹配:
-rws------s | ||
-rwxrws---s | ||
drwxrwxrwtt |
典型场景:
/usr/bin/passwd/etc/shadow。/tmpssh-agent关键点:
nosuid 时,setuid / setgid 会被忽略。x 位时 s 显示为大写 S,表示“设了特殊位但无效”。setuid 程序(如 mount -o nosuid)。Linux 标准的 ugo 权限之外,可挂载 acl 选项后,用 getfacl / setfacl 设置更多访问控制。
例如:
# user::rw-
# user:lisi:rw-
# group::r--
# mask::rw-
# other::---
user:lisi:rw-mask关键点:
chmod 改变 mask,会发现 chmod 600 后,named user 访问也不通了,因为 mask 被锁死。setfacl -btar、rsync)要带 --acls 才能正确保留 ACL。mount -o noacl 或 -o acl。SELinux 是 LSM 框架(Linux Security Module),基于策略对每个操作做额外校验。即使 rwx 全部通过,仍然可能因为 SELinux 拒绝。
基本概念:
enforcingpermissivedisabled核心字段:
httpd_t、sshd_t、unconfined_t 等)。httpd_sys_content_t、tmp_t、default_t 等)。常见错误关键字:
avc: denied { read } for pid=1234 comm="nginx" ...audit.log 或 /var/log/messages。Permission denied,但 ls -Z 看上下文是合理的,例如 /var/www/html 被标成 admin_home_t。排错策略:
getenforceausearch -m AVC -ts recentjournalctl -t setroubleshoot 找拒绝记录。audit2why < /var/log/audit/audit.logaudit2allowsemanage fcontext + restorecon)。AppArmor 是另一种 LSM,机制与 SELinux 不同:用路径而不是 ino 安全上下文来定义策略。
排错命令:
aa-statuscat /var/log/audit/audit.log | grep -i apparmoraa-logprofapparmor_parser -R /etc/apparmor.d/<profile>挂载选项对权限影响很大:
ro | Read-only file system |
noexec | /tmp 默认) |
nosuid | |
nodev | |
relatimeatime | |
aclnoacl | |
usrquotagrpquota | |
xattr |
排查时 mount 命令直接看,再 /proc/mounts、/etc/fstab、/proc/self/mountinfo。
进程跑起来时有一组身份字段:
uidgid:进程启动时真实身份。euidegid:实际权限判断的身份(setuid 后会变)。groupscap工具:
idcat /proc/<pid>/status | grep -E '^(Uid|Gid|Groups|Cap)'sudo -u user id:以 user 身份校验。
CAP_DAC_OVERRIDE:绕过文件读 / 写 / 执行权限检查。CAP_CHOWN:绕过 chown 检查。CAP_NET_BIND_SERVICE:绑定 < 1024 端口。CAP_SYS_ADMIN:一堆管理员能力。
排查 setuid 程序或服务时,getcap /usr/bin/xxx 看授予了哪些能力。
容器内的 root(UID 0)与宿主机的 root 不一定是同一身份。常见做法:
0 ↔ 宿主机大 UID(实际安全策略下)。user_namespaces容器内文件出现在宿主机时,权限会显示成宿主机 UID。例如容器内 chmod 666 /data/file,宿主机看到的是宿主机 UID chmod 666。
排查容器文件权限时:
docker exec -u <uid> <container> ls -la /pathUSER 切了普通用户read_only: true / tmpfs 挂载按从最外层到最内层的顺序排查:
ro / nosuid / noexec / acl)。strace 取内核证据。具体执行每一步时都有明确判断逻辑,进步骤章节会展开。
下面从“现场拿到一个报错”开始,每一步给出命令、判断、动作。
目的:把问题描述压到一句最小可复现的句子: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'
预期输出:得到报错原文(错误信息、错误码、关联错误号)。
异常表现:
下一步动作:进入步骤 1。
目的:用最少命令看清 owner、group、permission、type、modification、size。
命令:
bash ls -la <path>
stat <path>
file <path> # 文本 / 二进制 / 软链 / 块设备
判断逻辑:
root,用户不是 root:other 位决定。ls -la 看链指哪里,stat 看的是目标文件。mountPermission denied:考虑 ACL、mask、SELinux。nodev 阻止。下一步动作:进入步骤 2。
目的:避免被 ls 显示迷惑(比如误把大写的 S 当 s),一定要看准 setuid / setgid / sticky。
命令:
bash stat -c '%a %A %n' <path>
# %a 八进制权限
# %A 字符权限(含特殊位)
判断逻辑:
0777 -rwxrwxrwx4755 -rwsr-xr-xS:特殊位有但 x 没设,特意去 chmod u+x 才会让 s 有效。drwxrwxrwt:是 sticky,目录里只有 owner 能删自己的文件。下一步动作:进入步骤 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>。下一步动作:进入步骤 4。
目的:标准 ugo 权限之外,ACL 与 xattr 会影响访问。
命令:
bash getfacl /path/to/file
getfattr -d /path/to/file
# 看看 mask 是否锁住了有效权限
getfacl /path/to/file | grep -E '^# (effective|mask)'
判断逻辑:
setfacl -m u:user:rwchmod 600,会把 mask 同步成 600,原来的 named entry 实际可用权限变成最小值。noaclsetfacl 会报“Operation not supported”,getfacl 只显示 classic ugo。下一步动作:进入步骤 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#!/bin/bash 第一行也无法直接 #!)。/tmp 是 nosuid,nodev,noexec:在 /tmp 里放脚本并 chmod +x 不能直接执行。mount_namespaces 影响,容器场景下用 cat /proc/1/mountinfo 而不是宿主。下一步动作:进入步骤 6。
目的:确认问题不是 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 后才会影响业务。admin_home_t、tmp_t 等非预期值:context 不对,进程 domain 没权限读。下一步动作:进入步骤 7。
目的:Ubuntu / 部分发行版默认开启 AppArmor。
命令:
bash aa-status
cat /var/log/audit/audit.log | grep 'apparmor.*DENIED'
判断逻辑:
DENIEDenforce 模式:它就是被拒的原因。complain:仅记录不阻止,可以切到 enforce 后看是否影响。下一步动作:进入步骤 8。
目的:所有上层检查都无明确结论时,用 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)openat(...) = -1 EROFS (Read-only file system)openat(...) = -1 EACCES ...audit.log 有 AVC:SELinux 拦截,strace 也会标注 SELinux 相关 errno。stat(...) = -1 EACCESx 位,进程甚至没法 stat 子条目。下一步动作:进入步骤 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
判断逻辑:
auid= 是登录用户 ID,便于做用户画像。uid= 是执行进程的实际 UID。path=... 与 objtype=...(如 file、socket),确认拒绝对象。下一步动作:进入步骤 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> bashPasswd: 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 给出修复 + 验证。
走到这一步通常已经定位到根因,根据不同根因有不同的修复动作。重点是“只改一个变量再验证”,不要一次性把多种修复混合。常见修复动作:
chmod 644 /path/to/filechown user:group /path/to/fileusermod -aG group usersetfacl -m u:user:r /path/to/filemount -o remount,rw /mount_pointrestorecon -Rv /path/to/dirsetenforce 0aa-complain /usr/sbin/nginx每修一次都重复步骤 11 的验证:复现报错动作,确认不再是 Permission denied。
风险提醒:
chmod -R 777setenforce 0setfacl -R -brestorecon按用途归档常用命令。
bash # 看权限
ls -la
stat
stat -c '%a %A %n'
# 看 owner
id
getent passwd user
getent group group
# 整目录递归
ls -laR
tree -pug
bash chmod 644 file
chmod u=rw,g=r,o= file
chown user:group file
chown -R user:group dir
chgrp group file
bash chmod u+s file # setuid
chmod g+s file # setgid
chmod +t dir# sticky
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
bash getcap /usr/bin/xxx
setcap cap_net_bind_service=+ep /usr/bin/xxx
getpcaps <pid>
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
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
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
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
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
下面给几组典型场景的配置示例。
文件:/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)。
文件:/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 默认不支持。
文件:自定义 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 把业务相关的都纳入同一模块。
文件:/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
文件:/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
文件:/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
文件:/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
权限问题不像性能问题,没法靠单一指标直接收敛到根因,仍以日志为主。
bash journalctl -t kernel --since '15 minutes ago' | tail -100
journalctl -t setroubleshoot --since '15 minutes ago' | tail -50
dmesg -T | tail -50
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
bash grep -E 'apparmor.*DENIED' /var/log/audit/audit.log | tail -50
dmesg | grep -i 'apparmor'
bash journalctl -u sshd --since '15 minutes ago'
grep -E 'Failed password' /var/log/secure
last -f /var/log/btmp | head
业务侧日志会直接给出关键字:
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)把报错进程的 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
如果要接 audit 日志到告警:
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 777setenforce 0 + setenforce 1 之后没复验restorecon -Rv /semanage fcontext -l 看现状。setfacl -b -R dirusermod -aG group user-a 不要漏,否则会替换用户的 supplementary group 列表,把生效的组清理掉。mount -o remount,rw /ro 挂载是因为一致性或 snapshot 原因,强行 remount 会带来数据不一致风险。apt remove apparmor / 升级后 policy 默认 denychown -R 7777 这种 UIDauditctl -a never,taskdocker exec 改文件docker exec 编辑的文件改动可能不会持久(取决于 bind-mount)。改完要确认。不论做什么变更,验证要给出“通过 / 失败”的标准,避免“我感觉好了”。
bash sudo -u <user> bash -lc 'cmd'
# 预期:不再有 Permission denied
bash ausearch -m AVC -ts recent
aa-status
journalctl -t setroubleshoot --since '1 hour ago'
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>
确认是按预期变化,而不是其他副作用。
bash strace -e trace=openat -p <pid> 2>&1 | grep -E 'EACCES|EPERM'
如果没有任何拒绝,且打开路径与预期一致,验证通过。
如果要长期避免,回退前至少有一条端到端测试:
bash sudo -u app bash -lc 'cd /var/app && ./run-stest'
准备每个修复动作对应的回滚。
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 要先腾出元数据
实践更稳的做法:把权限位打到一个文件里,恢复时按文件遍历回设。
bash setenforce 1
restorecon -Rv /path/to/dir
# 或:
semodule -r mynginx
bash apparmor_parser -R /etc/apparmor.d/usr.sbin.nginx
bash gpasswd -d user group
# 或
usermod -G <known-good-list> user
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
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 递归,但小心范围。acl、vers/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=0007 等显式指定。/etc/passwd/etc/group/etc/shadow 用统一配置管理(Ansible / Salt),不要散乱。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=<...> 看是否正确。把这次主题的关键点归纳成几句:
ls -la 看着对,未必真能访问chmod → 测 → 测下一个;不要一次性多个变量一起改。把这一套路写在团队 wiki 上,配合 auditd watch 关键文件、SELinux/AppArmor 状态监控,能把权限类工单从散乱的“凭直觉”变成“流程化处置”。
权限问题往往伴随系统行为边界,下面几个 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 = 1kernel.yama.protected_hardlinks = 1这些不影响直接权限判断,但影响业务场景里“应用跟随软链执行”类逻辑。
/proc/self/uid_map 是 user namespace 的映射关系,做容器时常常要修改:
bash echo"0 100000 65536" > /proc/<pid>/uid_map
权限问题在多进程协同(systemd + 容器 + 主机)下,需要看清进程跑的 namespace:
bash # 进程所在的 namespace
ls -la /proc/<pid>/ns
# 进入指定进程的 namespace
nsenter -t <pid> -m -u -i -n -p -- /bin/bash
nsenter 是处理容器内外权限差异的关键工具,能让运维临时获得容器视角的 id / mount。
下面是我和生产团队这几年的真实工单,每条都按闭环组织。
Permission denied while reading upstreamPermission denied 在和 upstream 通信之后,紧接着 502。多数人会猜 SELinux。少数情况是反向代理接口落到了本地 socket。
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
/var/run/upstream.sockroot:root 666httpd_tunconfined_t 或 initrc_tSELinux policy 不允许 httpd_t 访问 unconfined_t 的 socket。audit2why 输出大致类似 allow httpd_t unconfined_t:unix_dgram_socket read/write;,说明需要给 httpd_t 一个允许规则。
bash semanage fcontext -a -t 'httpd_sys_rw_t' /var/run/upstream.sock
restorecon -v /var/run/upstream.sock
# 或者改上游 socket 路径到 nginx 默认允许的 /var/cache/nginx/...
bash sudo -u nginx -- bash -lc 'cat /var/run/upstream.sock && echo ok' || true
curl --unix-socket /var/run/upstream.sock http://localhost/healthz
bash semanage fcontext -d /var/run/upstream.sock
restorecon -v /var/run/upstream.sock
要在自己服务里建通用 socket 路径时,要确认 SELinux 是否允许当前进程 domain 反代。
touch: cannot touch '/data/flag': Permission denied/data/flag,提示 Permission denied。/data 目录是 777。bash docker exec <container> ls -la /data
docker exec <container> id
docker exec <container> cat /proc/1/status | grep -E '^(Uid|Gid)'
/data owner 是 1000。UID 不对应,容器内进程不能写宿主机 1000 用户拥有的目录。
docker-compose.yml 里加入 user: "1000:1000",或在 docker run 用 --user 1000:1000。
bash docker exec -u 1000 <container> bash -lc 'touch /data/flag && echo ok'
docker run --user 0 ... 临时恢复回 root,但不要久用。
bash: ./run.sh: Permission denied#!/bin/bash,但报 Permission denied。bash ls -la run.sh
mount | grep $(pwd)
file run.sh
x)noexec通常两者之一:
chmod +x run.shbash run.shchmod +x run.sh,如果挂在 noexec,编辑 fstab 去掉。
bash ./run.sh --ok-test
不重要,必要时 chmod 回原来权限。
sudo 提示 unable to resolve hostsudounable to resolve host xxx。bash cat /etc/hosts
hostname
cat /etc/resolv.conf
/etc/hostsbash echo"127.0.0.1 $(hostname)" >> /etc/hosts
sudo id 不再报错。
git pull 在 NFS 卷上 Permission deniednoacl。bash mount | grep <nfs-mount>
getfacl <file>
lsattr <file>
setfacl 又在很多机器上默认启用了。vers=4.1,acl。下面是一些安全的命令,适合贴工位上。
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'
下面是我自己常用的 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
semodule -l 可以看到已装模块,挑三个常见的:
selinux-policysemanagepolicycoreutils-python-utilsaudit2allow、sealert。bash # 列出 enabled 模块
semodule -l | head -20
# 看模块细节
semodule --info=selinux-policy
# 装载自定义模块
semodule -i mynginx.pp
# 移除
semodule -r mynginx
/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 生成。
下面几个工具是我日常会用到的:
setprivsu 轻量。runusersu 类似,用于强制以指定 uid 启动脚本。getcap / setcapnsenterchroot / bwrapsmack 系列efivars 相关注意:任何卸载 LSM 的操作都建议先在 dev 环境模拟,避免在生产做不可逆变更。
线上多机环境里,同一台主机有时会因为 /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 监控关键文件变更也很有用。
如果维护多套 /etc/passwd、/etc/group 已经非常痛苦,可以把账号统一到 LDAP / FreeIPA,并把客户端切到 sssd + nslcd。
sssdnslcd部署时注意:
getent passwd <username>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
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
清理日志是有风险的:
truncate /var/log/xxx.log> /var/log/xxx.log,并配合日志服务(如 rsyslog)reopen。rm /var/log/xxx.loglogrotate 才是标准化方式:postrotate 触发 kill -HUP。权限经常被忽略:
/var/log/adm 组可读,如 /var/log/auth.log。文件 socket 是一种特殊的文件,权限要按 socket 本身的 mode 来设置,客户端启动时用 unix socket 路径连。
ls -la /var/lib/mysql/mysql.sock
# srwxrwxr-x 1 mysql mysql 0 ... mysql.sock
如果有其他用户要连(一般是错的),可调 mode,但不要 777。
unixsocket /tmp/redis.sock 和 unixsocketperm 770。/var/spool/postfix/private/*要让一个非 root 用户绑定 80 端口:
bash setcap cap_net_bind_service=+ep /usr/bin/python3.11
sudo -u app python3.11 -m http.server 80
bash capsh --decode=<HEX_MASK>
# 或
getpcaps <pid>
# 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
容器内权限可用下面 5 个维度评估:
查 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权限问题不复杂但很碎。最好用的工具依然是 流程化 + 最小变更:
chmod 777,先查再改。/var/backup/perm.YYYYMMDD-HHMMSS 快照。permissive / complain 而不是直接关闭。rm、setfacl -b -R、整卷 chown -R 永远要二次确认。按上面这个节奏,工单处理时间通常从“折腾两小时”降到“5 分钟内找到根因”。
下面 6 个坑是写权限相关脚本时最容易踩的,做一些通用 case:
bash BACKUP="/var/log/my app.log"
ls -la $BACKUP
# 实际执行的是:ls -la /var/log/my app.log
# 正确:ls -la "$BACKUP"
解决:所有路径变量都 "$BACKUP"。
bash cd"$DIR"
# 注意:cd 之后如果 DIR 不存在,下面的命令行 $PWD 不会更新,除非重新读。
解决:每条命令检查 cd $DIR && ...。
bash find /var/log -type f -name "*.log" -mtime +7 -delete
# 高风险操作
解决:加 -print 或 -ok。
-Xbash chmod -R u=rwX,go=rX .
# X = 只给可执行文件 / 目录加成可执行
# 不加 X 会导致普通文件变成 rw,但非文件不期望;
解决:明确 -X 或 -x。
bash mount /dev/sda1 /mnt
# 挂在失败,但 echo 没输出,下面的程序以为成功了
解决:用 set -euo pipefail 或每条命令检查 $?。
bash # CentOS
ls /etc/sysconfig/iptables
# Ubuntu
ls /etc/iptables/rules.v4
解决:判断 /etc/os-release,脚本里区分发行版。
setuid + nosuidbash mount -o remount,suid /tmp
# 临时解除 nosuid,或编辑 /etc/fstab
read-only 上写bash # 重 mount
mount -o remount,rw /path
# 然后看 /proc/mounts 是否生效
bash # 容器里 setcap 不会被保留
# K8s 里,做法:
# securityContext.capabilities.add: [NET_BIND_SERVICE]
# securityContext.capabilities.drop: ["*"]
bash docker run --security-opt apparmor=my-profile ...
redis 不能 bind 0.0.0.0bind 127.0.0.1 ::1 或 protected-mode yes 时,仅允许本地。生产开 bind 0.0.0.0 要把 protected-mode no,并且防火墙开放。注意:bind 接 IPv6 前缀 - 的写法仅在部分版本支持,建议直接 bind 0.0.0.0。
权限类故障在业务指标里体现不像性能问题那样直观,但可以做以下监控:
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.logDocumentation/admin-guide/cgroups-v2.rstDocumentation/admin-guide/LSM/systemd.execLimitNOFILE、User、Group、Capabilities、NoNewPrivileges 等配置项http://tldp.orgman 即可为了让读者练习,列 5 个 mini 工单出来:
db.service 无法访问 /var/lib/mysql/datadrwxrwx--T 4 root web 4096 /var/www/uploads/var/www/uploads/x.png 吗?/opt/app/run.sh 有 x,但运行 bash: /opt/app/run.sh: No such file or directory#!/bin/bash 指向不存在的解释器,或 #! 后必须紧跟解释器路径。EACCES 在容器内,但 host 上正常my-cron 服务,通过 systemd 部署且以非 root 运行crond 而不是自写。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]
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 日志确认无新拒绝
[ ] 通知变更窗口
[ ] 文档记录更新
文末阅读福利






