← 全部笔记

Docker · 8 安全与生产实践

不用 root 运行容器;
只保留必需的能力

让应用以非 root 身份运行,再删除不需要的内核能力,缩小程序被攻破后的影响范围。

约 15 分钟 · 截至 2026-11-03,Docker Engine 29.x · 实验未实测 · Linux 容器

普通 rootful · 未启用用户映射

无用户映射时,容器 root 对应宿主 UID 0 虚线容器边界内的进程以 UID 0 发起系统调用。同一个宿主内核看到 UID 0,但进程仍受命名空间、能力和安全策略约束。 容器 · 隔离的进程与文件视图 应用进程:UID 0(root) 按 UID 0 检查 请求内核操作 宿主 Linux 内核 · 仍检查隔离策略

userns-remap · 启用用户映射

用户映射把容器 root 映射为宿主非 root 身份 容器内依然显示 UID 0,宿主内核按映射后的非零 UID 检查。此图只表示映射关系,不指定实际 UID 数值。 容器 · 用户命名空间 应用进程:UID 0(root) 映射为非零 UID 请求内核操作 宿主 Linux 内核 · 按映射身份检查
图 1 容器里的 root 只是进程身份;宿主看到哪个 UID,取决于是否启用了用户映射。[1]
学完你能做什么

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

本次容器使用覆盖值;镜像默认值仍是原值。

内核再检查文件权限目录属主 + 权限位

切换身份不会改变文件属主;可写目录要预先准备。

图 2 身份选择先看启动覆盖值,再看镜像默认值;文件权限独立检查。

下面在终端新建一个练习目录,把代码保存为 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]

docker build -t least-user-lab:local .
docker run --rm --network none --cap-drop ALL least-user-lab:local

预期现象(未实测):id 显示应用 UID、GID 均为 10001;文件创建成功,其数字属主和组也是 10001。这里没有贴终端输出记录,待维护者实测补齐。

docker run --rm --network none --cap-drop ALL --user 10002:10002 least-user-lab:local sh -c 'id; touch /work/override-probe'

预期现象(未实测):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 也能因缺能力而失败。

  1. ① 固定身份UID 0 / GID 0
  2. ② 删除能力--cap-drop ALL
  3. ③ 创建自己的文件可在 /tmp 创建文件
  4. ④ 尝试改属主缺 CHOWN → 预期失败
  5. ⑤ 新建对照容器同一命令,只加回 CHOWN
  6. ⑥ 再次改属主有 CHOWN → 预期成功
  1. 两次都固定为 UID 0,让身份变量保持一致。
  2. 先删除所有能力,避免默认能力干扰。
  3. 在可写的 /tmp 创建自有文件。先猜:UID 0 能把它送给另一个 UID 吗?
  4. 预期改属主失败。UID 0 的身份没有补回 CHOWN。
  5. 创建新的临时容器,只加回 CHOWN。先猜:这是否也会开放全部宿主设备?
  6. 预期改属主成功;加回 CHOWN 没有授予其他能力或开放全部设备。
图 3 在身份、路径和操作相同的条件下,只改变 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]

只需要改属主

能力 → 仅 CHOWN
设备 → 保持原有访问限制
安全策略 → 继续参与检查

权限范围跟着单项任务走。

使用 --privileged

能力 → 全部开放
设备 → 开放宿主设备
安全策略 → 多项限制被放宽

开放范围远大于改一个文件属主。

图 4 普通 rootful 环境中,privileged 同时放宽多层限制,远超单独增加 CHOWN。[4]

应用因权限失败时,先找出具体操作:目录需要写入就修目录权限;确实需要某项特权再评估单项能力;确实需要设备再评估 --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 / 间隔检索

把权限收回来

普通 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。

  1. Docker Docs · Isolate containers with a user namespace:UID 映射、绑定挂载限制及守护进程仍为 root。
  2. Dockerfile reference · USER:默认身份对后续 RUN 与运行时 CMD / ENTRYPOINT 的作用。
  3. Running containers · Linux capabilities 及 User:CHOWN、ALL、用户覆盖与数字用户。
  4. docker container run · --privileged:设备、能力及安全策略被放宽的范围。
  5. Building best practices · USER:无需特权的服务使用非 root 用户。
  6. Docker Desktop · Linux user namespace isolation:Docker Desktop 的 Linux 虚拟机及用户映射。
  7. Rootless mode:守护进程与容器的非 root 运行方式、与 userns-remap 的区别。