多容器共享目录权限踩坑记

背景

项目中有算法服务和后端服务,后端服务负责文件上传与保存,算法服务负责文件内容解析、报告生成与保存;采用docker-compose方式部署,

两个服务都会读写同一目录 /shared(挂载自宿主机 /data/project/share-data)。

上线后发现:后端服务无法成功上传文件,算法服务无法将生成的报告写入目录,服务日志提示权限不足的错误,本文记录排查过程,并给出共享组+SGID+umask的解决方案。

正文

问题现象与环境

权限不足的错误

bash 复制代码
PermissionError: [Errno 13] Permission denied: '/shared/uploads'

docker-compose.yml 挂载配置

bash 复制代码
# ---- 后端服务 ----
  backend-app:
    volumes:
      - /data/project/share-data:/shared
# ---- 算法服务 ----
  ai-app:
    volumes:
       - /data/project/share-data:/shared

Dockerfile

bash 复制代码
# ---- 算法服务 ----
# 创建了 UID 为 10001、名为 appuser 的用户
RUN useradd ... --uid 10001 appuser 
# 指定后续指令及容器运行时以此用户身份执行
USER appuser
# ---- 后端服务 ----
# 创建了 appuser 的用户和用户组(useradd -r appuser 的 uid是系统分配的,建议使用固定 uid,防止镜像重建时 uid 变更)
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
# 指定后续指令及容器运行时以此用户身份执行
USER appuser

排查过程

本着 "尽量改配置、不动两个服务 Dockerfile" 的原则,开始了排查。以下内容均排除 root 场景。

第 1 步:查看宿主机的共享目录的权限。

bash 复制代码
$ ls -l /data/project/
drwxrwxr-x 2 ubuntu ubuntu 4096  9月 14 16:21 share-data
$ id ubuntu
uid=1000(ubuntu) gid=1000(ubuntu)

目录的属主和属组都是 ubuntu, 权限 775 (属主 rwx,属组 rwx,其他人 r-x)

第 2 步:对比两个容器内的用户身份。

bash 复制代码
# 后端服务容器内
$ id
uid=999(appuser) gid=999(appuser) groups=999(appuser)

# 算法服务容器内
$ id
uid=10001(appuser) gid=10001(appuser) groups=10001(appuser)

# 宿主机确认目录属主属组的数字 id
$ id ubuntu
uid=1000(ubuntu) gid=1000(ubuntu)

两个容器内的进程 UID 不同,且都不是 root,也不属于 共享目录的属组gid(1000)。

第 3 步:定位根因。

两个容器里的用户 id 都不等于 共享目录的属主 uid(1000) ,也不属于共享目录的属组 gid(1000),作为共享目录的其他用户,其他用户没有 写(w)的权限,而在目录里新建文件 需要目录的 w+x 权限,两个容器都不满足权限需求,所以提示Permission denied。

根因分析: Linux 是怎么判断文件或目录的读写权限的?

前提:先通过路径解析

访问一个文件,内核先逐级解析路径,每一级目录都需要 x 权限:

powershell 复制代码
访问 /data/project/share-data/a.txt
  ├─ /           需要 x
  ├─ /data       需要 x
  ├─ /project 需要 x
  ├─ /share-data 需要 x
  └─ /a.txt      需要看文件权限

任何一级目录缺 x,路径解析失败,直接拒绝。

第 1 步:判断进程的 UID 是否是 root

bash 复制代码
if (进程 UID == 0) {
    // root 跳过权限检查(除少数特殊情况,如执行位;实际情况还受其他条件的影响)
    允许
}

注意: root 对文件读写通常无条件允许,但对执行仍需至少一个 x 位。

第 2 步:比较属主权限位(属主位:只有文件属主本人能用)

bash 复制代码
if (进程有效 UID == 文件属主 UID) {
    使用文件的"属主权限位" # 如果属主权限位不满足读写条件,则拒绝
    跳到第 5 步
}

第 3 步:比较属组权限位 (属组位:文件属组的所有成员都能用)

bash 复制代码
if (进程组列表(主组 + 附加组)包含 文件属组 GID) {
    使用文件的"属组权限位"  # 如果属组权限位不满足读写条件,则拒绝
    跳到第 5 步
}

组位是 rw- 还是 r--,决定能不能写

关键:组列表是主组 + 附加组的并集,只要包含文件属组 GID 就算匹配。

第 4 步:比较其他人权限位

bash 复制代码
否则 {
    使用文件的"其他人权限位" # 如果其他权限位不满足读写条件,则拒绝
}

第 5 步:根据操作类型判断权限位

拿到对应的权限位(属主/属组/其他人之一)后,看具体操作:

操作 需要的权限位
读文件内容(cat、read ) r
写文件内容(echo >>、write) w
执行文件(./run) x
列出目录(ls) 目录的 r
进入目录(cd) 目录的 x
在目录里建/删/重命名 目录的 w + x

只要需要的位在对应的权限段里有,就允许;否则拒绝。

注意:linux内核在容器内外不看用户名,只看 uid/gid 数字。999 ≠ 10001 ≠ 1000。

方案评估与选择

方案 做法 优点 缺点 结论
1.统一 uid / gid 两个容器用相同 uid/gid 运行,对齐目录属主/属组;或修改目录属主/属组与容器 uid/gid 对齐 简单直接 两个容器服务都要改 Dockerfile / docker-compose / 启动参数,还可能影响容器内其他文件权限 放弃
2.共享组(SGID + 组可写) 建共享组,目录属组设为共享组,容器进程加入附加组,并设置 SGID 与组可写权限 不动 Dockerfile,只加配置 需要调整 docker-compose 启动配置(group_add、umask) 采用
3.ACL setfacl 分别给 999、10001 授权 只针对 uid 授权,无需改属组和 umask 容器 uid 变化时需同步调整 ACL 命令 备选
4.放开 other 写权限 chmod o+w 目录,或 chmod -R 777 /data/project/share-data 只改一句,其他都不用动 a. 安全风险较大;b. 新文件的 other 位仍受 umask 限制为 r--,单独用解决不了互写 放弃
5.容器以 root 运行 两个服务都用 root 用户运行 简单省事 安全风险极大 放弃

综合评估,选择了方案二,只加配置不动镜像

共享组(SGID + 组可写)方案落地

宿主机侧:

bash 复制代码
# 0. 关键:目录必须先建好再启动容器,否则 Docker 会建一个 root:root 的目录(当前情况下已有该目录)
sudo mkdir -p /data/project/share-data

# 1. 建共享组,固定 GID 
groupadd -g 1001 fileshare

# 2. 共享目录属组改为 1001
chown 1000:fileshare /data/project/share-data   # 1000 可替换为当前登录的系统用户的id

# 3. 【易漏,容易踩坑点】历史文件和目录属组一并迁移:SGID 只对"新建"文件生效
chgrp -R fileshare /data/project/share-data
# 历史文件补组写位(保留 x 和其他位):umask只对"新建"文件生效 
find /data/project/share-data -type f -exec chmod g+w {} + 
# 历史子目录补「组写 + SGID 位」:
find /data/project/share-data -type d -exec chmod g+w,g+s {} + 

# 4. 目录设 SGID + 组的读写执行权限 rwx: 2775 = 2(setgid) + 775
chmod 2775 /data/project/share-data

# 5. 宿主机侧用户放文件时也要保留组写位(umask 022 会导致新文件为 644)(看具体情况设置)
echo "umask 002" >> ~/.profile      # 或每次放完后:sudo chmod -R g+w /data/project/share-data

# 6. 确认结果(-n 显示数字 uid/gid,避免容器内组名解析不出来)
ls -ldn /data/project/share-data     # 期望:drwxrwsr-x 1000 1001

容器侧(docker-compose.yml) 调整 group_add 和 umask:

bash 复制代码
# 后端服务加入共享组,(group-add 只增加进程的附加组身份,不会自动改变目录属组,也不会自动让新建文件拥有组写权限。)
backend-app:
  group_add:
    - "1001"
  # 设置 umask 0002,使新建文件属组可写
  command: sh -c "umask 0002 && exec python app.py"
    
# 算法服务加入共享组
ai-app:
  group_add:
    - "1001"
  # 设置 umask 0002,使新建文件属组可写
  command: sh -c "umask 0002 && exec python algorithm.py"

容器侧(entrypoint/command) 这里和上述 docker-compose command 的配置选其一即可:

bash 复制代码
umask 0002   # 必须在 exec 之前设置
exec "$@"    # exec 保持信号传递与 PID 1 语义

umask 0002 的作用:新文件默认权限 666 & ~002 = 664,保留组写位;新目录 777 & ~002 = 775。

结果验证

bash 复制代码
目录: drwxrwsr-x  1000 1001   /data/project/share-data ,属组权限 rws 出现了 s说明 sgid生效
文件: -rw-rw-r--  999  1001  a.txt   ←  后端服务 (uid 999) 建的文件
文件: -rw-rw-r--  10001 1001  b.txt   ←  算法服务 (uid 10001)建的文件

在两个容器的 UID(文件所有者)无法统一的情况下,通过 共享组(gid)的方式,使两个容器进程拥有了公共的身份,设置SGID 目录让目录下的新文件继承目录的属组(说明:新建的子目录不仅继承属组,还会继承 SGID 位,所以共享目录的子层级自动继续生效;普通文件只继承属组,不继承 SGID 位。),设置 umask 让新文件组的权限位有了写的权限,以上共同作用两个容器才都能写入共享目录,操作其中的文件。

步骤 作用对象 机制 解决 缺失后果
共享组 进程(用户本质上是进程)+目录 group_add + chgrp 进程有 1001 组身份,目录属组 1001 组权限不生效
目录 rwx 目录 chmod 775 的组位 能在目录里建/删文件、进入目录 进不了目录或建不了文件
目录 SGID 目录 chmod g+s 新文件属组 = 1001 新文件属组 = 创建者主组,对方写不了
umask 002 进程 umask 0002 新文件组位有 w 新文件组位 r--,对方只能读

最容易踩坑的几个点

  1. 删除、新建、重命名文件看父目录权限
    删除、新建、重命名文件需要父目录的 w+x 权限,不需要文件本身的权限,新建的文件的权限由基本权限和 umask 决定。
  2. SGID 继承组,不继承完整权限
  • 目录设置 SGID(chmod g+s 或 2775)后,在该目录下新建的文件/子目录会继承该目录的所属组,而不是创建者的主组。但它不会继承目录的完整权限位。
  • 目录里的文件权限还是由新建文件时的默认权限和 umask 综合设置的,组是组,权限是权限,权限是对文件或目录设置的。
  1. umask和 SGID 只影响新建对象
    umask 和SGID只在创建文件/目录时参与计算默认权限,对已有对象没有任何作用,所以当前问题需要对已有的目录/文件批量补一次。
  2. 目录必须有 x 权限
  • 目录的 x 位表示"能否进入/穿越该目录",是访问目录内任何对象的前提。所以目录新建时的默认权限保留 x,基准权限为 777,实际默认权限由 umask 决定。
  • 只给 r 不给 x:ls dir 可能显示名字,但 ls -l dir、cat dir/file 会失败。
bash 复制代码
$ sudo chmod 444 /data/demo/test
$ ls -al test
ls: 无法访问 'test/1.txt': 权限不够
ls: 无法访问 'test/2.txt': 权限不够
ls: 无法访问 'test/..': 权限不够
ls: 无法访问 'test/.': 权限不够
总计 0
d????????? ? ? ? ?             ? .
d????????? ? ? ? ?             ? ..
-????????? ? ? ? ?             ? 1.txt
-????????? ? ? ? ?             ? 2.txt
$ cat test/1.txt
cat: test/1.txt: 权限不够
  1. 文件和目录的 rwx 含义不同
位 对文件 对目录
r 读取文件内容 列出目录中的条目名
w 修改文件内容 在目录中创建/删除/重命名条目
x 执行文件 进入/穿越目录,访问目录内对象
  1. echo "hello" > /shared/new.txt 指令中权限如何判断?是文件还是目录的权限?
    执行上述一条命令,写了个新文件,但实际分为两个独立动作,先创建文件,再写文件内容

动作 1:在目录 /shared 里创建一个新目录项 new.txt

→ 权限判断对象:目录 /shared,检查目录有 w+x 权限,才允许创建

动作 2:往 new.txt 这个文件里写入内容 "hello"

→ 权限判断对象:文件 new.txt,检查文件有 w权限,才允许写入

  1. 组的作用对象是谁?
  • 作用对象:用户、目录或文件、进程 ;
  • 通过为用户设置共享组、目录或文件设置共享组,容器内的进程加入共享组,才让容器里的用户或进程操作共享目录,共享的是 gid,容器里的属组靠识别数字匹配生效。

排错速查清单

bash 复制代码
# 1. 进程到底是谁 ------ 容器内执行
id

# 2. 目录到底归谁 ------ 用 -n 看数字,容器内组名常常解析不出来
ls -ldn /data/project/share-data

# 3. 路径每一级的权限 ------ 逐级列出,定位"卡在哪一层"
namei -l /data/project/share-data/uploads/a.txt

# 4. 有没有 ACL 覆盖 ------ 传统 rwx 看着对,但可能被 ACL 拦下
getfacl /data/project/share-data

# 5. 挂载选项 ------ 只读挂载 / nosuid 会让一切配置失效
mount | grep share-data

总结

本次的排查过程也是逐渐发现和学习的过程,当前本次的解决方案也是建立在一些前提下的,如果 docker engine 开启了 userns-remap,或者目录位于 NFS 或者设置了只读挂载,本次共享组的方案是不生效的,本文中的一些内容未详细说明,包括linux的权限模型、ACL设置、方案不生效的原因等还需要探究,下篇博客将为您继续解答,敬请期待!

相关推荐
Android系统攻城狮4 小时前
Linux Gstreamer深度解析之gst_audio_encoder_set_frame_max调用流程与实战(六十一)
android·linux·运维·音视频·gstreamer音视频·音视频进阶·gstreamer音视频进阶
H.莓飛5 小时前
【C++】命名空间、缺省参数、函数重载与引用
linux·开发语言·c++·visual studio
91刘仁德5 小时前
IP协议详解:从IP协议头到网段划分、路由与NAT
linux·服务器·网络·网络协议·tcp/ip
硅基手札6 小时前
【Linux内核专栏 14】网络协议栈
linux·运维·网络协议
沫璃染墨7 小时前
《Linux工程实践篇(一):认识设计模式——从日志系统看策略模式》
linux·c++·安全·设计模式·策略模式
工作10年+,存储芯片行业8 小时前
Linux NVMe 中断排查与性能优化:CPU 亲和性
linux·运维·服务器·windows·性能优化·ssd·pcie
wuminyu8 小时前
ForkJoinPool内部WorkQueue的Lock-Free数组操作以及并发任务窃取原理剖析
java·linux·c语言·jvm·c++
她说彩礼65万8 小时前
C语言 堆区和栈区
java·linux·c语言
一号弯9 小时前
装完LINUX,请先新建日常用户
linux·运维·服务器