12k
All articles

加固 Linux 主机上的 Docker 安全

Docker组权限等同于root权限。通过无根模式、sudo规则、Podman、容器加固和审计清单,保护Linux上的Docker。

OpenReplay Team
OpenReplay Team
加固 Linux 主机上的 Docker 安全

把用户加入 docker 组,就等于把这台机器的 root 权限交给了他。这不是“接近 root”,而是完整的 root:任何能访问 Docker socket 的人,都可以把宿主机文件系统挂载进容器并随意修改。

为了不必每次都输入 sudo,你执行了 usermod -aG docker $USER,这也是 Docker 官方安装后指南建议的做法。可同一份 Docker Engine 的 Linux 安装后配置页面里还有一个提示框,大多数人都一扫而过:加入这个组等同于拥有 root 权限。如果你对基础概念还不太熟悉,可以先阅读 Docker 镜像与容器入门指南。本文先用一条命令证明这一风险,再按防护强度由弱到强介绍修复方案,最后给出一份清单,用于审计从别人手里接手的机器。

核心要点

  • docker 组的任何成员,以及以该成员身份运行的任何进程,都可以通过 docker run --rm -it -v /:/host alpine chroot /host sh 在宿主机上拿到 root shell。
  • Rootless Docker 将 dockerd 和容器都运行在 user namespace 中。即使守护进程被攻破或发生容器逃逸,攻击者拿到的也只是宿主机上的非特权 UID,而不是 root。
  • 针对 /usr/bin/docker 的受限 sudoers 规则仍然等同于 root,但它能在使用 Docker 前要求输入密码,并记录每一次调用。
  • Podman 没有守护进程,以普通用户身份使用时默认即为 rootless 模式。其 CLI 与 Docker 基本兼容,alias docker=podman 足以覆盖大多数日常工作流。
  • 接手一台主机时,应检查以下几项:哪些用户在 docker 组中、dockerd 以哪个用户身份运行、socket 的权限设置,以及守护进程是否监听 TCP 2375 端口。

为什么 docker 组等同于 root?

docker 组之所以等同于 root,是因为 Docker 守护进程以 root 身份运行,而该组控制着对其 Unix socket 的写权限。能写这个 socket,就能指挥一个 root 进程做任何事。如果想了解底层机制,Linux 文件权限指南解释了文件的组权限如何决定谁能打开它。就 Docker 而言,唯一需要关注的细节是:该 socket 对 docker 组可写。

证明如下:

docker run --rm -it -v /:/host alpine chroot /host sh

这条命令把宿主机的 / bind mount 到容器中,然后 chroot 进去。你现在就拥有了一个作用于真实宿主机文件系统的 root shell,可以修改 /etc/shadow 或 /etc/sudoers,也可以往 /root 中添加 SSH 密钥。整个过程既不需要密码,也不需要利用任何漏洞。以你的用户身份运行的任何脚本、依赖包或被入侵的编辑器扩展,都能做同样的事。Docker 文档在 Docker 守护进程攻击面一节中对此有专门说明。

配置 Rootless Docker

Rootless Docker 是一种运行模式,它将 dockerd 和所有容器都运行在归属于你账户的 user namespace 中,因此是解决 docker 组问题最直接的方案。其 socket 位于 $XDG_RUNTIME_DIR/docker.sock,而不是 /var/run/docker.sock。namespace 由 RootlessKit 创建。setuid 辅助程序 newuidmap 和 newgidmap 负责将 namespace 中的 UID 0 映射为你自己的 UID,并把 /etc/subuid 和 /etc/subgid 中的一段范围映射给容器内的其他 UID。网络则通过用户态网络栈实现,例如 slirp4netns、gvisor-tap-vsock 或 pasta。

先安装前置依赖,然后检查你的从属 ID(subordinate ID)范围。rootless 模式文档要求在 /etc/subuid 和 /etc/subgid 中为你的用户分配至少 65,536 个从属 ID。

sudo apt-get install -y uidmap docker-ce-rootless-extras
grep ^$(whoami): /etc/subuid /etc/subgid

停用 root 守护进程,然后以普通用户身份运行安装工具:

sudo systemctl disable --now docker.service docker.socket
dockerd-rootless-setuptool.sh install
systemctl --user enable --now docker
sudo loginctl enable-linger $USER
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

enable-linger 这一行能让你的守护进程在注销后继续运行。确认 rootless Docker 正常工作后,用 sudo gpasswd -d $USER docker 把自己从原来的组中移除。其他发行版的配置方法请参阅官方 rootless 模式文档。如果之后客户端无法连接守护进程,可参考排查“Docker daemon not running”错误一文,其中介绍了 socket 和 systemd 相关的检查步骤。

Rootless 模式依赖非特权 user namespace。Ubuntu 24.04 及更高版本默认通过 AppArmor 限制了这一功能。Ubuntu 的 apparmor 软件包已经自带 rootlesskit 所需的 profile,因此通过 apt 安装 docker-ce-rootless-extras 无需额外步骤。但如果使用 get.docker.com/rootless 脚本安装,就需要自行添加该 profile。不要通过 kernel.apparmor_restrict_unprivileged_userns=0 在全系统范围内关闭这一限制。其他故障场景请参阅 rootless 故障排查页面。

Rootless 模式有哪些代价?

与 rootful 守护进程相比,主要的权衡如下:

限制解决办法
默认无法绑定 1024 以下的端口sysctl net.ipv4.ip_unprivileged_port_start=80,或使用反向代理
--privileged 不会授予真正的宿主机特权仅 user namespace 内的 capabilities 生效
用户态网络比 bridge 驱动慢使用实验性的 lxc-user-nic 驱动,或在上生产前做好基准测试
5.11 之前的内核需要 fuse-overlayfs5.11+ 内核原生支持 overlay2

故障排查页面还列出了其他一些限制,例如不支持 AppArmor、checkpoint 和 overlay 网络。

使用受限规则的 sudo

当 rootless 模式与你的工作负载不兼容时,改为要求通过 sudo 使用 Docker、而不是依赖 docker 组成员身份,是一种折中方案。这仍然等同于 root,因为 sudo docker 同样可以执行上面的 chroot 命令。但你换来的是:每次调用都需要输入密码,并会留下一条审计日志。以你的用户身份静默运行的代码也就无法再直接访问 socket。

sudo gpasswd -d alice docker
sudo visudo -f /etc/sudoers.d/docker
alice ALL=(root) /usr/bin/docker

该规则只允许执行 Docker 二进制文件,而不是任意命令。可以用 journalctl _COMM=sudo 审查使用记录。安装后配置文档中还介绍了一种 newgrp 方法:在 CLI 仍以你自己的用户身份运行的同时,为 Docker 加上一层组密码保护。

改用 Podman

Podman 没有守护进程,以普通用户身份运行时默认就是 rootless 模式。容器作为你用户的子进程运行在 user namespace 中,使用同样的 /etc/subuid 映射。它的 CLI 与 Docker 高度一致,大多数命令无需修改即可使用。

sudo apt-get install -y podman
alias docker=podman
podman run --rm -p 8080:80 docker.io/library/nginx

差异主要集中在边缘场景:需要使用完全限定的镜像名、Compose 支持程度不同,以及依赖 Docker socket 的工具。在让整个团队迁移之前,请先测试你的脚本。

加固容器本身

无论运行哪种守护进程,都应限制容器内进程的能力。以下四项设置可以覆盖大部分风险:使用非 root 的 USER;只保留应用必需的 capabilities;根文件系统只读;禁止通过 setuid 二进制文件提权。

FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
docker run -d \
  --cap-drop=ALL \
  --read-only --tmpfs /tmp \
  --security-opt no-new-privileges \
  -p 8080:8080 myapp

Dockerfile 的 USER 指令用于设置运行时 UID。--cap-drop=ALL 会移除所有 capabilities;如果应用确实需要某项能力,可以通过 --cap-add 加回来。本例中的应用监听 8080 端口,因此不需要任何 capability。--read-only 配合 --tmpfs,意味着只有 /tmp 可写。no-new-privileges 可以阻止 setuid 二进制文件提升权限。以上选项均可在 docker run 参考文档中查到。

审计接手的机器

面对一台别人配置的 VPS,需要回答四个问题:哪些用户在 docker 组中?dockerd 以哪个用户身份运行?谁可以写 socket?守护进程是否在网络上监听?

getent group docker
ps -eo user,cmd | grep [d]ockerd
ls -l /var/run/docker.sock
sudo ss -ltnp | grep -E ':2375|:2376'
systemctl cat docker | grep -- '-H'
grep -s hosts /etc/docker/daemon.json

getent 输出的每一个用户名都拥有 root 权限。如果 ps 显示的是 root,说明守护进程是 rootful 模式。socket 的权限应为 srw-rw---- root docker,一旦发现任何对所有人可写(world-writable)的权限位,必须立即修复。如果 2375 端口有监听,说明 Docker API 在没有 TLS 保护的情况下对外开放。当前版本的 Docker Engine 会拒绝以这种配置启动,但接手主机上的旧版引擎可能仍在这样运行。任何能访问该端口的人都拥有 root 权限。在 2376 端口使用 TLS 监听更安全一些,但持有客户端密钥的人依然拥有 root 权限。请按照保护守护进程 socket 指南改用 SSH 或双向 TLS。此外,还要检查是否有容器挂载了该 socket,因为每一个这样的容器都相当于一个 root 入口:

docker ps -q | xargs -r docker inspect \
  --format '{{.Name}} {{range .Mounts}}{{.Source}} {{end}}' | grep docker.sock

对 Docker socket 的访问权就是 root 权限,没有任何余地。先在你使用的每台机器上运行 getent group docker,然后要么将这些用户迁移到 rootless Docker 或 Podman,要么把 socket 放到带日志记录的 sudo 规则之后。

常见问题

rootless Docker 和 userns-remap 有什么区别?

使用 userns-remap 时,dockerd 仍以 root 身份运行,只有容器运行在重新映射的从属 UID 下,例如在 /etc/docker/daemon.json 中将 userns-remap 设置为 'default' 时创建的 dockremap 用户。它无法堵住 docker 组的漏洞:组成员可以传入 --userns=host,为单个容器关闭重映射,然后执行 chroot 逃逸。而 rootless 模式会让守护进程本身以你的用户身份运行。

rootless 和 rootful Docker 可以在同一台机器上共存吗?

可以。两个守护进程使用不同的 socket:rootful 使用 /var/run/docker.sock,rootless 使用 $XDG_RUNTIME_DIR/docker.sock。运行 dockerd-rootless-setuptool.sh install 会创建一个名为 'rootless' 的 CLI context 并切换过去。使用 docker context use default 可指向 root 守护进程,使用 docker context use rootless 可切换回来。只要 rootful 守护进程仍在运行,任何能写其 socket 的人依然拥有 root 权限。

现有的镜像和容器会迁移到 rootless Docker 吗?

不会。rootless 守护进程使用独立的数据根目录,默认为 ~/.local/share/docker,与 root 守护进程使用的 /var/lib/docker 目录相互独立。在 rootful Docker 下创建的镜像、容器和卷对它不可见。你需要重新拉取镜像,或者在 rootful 守护进程上用 docker save 导出,再在 rootless 守护进程上用 docker load 导入。如果有备份任务只覆盖了 /var/lib/docker,也要相应更新。

为什么 rootless 容器写入的文件在宿主机上显示的属主很奇怪?

Rootless Docker 会把容器 UID 映射为宿主机 UID。容器内的 root 映射为你自己的 UID,因此它写入 bind mount 的文件在宿主机上显示为你的文件。容器内的其他 UID 则映射到你在 /etc/subuid 中的范围。如果该范围从 100000 开始,容器 UID 1000 就会变成宿主机 UID 100999,你的用户无法直接编辑这些文件。在容器内将文件 chown 为 0:0,即可把它们归还给你的用户。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.