本节摘要:Docker 用一个以 root 身份常驻后台的守护进程换来了容器管理的便利,这份便利在容器进入生产环境后变成了三类可量化的风险:守护进程单点故障、容器逃逸的权限放大、CI 流水线挂载 Docker socket 带来的越权。本节把这笔账逐项算清,它是全册所有对比的出发点。
容器的故事比 Docker 早得多。1979 年 chroot 进入 Unix,给了进程一个"假根目录";2008 年前后 LXC 把命名空间与 cgroup 打包成可用的容器;2013 年 Docker 出现,真正的贡献不是内核机制——那些早就有了——而是把"镜像分层 + 分发仓库 + 一行命令运行"这套交付范式做顺了。从此"在我机器上能跑"变成了"这个镜像在哪都能跑"。
Docker 架构上做了一个关键取舍:客户端只负责拼命令,所有脏活(创建命名空间、配 cgroup、搭网络、管理镜像层)都交给 dockerd。这个以 root 身份常驻的守护进程,就是本节的账单主角。

单点不是抽象词。运维 Docker 的工程师都见过这样的场景:升级 docker-ce 包,systemd 重启 dockerd,几百个正在跑的容器瞬间失去管理接口——容器进程还在,但 docker ps 再也看不到它们,重启策略也全部失灵。要让容器在 daemon 重启后自动回来,得依赖 live-restore 这类配置,而它默认不开启。
反过来对比一个数字化的感受:Podman 没有 daemon 升级窗口这个概念,因为引擎升级就是换一个二进制文件,正在运行的容器进程是用户会话或 systemd 的子进程,与管理工具是否重启无关。第一次从 Docker 迁过来的用户,最惊讶的往往是"卸载再装 Podman,容器还在跑"。
容器逃逸本身两类引擎都难完全避免——内核漏洞是共同的地基。区别在逃逸之后:Docker 默认配置下,容器内 UID 0 就是宿主机 UID 0(未启用用户命名空间映射时),逃逸者直接站在宿主机的最高权限上,可以读任意容器的数据、装后门、横向移动。
同样场景换到 rootless Podman:容器里的 root 在宿主机上只是一个映射出来的高位 UID(比如 100000),逃逸者拿到的是一个连 sudo 组都不在的普通身份,能碰到的只有自己 UID 范围内的文件。攻击面从"一台宿主机"缩到"一段无特权的 UID 区间"。这就是第 2 章要展开的用户命名空间映射机制——这里先把账记上。
最被低估的一栏。大量 CI 配置为了在流水线里构建镜像,把宿主机的 Docker socket 直接挂载进构建容器:
# 一段看起来无害、实则等于交出宿主机的 CI 配置 docker run -v /var/run/docker.sock:/var/run/docker.sock \ -v $(pwd):/workspace ci-builder:latest ./build.sh # 构建容器里随后可以做的事: docker run --privileged -v /:/host alpine sh -c 'chroot /host' # 解释:能访问 docker.sock 就能以任意参数让 dockerd 拉起容器, # --privileged 加挂载宿主机根目录,等价于直接登录宿主机 root shell
这段配置的问题不在某条命令,而在权限传导链:构建容器拿到 socket,socket 背后是 root 的 dockerd,dockerd 能创建特权容器——链条终点是宿主机 root。任何能提交代码到这条流水线的人,都事实上拥有宿主机。安全团队把这称为"CI 即 root"。
Podman 时代同样的需求怎么解?构建容器里跑 rootless Podman,构建进程fork出Buildah,全部发生在用户的命名空间里,不存在一个可以被挂载的 root socket。第 9 章的迁移实战会给完整方案,这里只需看到账单上的对应关系。
把三栏并起来看,会发现一个共同根源:Docker 把所有用户的容器操作代理给了一个 root 常驻进程。还账的思路因此只有两条——要么把守护进程锁进笼子,要么干脆不要守护进程。
第一条路 Docker 自己也在走,值得公平地记录:后来版本的 Docker 支持 rootless mode(以普通用户跑 dockerd),社区也出现了 socket 代理方案(在容器与 dockerd 之间加一道过滤层)。但这两剂药都是"在既有架构上打补丁":rootless dockerd 仍是一个常驻进程,只是降了权限;socket 代理则要求你维护一份允许清单,而绕过它只需要一个没被拦住的调用。补丁的存在本身说明设计者承认了账单,但架构的惯性让每张补丁都比"一开始就不这么设计"更费劲。
第二条路是釜底抽薪:不要守护进程,让每个用户的容器操作由用户自己的进程完成,"特权代理"这个概念整个消失。Podman 选了第二条,代价与收益是第 1.2 节的主角。
一个合理的疑问:这些风险从 Docker 诞生第一天就存在,为什么直到容器进生产多年后才被认真对待?答案藏在威胁模型的变化里。容器早期的主要用户是开发者,攻击面是"自己的笔记本上跑自己的容器",root 守护进程的额外风险几乎无感。当容器成为多租户平台、CI 基础设施、企业合规审计的对象,威胁模型从"我攻击我自己的机器"变成"低权限租户能否借容器拿到宿主机"——同一份架构在旧模型下是便利,在新模型下是漏洞。理解这个转变比记住任何一条具体风险都重要:它解释了为什么安全重构总是姗姗来迟,也提醒你在评估任何基础设施时先问一句"我的威胁模型是什么"。