user namespace 与 subuid/subgid
一句话总结
rootless container 之所以可行,是因为创建新的 user namespace 后,可以在其中成为 UID 0。从外部看,它仍然只是普通用户。
为什么需要了解这一点
创建 container 需要执行 mount、pivot_root、创建 network namespace 等特权操作。因此,container daemon 长期以来都以 root 运行。结果,/var/run/docker.sock 实际上就等同于 root 权限本身,针对它的攻击从未停止。能够访问该 socket 的用户,只需启动一个挂载 host root filesystem 的 container,就能控制 host。
user namespace 从根本上改变了这个问题。
工作原理
映射原理
通过 unshare(CLONE_NEWUSER) 创建新的 user namespace 后,可以把自己的 UID 映射为其中可见的 0。而且在新 namespace 内会拥有全部 capability,所以能够执行 mount 或创建 network namespace 等操作。
关键在于,这些权限只在 namespace 内有效。对于外部文件,仍然只有原 UID 的权限。即使 container 逃逸,拿到的也只是普通用户权限。
为什么一个 UID 不够
container image 中存在属于多个 UID 的文件,例如 root(0)、nobody(65534)、application account(1000)等。只映射一个 UID,无法正确解包这个 image。
所以要借用一段 UID 范围。/etc/subuid 和 /etc/subgid 就是相应台账。
# /etc/subuid
podster:100000:65536
它的含义是:用户 podster 可以在自己的 namespace 中自由使用从 host UID 100000 开始的 65536 个 UID。
映射结果如下。
| Container 内 UID | Host UID |
|---|---|
| 0(root) | podster 自身的 UID(例如 1000) |
| 1 | 100000 |
| 2 | 100001 |
| 1000 | 100999 |
| 65535 | 165534 |
需要注意,container 内 UID 1 对应 host 100000。0 映射到用户自身,subuid 范围从 1 开始。因此,container 内 UID N(N≥1)对应的 host UID 是 100000 + N - 1。
之所以分配 65536 个,是因为 container image 使用的大多数 UID 都位于这个范围内。每个用户必须获得互不重叠的范围,因此通常按 100000、165536、231072……这样每次间隔 65536 进行分配。
要实际应用该范围,需要名为 newuidmap / newgidmap 的 setuid helper(uidmap package)。如果缺少这些 binary 或 file capability,范围映射会失败并回退到单 UID 模式。这种状态下 container 仍能启动,但使用多个 UID 的 image 会出现权限错误。
检查方法
grep '^podster:' /etc/subuid /etc/subgid
podman unshare cat /proc/self/uid_map
podman info --format '{{.Host.Security.Rootless}}'
podman info --format '{{.Store.GraphRoot}}'
podman unshare 会在与 podman 所使用的同一个 user namespace 中执行命令,是调试文件所有权问题的必备工具。例如,当 volume directory 的 owner 看起来异常时,使用 podman unshare ls -l <경로> 就能看到 container 视角的 owner。
Rootless 的限制
| 限制 | 原因 | 绕过方式 |
|---|---|---|
| 无法绑定低于 1024 的端口 | 特权端口 | 调整 net.ipv4.ip_unprivileged_port_start,或使用高端口 + reverse proxy |
| network 使用 userspace stack | 经由 pasta/slirp4netns | 需要超高性能时使用 rootful |
| 部分 storage driver 受限 | overlay mount 需要特权 | fuse-overlayfs 或 vfs |
| cgroup 控制受限 | 需要委派 cgroup v2 | 设置 systemd user slice delegation |
| ping 可能无法使用 | ICMP socket 权限 | 调整 ping_group_range |
实际工作中的表现
“按 docker system df 的经验寻找磁盘时感到困惑。” rootless podman 的 image 不会积累在 /var/lib/docker,而是在 ~/.local/share/containers/storage。对于 home partition 较小的服务器,单凭这一点就可能填满磁盘。因此,下一模块中的 graph root 迁移场景在实际工作中经常出现。
podman info 中 rootless 显示为 false。 要么以 root 运行,要么没有 mapping 设置而回退到 rootful。如果不确认究竟是哪种情况,可能连续几天都在困惑“为什么文件 owner 如此奇怪”。
下一项检查将看什么
后续测验会区分 user namespace 的权限范围、subuid/subgid mapping,以及 rootless
network 与 storage 限制。确认这些标准后,下一模块将为用户
podster 编写 mapping、storage.conf 与 registries.conf。