01 / 身份模型
root 仍受约束
UID 是用户的数字标识,GID 是用户组的数字标识。Linux 的 root 用户 UID 为 0。权限判断看数字身份,不只看用户名。
rootful 指 Docker 守护进程以宿主 root 身份运行。普通 rootful Docker 未启用 userns-remap 时,容器 UID 0 对应宿主 UID 0。用户命名空间(user namespace)可以改变这种对应关系,图 1 右侧就是映射后的情况。[1]
相同的 UID 不代表能访问宿主的一切。命名空间限制进程能看到什么,capability(能力)把特权操作拆成可单独授予的权限项;安全策略还会限制操作。能力将在第三节实验中用到。[3]
普通 rootful、无用户映射的容器里,id -u 显示 0。你能据此断定什么?
选第二项。没有用户映射,就不会自动换成非零 UID;但容器自己的文件视图、保留的能力和安全策略仍在生效。UID 0 与“无条件拥有全部宿主权限”之间不能画等号。
风险在于你开放了哪些入口。例如把宿主目录绑定挂载为可写,会让容器进程接触到真实的宿主文件。最小权限就是只开放应用需要的身份、能力、目录和设备。可回看共享内核与隔离边界。
深入一点:Docker Desktop 的“宿主”是哪一层?
这里的宿主指运行 Linux Docker Engine 的环境。Docker Desktop 的 Linux 容器通常运行在 Linux 虚拟机中;图中的内核属于那台虚拟机,不能直接把它等同于 macOS 或 Windows 的内核。文件共享仍会把外部文件带进可访问范围。[6]
02 / 改变应用身份
先设置非 root
USER 写入镜像的默认用户;docker run --user 为这次启动覆盖默认值。它们都不会自动创建用户或修好目录权限。[2][3]
USER 10001:10001后续 RUN 和运行时的 CMD / ENTRYPOINT 使用此身份。
--user 10002:10002本次容器使用覆盖值;镜像默认值仍是原值。
目录属主 + 权限位切换身份不会改变文件属主;可写目录要预先准备。
下面在终端新建一个练习目录,把代码保存为 Dockerfile。使用与前序页面相同的 alpine:3.21 作为练习材料,不表示生产版本推荐。以下构建与运行均未实测。
FROM alpine:3.21
RUN addgroup -g 10001 app \
&& adduser -D -u 10001 -G app app \
&& mkdir /work \
&& chown 10001:10001 /work \
&& chmod 0755 /work
WORKDIR /work
USER 10001:10001
CMD ["sh", "-c", "id; touch /work/probe; ls -ln /work/probe"]
构建阶段先创建 app 用户及用户组,再把工作目录交给它。USER 之后运行应用;touch 创建空文件,ls -ln 显示数字属主与组。[5]
预期现象(未实测):id 显示应用 UID、GID 均为 10001;文件创建成功,其数字属主和组也是 10001。这里没有贴终端输出记录,待维护者实测补齐。
预期现象(未实测):UID、GID 变成 10002,写入 /work 失败。原因是目录归 10001 所有,普通权限未给其他用户写权限;此命令预期非零退出。数字用户不必有用户名条目,但某些应用依赖用户名或家目录,需要另外准备。[3]
构建会在本地留下练习文件和镜像;运行只改容器可写层。没有绑定宿主目录、端口映射或特权参数。--network none 禁用容器对外联网;构建首次拉取基础镜像仍需联网。--rm 只删除退出后的容器。
已有镜像默认 USER 为 10001:10001。补全“仅本次以 10002:10002 运行”的命令。
docker run --rm 10002:10002 least-user-lab:local id
--user(短写 -u)覆盖这次启动的用户和组;不用重新构建镜像。
服务换成非 root 后,写数据目录时报权限错误。你应先怎么做?
文件属主不匹配不需要开放全部设备和能力。先定位具体目录。
切回 root 会扩大应用权限,掩盖目录配置问题。
先确认失败的路径与所需权限。镜像内目录可在构建时准备属主;绑定挂载还要按宿主文件权限及 UID 映射单独处理。不要对整个目录树随意开放所有人写入。
03 / 把特权拆开验证
能力按需加回
capabilities 把传统 root 特权拆开。例如 CHOWN 允许任意改变文件的用户和组属主。Docker 默认保留一部分能力;--cap-drop ALL 全部删除,再用 --cap-add CHOWN 只加回这一项。[3]
非 root 与删除能力是两道约束。UID 决定文件身份,能力决定特定特权操作是否获准。实验特意固定 --user 0:0,只改 CHOWN,让你看到 UID 0 也能因缺能力而失败。
- ① 固定身份UID 0 / GID 0
- ② 删除能力
--cap-drop ALL - ③ 创建自己的文件可在 /tmp 创建文件
- ④ 尝试改属主缺 CHOWN → 预期失败
- ⑤ 新建对照容器同一命令,只加回 CHOWN
- ⑥ 再次改属主有 CHOWN → 预期成功
- 两次都固定为 UID 0,让身份变量保持一致。
- 先删除所有能力,避免默认能力干扰。
- 在可写的 /tmp 创建自有文件。先猜:UID 0 能把它送给另一个 UID 吗?
- 预期改属主失败。UID 0 的身份没有补回 CHOWN。
- 创建新的临时容器,只加回 CHOWN。先猜:这是否也会开放全部宿主设备?
- 预期改属主成功;加回 CHOWN 没有授予其他能力或开放全部设备。
在你的终端按顺序运行以下两组。sh -c 执行引号内命令;&& 表示前一项成功才继续。stat -c %u 只显示文件属主的数字 UID。
A · 删除全部能力
docker run --rm --network none --user 0:0 --cap-drop ALL \
alpine:3.21 sh -c 'touch /tmp/cap-probe && chown 10001:10001 /tmp/cap-probe && stat -c %u /tmp/cap-probe'
预期现象(未实测):文件能创建,但 chown 报权限不足,容器命令非零退出;最后的 stat 不执行。不要把拉取失败或命令不存在当成能力实验成功。
B · 只加回 CHOWN
docker run --rm --network none --user 0:0 --cap-drop ALL \
--cap-add CHOWN alpine:3.21 \
sh -c 'touch /tmp/cap-probe && chown 10001:10001 /tmp/cap-probe && stat -c %u /tmp/cap-probe'
预期现象(未实测):chown 成功,stat 显示指定的新属主 UID 10001,命令成功退出。没有展示逐字输出;若你的环境结果不同,保留实际错误和配置再排查。
只修改临时容器内的文件属主,不挂载宿主目录,不修改宿主网络、iptables 或设备。两次退出后容器自动删除,基础镜像保留。本页没有运行任何 --privileged 命令。
深入一点:非 root 加了 --cap-add,能力就一定生效吗?
不能据参数直接断定。Linux 区分能力的有效集(当前实际生效)、允许集等集合;用户切换、可执行文件属性和运行时规则都会影响最终能力。这里固定 UID 0,避免把“更换用户”和“改变能力”混在一起。
能力也不能绕过所有安全机制。seccomp 是系统调用过滤机制;AppArmor / SELinux 是额外的访问控制策略。即使具备某项能力,操作仍可能被其他策略或命名空间边界阻止。细节见官方运行文档及其 Linux capabilities 参考链接。[3]
初始化任务只需改文件属主。补上删除全部能力后唯一需要加回的项。
--cap-drop ALL --cap-add
CHOWN 对应修改属主;Docker 也接受 CAP_CHOWN 写法。只增加它,比恢复所有能力更贴近任务需求。日常应用通常应提前准备数据目录,继续以非 root 运行。
04 / 判断例外
保留安全边界
--privileged 会授予全部能力、开放宿主设备,并放宽 AppArmor / SELinux 等隔离限制。在普通 rootful 环境,这会显著扩大影响宿主的可能性。不要把它当作权限错误的通用修复。[4]
只需要改属主
权限范围跟着单项任务走。
使用 --privileged
开放范围远大于改一个文件属主。
应用因权限失败时,先找出具体操作:目录需要写入就修目录权限;确实需要某项特权再评估单项能力;确实需要设备再评估 --device 的明确路径。不要对不明原因的错误一次性开放全部权限。
rootless 管的是 Docker 本身
rootless Docker 让守护进程和容器都在用户命名空间中,以宿主非 root 权限运行,降低守护进程或运行时漏洞的影响。容器里仍可能显示 UID 0;这不代表宿主 root。[7]
USER 10001 只改变应用默认身份,并不会把 rootful 守护进程变成 rootless。userns-remap 改容器 UID 映射,但守护进程仍是 root。三者作用层次不同,可按需求组合。rootless 的安装和兼容性边界留给官方指南,本页不改你的宿主配置。
同事说:“Dockerfile 已写 USER app,所以 Docker 已经是 rootless;写入失败加 --privileged 就行。”请指出两处问题。
USER app 只设置应用身份;是否 rootless 要看守护进程的运行方式。写入失败应先核对路径、UID/GID 与文件权限;privileged 放宽范围过大,也不会自动修正文件属主。
- 区分应用用户与守护进程权限
- 先定位具体缺失权限,不把 privileged 当通用解法
05 / 间隔检索
把权限收回来
- 解释 root 时先说明运行模式与用户映射;UID 相同不等于没有隔离。
- 用 USER 保存非 root 默认值,必要时用 --user 覆盖,并准备目录权限。
- 从 --cap-drop ALL 开始验证,只加回实际需要的能力。
- privileged 扩大权限;rootless 改守护进程权限。应用仍应遵循最小权限。
普通 rootful 容器中,UID 0 的任务在 --cap-drop ALL 后改文件属主失败。已确认是缺少 CHOWN,最小改动是什么?
privileged 同时开放全部能力和宿主设备,超出这个已定位的需求。
题目已经是 UID 0。身份不变不会恢复删除的能力。
实验把身份与能力分开了:UID 已是 0,缺的是修改属主的 CHOWN。只补这一项,保留其他限制。任务若是日常应用,应优先在准备阶段处理目录属主,再用非 root 启动。
完成标准:能给同事画清图 1 的前提,跑完两个对照实验,并用实际结果解释为何成功或失败。当前页面的 Docker 输出仍待维护者实测补齐。
来源
核对日期:2026-11-03。命令语义以 Docker 官方文档为准;本文面向 Docker Engine 29.x,不声称已在本环境运行 Docker。
- Docker Docs · Isolate containers with a user namespace:UID 映射、绑定挂载限制及守护进程仍为 root。
- Dockerfile reference · USER:默认身份对后续 RUN 与运行时 CMD / ENTRYPOINT 的作用。
- Running containers · Linux capabilities 及 User:CHOWN、ALL、用户覆盖与数字用户。
- docker container run · --privileged:设备、能力及安全策略被放宽的范围。
- Building best practices · USER:无需特权的服务使用非 root 用户。
- Docker Desktop · Linux user namespace isolation:Docker Desktop 的 Linux 虚拟机及用户映射。
- Rootless mode:守护进程与容器的非 root 运行方式、与 userns-remap 的区别。