容器权限问题通常集中在两个层面:用户身份和设备访问。容器内进程以 UID 0 运行时,默认隔离机制下它并不是宿主机 root,但一旦出现内核漏洞、错误挂载或 unsafe sysctl,权限提升风险会明显增加。反过来,强制使用非 root 身份又容易碰到挂载卷文件属主不一致、无法绑定低端口、访问 GPU 或串口设备失败等问题。要解决这个矛盾,必须理解容器内 UID 如何映射到宿主机,以及设备节点和 cgroup 规则如何共同决定进程能否操作硬件。

一、用户 ID 映射:user namespace 与 UID/GID 翻译
Linux 内核的 user namespace 允许不同命名空间拥有各自的 UID/GID 范围。容器启动时,如果没有启用 user namespace,容器内的 UID 0 就对应宿主机的 UID 0,只不过容器进程受到 Capabilities 和 cgroup 限制。这也是为什么容器内 root 仍然危险:一旦某些能力被放行或内核漏洞被利用,攻击者可能直接在宿主机上执行操作。
启用 user namespace 后,容器内的 UID 会映射到宿主机上一个偏移量。例如 Docker 配置 userns-remap 后,容器内的 UID 0 可能对应宿主机 UID 100000,容器内 UID 1 对应宿主机 UID 100001,以此类推。这种映射通过 /proc/<pid>/uid_map 文件呈现。下面的命令可以查看当前 shell 所在命名空间的映射:
cat /proc/self/uid_map # 输出示例: # 0 1000 1
输出的第一列是命名空间内起始 UID,第二列是宿主 UID,第三列是映射数量。上面示例表示容器内 UID 0 映射到宿主机 UID 1000,映射数量为 1。若没有启用 user namespace,该文件通常显示 0 0 4294967295,也就是原样映射。
Docker 的全局配置可以通过修改 daemon.json 来启用默认用户命名空间重映射:
{
"userns-remap": "default"
}
重启 Docker 后,所有新创建的容器都会在映射后的用户命名空间中运行。如果想要针对单个容器使用特定 UID/GID,可以直接在运行参数中指定,例如 docker run --user 1000:1000 ...。不过要注意,单独使用 --user 并不会创建 user namespace,它只是改变容器内进程的初始 UID,该 UID 仍然直接对应宿主机上的同名 UID。这与 userns-remap 的翻译机制有本质区别。理解这一点对排查权限问题至关重要。
二、设备访问控制:cgroup 规则与设备节点
容器内访问硬件设备并非简单地挂载 /dev 目录就能完成。Linux 使用两条路径来控制设备访问:一是文件系统上的设备节点,二是 cgroup 的设备控制器。设备节点对应主设备号和次设备号,进程通过 open、read、write 等系统调用操作设备文件。cgroup 的 devices 控制器则在内核层面对这些操作进行过滤,即使容器内有 /dev 节点,如果 cgroup 规则不允许,访问仍会被拒绝。
docker run 的 --device 参数同时做了两件事:它会在容器内创建对应设备节点,并写入一条允许该设备读写的 cgroup 规则。以访问串口为例:
docker run --device=/dev/ttyUSB0:/dev/ttyUSB0 --rm -it ubuntu bash
这表示把宿主机的 /dev/ttyUSB0 暴露给容器,容器内路径也是 /dev/ttyUSB0。运行之后,可以在容器内查看 cgroup 设备规则:
CONTAINER_ID=$(docker ps -lq) cat /sys/fs/cgroup/devices/docker/$CONTAINER_ID/devices.list
文件内容会包含类似 c 188:0 rwm 的规则,其中 c 表示字符设备,188:0 是主次设备号,rwm 表示允许读、写和创建设备节点。若只使用 -v /dev/ttyUSB0:/dev/ttyUSB0 挂载文件节点但不添加 cgroup 规则,容器内虽然能看到设备文件,实际读写仍会被 cgroup 拒绝。这也是很多人在排查设备访问失败时容易忽略的一点。
对于更精细的控制,可以关闭默认设备策略并只放行特定设备。例如在 systemd 管理的容器中,可以通过 DeviceAllow 和 DevicePolicy=closed 来定义白名单:
[Service] DevicePolicy=closed DeviceAllow=/dev/ttyUSB0 rw
如果设备类型是块设备(如磁盘分区),主设备号前缀使用 b,规则类似 b 8:1 rw。cgroup 规则的最小化可以显著降低容器逃逸后对硬件的滥用风险。
三、生产环境中的权限加固与常见故障排查
生产环境配置容器权限时,通常遵循最小权限原则:避免使用 --privileged,避免挂载宿主机 /dev 整个目录,只放行业务必需的设备;对不需要 root 的服务使用非 root UID;对需要共享宿主机文件的应用,提前规划好 UID/GID 一致性。
挂载卷属主不一致是最常见的权限故障。假设宿主机上数据目录属主为 UID 1000,而容器内进程以 UID 0 运行,则读写通常没问题;但如果为了安全改为以 UID 1000 运行,而宿主机上目录属主却是 UID 0,容器内就会报 Permission denied。解决思路有三种:一是在宿主机上执行 chown -R 1000:1000 /data;二是在镜像构建阶段就创建对应 UID 的用户,并让存储卷提前匹配;三是利用 user namespace 映射,将容器内 UID 1000 映射到宿主机真正拥有数据目录的 UID。第三种方式最灵活,但配置复杂度更高。
设备访问方面的故障也很多。例如容器内运行 USB 相关程序时,如果只添加了 --device=/dev/bus/usb/001/002 而没有添加完整的 USB 控制器节点,可能热插拔后设备号变化导致访问失效。对于 GPU 场景,除了需要 --device=/dev/nvidia0:/dev/nvidia0 外,还必须在宿主机加载 NVIDIA 内核模块并安装容器运行时钩子,否则容器内无法看到正确的设备文件或缺少运行时库。生产环境的做法是使用 NVIDIA Container Toolkit,它会自动注入必要的设备节点和驱动库挂载,同时保持 cgroup 规则最小化。
排查权限问题时,可以先确认容器内进程的实际 UID:
docker exec -it container_name id # 输出 uid=1000(nginx) gid=1000(nginx) groups=1000(nginx)
再检查宿主机的用户命名空间映射和 cgroup 设备规则。只有把这些基础信息对照清楚,才能快速定位到是文件权限问题、user namespace 映射问题还是 cgroup 设备白名单问题。容器权限不是单一的开关,而是由 namespace、Capabilities、cgroup、文件属主四层共同作用的结果。理解每层的作用边界,才能在安全加固和业务可用性之间做出合理取舍。