Podman “Permission Denied” 谜团:为什么容器内的 Root 无法访问挂载的目录?

你是否曾经遇到过这样的场景?你兴致勃勃地用 Podman 和 Quadlet 配置了一个新的 systemd 服务,将主机上的一个目录挂载到容器中。你用 podman exec 进入容器,whoami 命令自豪地告诉你,你是无所不能的 root 用户。但当你尝试访问那个挂载的目录时,却被无情地告知:Permission denied

这简直就像是:你拿着总统套房的钥匙卡,却发现连房间门都打不开。这到底是怎么回事?

最近,我就遇到了这个令人费解的问题。这篇文章将记录我的排查过程,并揭示那个隐藏在幕后、默默守护着我们系统安全的“隐形保镖”—— SELinux。

案发现场

让我们先来重现一下“案发现场”。我正在配置一个服务,想将主机 ~/sniproxy/app 目录挂载到容器的 /app 目录。

我的 Quadlet .container 文件 (sniproxy.container) 大致如下:

[Container]
Entrypoint=/app/entry.sh
Image=docker.io/library/alpine:3.15
Pod=sniproxy.pod
Volume=/home/nite/pod/sniproxy/data/bin:/app:ro
Volume=/home/nite/pod/sniproxy/data/etc:/config:ro

[Service]
Restart=always

[Install]
WantedBy=default.target

配置好后,我运行了 systemctl --user daemon-reloadsystemctl --user start sniproxy.service,但是 sniproxy 服务启动失败了。

通过 journalctl 查看 sniproxy 的日志,发现是 Permission denied 错误。

随后我修改了 sniproxy.container 文件,将 Entrypoint 注释,Exec 设置为 sleep infinity,然后重启 sniproxy 服务。

podman exec -it systemd-sniproxy /bin/sh 进入容器执行 ls -l /app 发现报错 Permission denied

瞬间的困惑涌上心头。我是容器里的上帝(root),但却连自己的地盘(挂载的目录)都进不去?传统的 Linux 文件权限(owner, group, other)在这里似乎失效了。

揭开谜底:真正的“守门员” SELinux

在排除了常规文件权限问题后,我将目光投向了现代 Linux 发行版(如 Fedora, CentOS, RHEL)中强大的安全子系统:SELinux (Security-Enhanced Linux)

我们可以把 SELinux 想象成一个俱乐部里极其严格的保镖。

  • 传统文件权限(chmod:就像保镖检查你的身份证,看你是否成年。root 用户就像是俱乐部老板,默认可以去任何地方。
  • SELinux:这位保镖更进一步,他不仅看你的身份证,还要检查你的通行证类型(安全上下文,Security Context)。比如,你持有的是“普通观众”通行证,即使你是老板(root),也无法进入“后台员工区”。每个区域和每个人都被打上了特定的标签,只有标签匹配才能通行。

在我们的 Podman 场景中:

  1. 容器进程的标签:当 Podman 启动一个容器时,容器内的所有进程都会被赋予一个 SELinux 标签,通常是 container_t。这相当于一张“容器访客”通行证。
  2. 主机目录的标签:你在主机的家目录下创建的 ~/sniproxy/app 目录,其默认标签是 user_home_t。这相当于一个“私人住宅”区域。

SELinux 的默认策略规则非常明确地规定:持有 container_t 通行证的进程,禁止访问 user_home_t 标签的区域。

这完全是出于安全考虑。这条规则的存在,是为了防止容器内的进程(即使被攻破)逃逸出来,随意读取和破坏你在主机上的个人文件。

所以,Permission denied 的真正原因不是你权限不够,而是你“去错了地方”,被 SELinux 这位尽职尽责的保镖拦住了。

解决方案:给你的目录换一张“通行证”

既然问题出在 SELinux 标签上,我们只需要在挂载目录时,告诉 Podman 为这个目录“换一张合适的通行证”即可。这通过在卷定义的末尾添加一个标志来实现:

  • :z:将主机目录的 SELinux 标签修改为 container_file_t。这是一个共享标签,意味着多个不同的容器都可以挂载和访问这个目录。
  • :Z:为该主机目录创建一个私有的、专属于这个容器的 SELinux 标签。其他任何容器都无法访问。

对于大多数场景,:z 是最常用和最方便的选择。

修复方法

修改你的 .container 文件,在 Volume 指令的末尾加上 :z

修改前:

Volume=/home/nite/pod/sniproxy/data/bin:/app:ro
Volume=/home/nite/pod/sniproxy/data/etc:/config:ro

修改后:

Volume=/home/nite/pod/sniproxy/data/bin:/app:ro,z
Volume=/home/nite/pod/sniproxy/data/etc:/config:ro,z

重要! 修改完文件后,必须执行以下两个步骤来应用更改:

  1. 重新加载 systemd 配置

    systemctl --user daemon-reload
  2. 重启你的服务

    systemctl --user restart sniproxy.service

现在,再次 exec 进入容器,ls -l /app 终于可以正常工作了!

0%