跳到主要内容

技术教程

Linux Permission denied:文件可读,为什么服务仍然打不开

验证与风险信息

适用环境
Alpine Linux 3.20 / BusyBox 1.36.1 / Docker 24.0.7
验证耗时
约 10 分钟
风险与回滚
身份、mode、ACL 与审计日志读取为低风险;权限修改前保存 owner/group/mode/ACL,禁止无边界递归 chmod/chown
最后复核
2026-08-12
验证证据
文件 root:v2ceapp 0640 可组读,但父目录 0640 缺少执行位导致服务账号读取失败;目录改为 0750 后读取恢复且写入仍被拒绝。

Linux 服务读取配置时提示 Permission denied,管理员经常只检查目标文件,发现它是 0644 或与服务同组,就认为权限没有问题。实际上,内核解析路径时必须逐级通过每一个目录;目录缺少执行(search/traverse)权限时,即使文件本身可读,进程仍无法打开它。把整棵目录改成 777 会扩大攻击面,也会掩盖真正缺失的权限。

适用范围与结论

本文适用于 Linux 普通文件权限、POSIX ACL、systemd 服务和 SELinux/AppArmor 约束下的文件访问故障。V2CE 隔离实验使用 Alpine 3.20,创建 UID 100 的 v2ceapp 服务账号、同组配置文件和一个故意缺少目录执行位的路径。

诊断顺序应当是:

  1. 确认失败进程的有效 UID、GID 和附加组,而不是当前登录管理员的身份;
  2. 逐级检查路径每个目录的 owner、group 和 mode;
  3. 在同一服务身份下执行无副作用的读取测试;
  4. 若传统权限允许,再检查 ACL、SELinux/AppArmor、只读挂载和 systemd 沙箱;
  5. 只授予服务完成任务所需的最小读、写或遍历权限。

第一步:确认究竟是谁被拒绝

交互式 shell 中用 root 测试成功,不能证明服务账号能够访问。先从服务管理器和进程读取实际身份:

systemctl show my-app.service \
  -p User -p Group -p SupplementaryGroups -p DynamicUser
ps -o pid,user,group,egroup,comm,args -p 12345
id my-app

如果使用 DynamicUser=yes、容器 user namespace 或 Kubernetes securityContext,宿主机上看到的 UID 与容器内名称可能不同。证据中同时保存数值 UID/GID 和名称。

第二步:逐级检查路径,而不是只看文件

对完整路径使用 namei,它会列出每个组成部分:

namei -l /srv/my-app/config/app.conf
stat -c '%A %a %U:%G %n' \
  /srv /srv/my-app /srv/my-app/config /srv/my-app/config/app.conf

普通文件的 r 表示读取内容,w 表示修改内容,x 表示执行文件。目录的含义不同:

  • 目录 r:允许列出目录项名称;
  • 目录 x:允许搜索/穿过该目录并访问已知名称;
  • 目录 w:与 x 配合时允许创建、删除或重命名目录项。

访问 /srv/my-app/config/app.conf 时,进程需要对 /srv/srv/my-appconfig 都拥有目录执行权限,并需要对文件本身拥有所需权限。能看到文件名但无法 cat,正是“目录可读但不可遍历”的典型信号。

第三步:用服务身份做最小测试

对只读配置可以执行:

sudo -u my-app -- test -r /srv/my-app/config/app.conf
sudo -u my-app -- head -c 80 /srv/my-app/config/app.conf

不要为了测试而直接运行第二个生产服务进程,它可能争抢端口、锁文件或消费队列。对密钥和凭据也不要把内容写入终端或工单;使用 test -r、应用的配置检查命令或只读取非敏感实验文件。

第四步:传统 mode 正确时继续查什么

POSIX ACL

getfacl -p /srv/my-app/config /srv/my-app/config/app.conf

ACL 的 mask 会限制命名用户和组条目的有效权限。ls -l 权限位后出现 + 时尤其要检查 ACL。不要用 chmod 猜测性覆盖,因为它可能改变 ACL mask。

SELinux 与 AppArmor

ls -lZ /srv/my-app/config/app.conf
sudo ausearch -m AVC -ts recent
sudo journalctl -k --since "15 minutes ago" | grep -Ei 'apparmor|denied'

如果 DAC 权限允许但内核审计记录了拒绝,应修复标签、策略或应用路径。临时关闭 SELinux/AppArmor 只会移除保护并掩盖错误,不是生产修复。

只读挂载、不可变属性与 systemd 沙箱

findmnt -T /srv/my-app/config/app.conf -o TARGET,SOURCE,FSTYPE,OPTIONS
lsattr /srv/my-app/config/app.conf
systemctl show my-app.service \
  -p ProtectSystem -p ProtectHome -p ReadOnlyPaths -p InaccessiblePaths

这几类约束更常影响写入;错误文本可能是 Read-only file systemOperation not permitted,也可能被应用统一包装成 permission denied。保存原始系统调用或内核日志,不要只依赖应用二次翻译后的信息。

第五步:实施最小权限修复

假设服务只需读取配置,常见安全结构是目录由 root 管理、服务组可遍历和读取,其他用户无权限:

sudo chown root:my-app /srv/my-app/config
sudo chmod 0750 /srv/my-app/config
sudo chown root:my-app /srv/my-app/config/app.conf
sudo chmod 0640 /srv/my-app/config/app.conf

具体模式必须基于业务需求。若服务需要在目录内创建文件,可能需要组写权限、默认 ACL 或独立运行目录,但不应顺手开放配置目录。避免 chmod -R 777 和未确认范围的递归 chown:它们会修改密钥、可执行文件、socket 和子挂载点,回滚困难。

systemd 服务修改组成员后通常需要重启服务,使新进程获得新的附加组;仅重启 shell 不会改变已经运行进程的凭据。

复核与回滚

修复后再次以服务身份执行只读测试,然后启动单个实例并检查应用日志与健康端点。若只授予读取权限,还应验证写入仍被拒绝,证明没有扩大到不需要的权限。

变更前用 statgetfacl -p 保存 owner、group、mode 和 ACL。回滚时恢复这些精确值;不要依赖记忆。SELinux 标签变更则使用策略规定的标签和 restorecon 流程恢复。

V2CE 复现实验记录

实验文件 /srv/v2ce/config/app.confroot:v2ceapp 0640,服务账号 UID 100 属于 v2ceapp 组。文件本身具备组读权限,但父目录 /srv/v2ce/config 被设置为 root:v2ceapp 0640:组可以列出名称,却没有目录执行权限。服务账号执行 cat 返回 Permission denied,退出码为 1。

实验只把该目录改为 0750,没有修改文件模式。随后服务账号成功读到 mode=verified-lab;追加写入仍返回 Permission denied。这证明故障点是父目录遍历权限,且最小修复恢复了读取而没有授予写权限。

参考资料