1. 容器隔离的信任边界从一个命名空间缺陷说起容器技术这几年几乎成了后端部署的默认选项Docker 把应用和依赖打包在一起跑起来轻量、启动快、资源占用低很多人下意识会觉得“容器就是一个轻量级虚拟机天然安全隔离”。这个认知其实有偏差。容器本质上是宿主机上的一组进程靠 Linux 内核的 namespace 做视图隔离、靠 cgroup 做资源限制、靠 capability 和 seccomp 做权限收敛。隔离的强度取决于这些内核机制配置得对不对一旦某个环节有缺口容器里的进程就可能摸到宿主机的资源。CVE-2020-15257 就是这样一个典型的隔离边界缺陷。它影响的是 containerd 这个组件具体出问题的地方在containerd-shim处理abstract unix domain socket的方式上。攻击者如果已经拿到了一个容器的 shell并且这个容器和宿主机共享了网络命名空间比如用了--nethost就能通过这个 socket 与 shim 通信进而以 root 身份在宿主机上执行操作实现容器逃逸。这个漏洞的 CVSS 评分是 5.3属于中危但它的实际危害不小因为它不需要什么复杂的利用链条件满足时利用门槛很低。我写这篇东西是想把这类漏洞的来龙去脉讲清楚它为什么会产生、影响范围到底有多大、怎么判断自己的环境是否中招、怎么修、修完之后怎么验证。适合已经用过 Docker、对容器有基本操作经验、但没深入过内核隔离机制的开发和运维同学。看完你应该能独立完成一次自查和修复也能理解容器安全里“共享命名空间”这件事为什么值得警惕。2. 漏洞的成因拆解abstract socket 与共享命名空间的碰撞2.1 containerd-shim 到底在干什么要理解这个漏洞得先知道containerd-shim的角色。Docker 的架构里dockerd负责接收命令真正干活的是 containerd而 containerd 又会为每个容器启动一个containerd-shim进程。这个 shim 进程的作用是“托管”容器的主进程它让容器进程和 containerd 解耦这样即使 containerd 重启容器也不会跟着挂掉同时它负责收集容器的退出状态、转发信号、管理 stdin/stdout 等。shim 和 containerd 之间需要通信通信方式之一就是 unix domain socket。unix socket 有两种一种是基于文件系统的会在磁盘上生成一个 socket 文件有路径另一种是 abstract socket它没有文件实体名字存在内核里通过一个前导的\0字节来标识。abstract socket 的好处是不依赖文件系统不用担心路径权限和清理问题所以 containerd 的 shim 用了它。问题就出在这里abstract socket 是属于网络命名空间的。也就是说两个进程要能通过 abstract socket 通信它们必须处在同一个网络命名空间里。正常情况下每个容器有自己的网络命名空间shim 在宿主机的命名空间两者隔离容器里的进程看不到 shim 的 socket。但如果容器被配置成共享宿主机的网络命名空间这个隔离就没了。2.2 共享网络命名空间为什么是导火索docker run --nethost这个参数会让容器直接使用宿主机的网络命名空间。用它的场景其实不少需要极致网络性能、需要容器直接绑定宿主机端口、或者一些监控类容器需要看到宿主机所有网卡。用了这个参数之后容器里的进程和宿主机进程共享同一套网络栈包括所有的 abstract socket。这时候容器里的进程只要知道 shim 的 abstract socket 名字就能连上去。而 shim 的 socket 名字是有规律的它由 containerd 生成格式大致是\0containerd-shim/hash/id这样的结构。攻击者可以通过遍历或者猜测的方式找到它。一旦连上shim 提供的 API 里有一些操作是可以被利用的比如它能接受创建新进程的请求。由于 shim 是以 root 身份运行的攻击者就能借它之手在宿主机上以 root 执行命令。这里的关键点在于漏洞本身不是 shim 的 API 设计有逻辑漏洞而是它暴露在了一个不该暴露的命名空间里。如果容器没有共享网络命名空间容器进程根本看不到这个 socket也就无从利用。所以这个漏洞的触发条件里“共享网络命名空间”是必要的一环。2.3 影响版本与组件范围这个漏洞影响的是 containerd 1.3.x 系列1.3.0 到 1.3.8以及 1.4.x 系列1.4.0 到 1.4.2。修复版本是 1.3.9 和 1.4.3。需要注意的是Docker 本身并不直接包含 containerd 的代码但 Docker 依赖 containerd所以受影响的 Docker 版本取决于它捆绑的 containerd 版本。一般来说Docker 19.03.x 早期版本会受影响具体要看发行版打包时用的 containerd 版本。另外Kubernetes 集群如果用了 containerd 作为运行时也可能受影响尤其是那些允许 Pod 使用hostNetwork: true的集群。不过 Kubernetes 默认的 Pod 网络是独立的只有显式配置了 hostNetwork 的 Pod 才有风险。3. 自查判断你的环境是否在风险范围内3.1 确认 containerd 版本第一步是查 containerd 的版本。在宿主机上执行containerd --version或者如果 containerd 是通过 systemd 管理的可以看服务状态systemctl status containerd输出里会带版本号。如果版本落在 1.3.0–1.3.8 或 1.4.0–1.4.2 之间就需要警惕。如果用的是 Docker可以用docker version看 Server 部分的输出但 Docker 的版本号不一定直接对应 containerd 版本更可靠的方式还是直接查 containerd。3.2 检查是否有容器使用了 host 网络这一步是判断实际暴露面。列出所有正在运行的容器看它们的网络模式docker ps --format {{.ID}} {{.Names}} {{.Networks}}如果Networks字段显示host说明这个容器共享了宿主机的网络命名空间。也可以用docker inspect --format {{.Name}} {{.HostConfig.NetworkMode}} $(docker ps -q)NetworkMode为host的就是风险容器。对于 Kubernetes检查是否有 Pod 设置了hostNetwork: truekubectl get pods --all-namespaces -o jsonpath{range .items[?(.spec.hostNetworktrue)]}{.metadata.namespace}{/}{.metadata.name}{\n}{end}3.3 一个快速验证思路如果你想确认漏洞是否真的可被利用可以在一个--nethost的容器里尝试列出 abstract socket。Linux 下可以用ss命令ss -a -x | grep containerd如果能看到类似containerd-shim的 abstract socket 条目说明容器确实能感知到 shim 的 socket风险是实打实的。这个命令只是查看不涉及任何攻击行为属于安全自查的常规手段。注意自查操作请在你有权限的环境里进行不要对生产环境做未授权的探测。4. 修复方案升级、配置收敛与纵深防御4.1 首选方案升级 containerd最直接、最彻底的修复就是升级 containerd 到 1.3.9 或 1.4.3 及以上版本。官方在修复版本里改动了 shim 的 socket 处理逻辑具体做法是给 abstract socket 加上了访问控制使得只有特定命名空间里的进程才能连接。升级步骤以 systemd 管理的环境为例# 先停掉相关服务 systemctl stop docker systemctl stop containerd # 备份当前二进制 cp /usr/bin/containerd /usr/bin/containerd.bak cp /usr/bin/containerd-shim /usr/bin/containerd-shim.bak # 下载并替换新版本二进制具体下载方式依发行版而定 # 假设已经拿到 1.4.3 的二进制包 tar -xzf containerd-1.4.3-linux-amd64.tar.gz cp bin/containerd /usr/bin/containerd cp bin/containerd-shim /usr/bin/containerd-shim cp bin/containerd-shim-runc-v2 /usr/bin/containerd-shim-runc-v2 # 重启服务 systemctl start containerd systemctl start docker升级完之后用containerd --version确认版本号已经变了。这里有个坑有些发行版的包管理器和手动替换二进制会冲突如果你是用 apt 或 yum 装的优先用包管理器升级避免版本混乱。4.2 次选方案避免不必要的 host 网络如果因为某些原因暂时没法升级或者升级窗口还没到可以先把使用--nethost的容器梳理一遍。很多容器用 host 网络其实是不必要的比如一些内部服务完全可以用 bridge 网络加端口映射来替代。把非必要的 host 网络容器改成默认网络能直接消除这个漏洞的触发条件。改法很简单把docker run里的--nethost去掉改用-p做端口映射# 原来 docker run --nethost myapp # 改成 docker run -p 8080:8080 myapp对于 Kubernetes把 Pod spec 里的hostNetwork: true去掉改用 Service 暴露。这个改动可能需要调整一些依赖宿主机网络的逻辑比如某些监控 agent 需要看到所有网卡那就得评估是否真的必须。4.3 纵深防御限制容器能力即使升级了也建议顺手做一层权限收敛。Docker 默认给容器的 capability 里有一些是不必要的可以用--cap-drop去掉docker run --cap-dropALL --cap-addNET_BIND_SERVICE myapp这样即使将来出现类似漏洞攻击者能做的事情也被限制住了。另外启用 seccomp 默认配置、避免用--privileged都是常规的加固手段。--privileged会关掉大部分隔离等于把容器变成半个宿主机进程能不用就不用。5. 验证与回归修完之后怎么确认真的安全了5.1 版本确认与 socket 行为验证升级完成后除了看版本号还可以再跑一次之前的自查命令。在一个--nethost的容器里执行ss -a -x | grep containerd如果修复生效应该看不到 shim 的 abstract socket或者即使看到也无法连接。这是因为修复后的 shim 对 socket 的访问做了限制非授权命名空间的连接会被拒绝。5.2 功能回归检查升级 containerd 是有一定风险的操作因为它直接关系到容器的生命周期管理。升级后要重点验证几件事已有容器是否还能正常重启、停止、删除新建容器是否正常容器的日志收集是否正常shim 负责 stdout/stderr 转发如果用了 Kubernetes节点上的 Pod 是否正常调度和运行我自己的习惯是先在测试环境升级跑一遍完整的容器生命周期操作确认没问题再上生产。生产环境升级时尽量选业务低峰期并且准备好回滚方案——把旧版本的二进制备份好万一出问题能快速切回去。5.3 持续监控漏洞修完了不代表一劳永逸。建议把 containerd 的版本纳入资产清单定期检查是否有新版本发布。同时监控是否有容器异常地使用了 host 网络可以在 CI/CD 流程里加一道检查禁止--nethost出现在部署配置里除非有明确的审批。6. 常见问题与排查实录6.1 升级后容器起不来怎么办这是升级 containerd 后最常见的问题。原因通常是新旧版本的 shim 二进制不匹配或者 runc 版本不兼容。排查思路看 containerd 的日志journalctl -u containerd -n 100看具体容器的错误docker logs container或者docker inspect container看状态确认containerd-shim和containerd-shim-runc-v2都在正确路径下且版本和 containerd 匹配如果报的是 runc 相关错误可能需要同步升级 runc。containerd 1.4.x 对 runc 的版本有最低要求太老的 runc 会不兼容。6.2 怎么判断一个容器是否真的需要 host 网络这个问题经常被问到。我的判断标准是如果容器需要绑定宿主机的特定网卡、需要看到宿主机的所有网络接口、或者对网络延迟极度敏感比如某些高性能网关才考虑 host 网络。普通的 Web 服务、数据库、缓存用 bridge 网络加端口映射完全够用性能差异在大多数场景下可以忽略。6.3 漏洞速查表检查项命令/方法风险判断containerd 版本containerd --version1.3.0–1.3.8 或 1.4.0–1.4.2 为受影响容器网络模式docker inspect --format {{.HostConfig.NetworkMode}} idhost为高风险abstract socket 可见性容器内ss -a -x | grep containerd能看到则暴露Kubernetes hostNetworkkubectl get pods -o jsonpath...true为高风险容器 capabilitydocker inspect --format {{.HostConfig.CapAdd}} id有SYS_ADMIN等为高风险6.4 几个容易踩的坑第一个坑是只升级了 Docker 没升级 containerd。Docker 和 containerd 是分开打包的有些环境里 Docker 升级了但 containerd 还是老的漏洞依然在。一定要单独确认 containerd 版本。第二个坑是以为不用 host 网络就绝对安全。这个漏洞的触发确实需要共享网络命名空间但如果你环境里有其他机制能让容器进程进入宿主机命名空间比如某些特权操作风险依然存在。所以升级才是根本解法配置收敛只是缓解。第三个坑是升级后忘了重启容器。containerd 升级后已经运行的容器还是用旧的 shim 进程需要把容器重启一遍才能让新的 shim 生效。滚动重启是个稳妥的做法。7. 从这一个漏洞看容器安全的通用思路CVE-2020-15257 给我的最大启发是容器的隔离不是靠单一机制实现的而是 namespace、cgroup、capability、seccomp 等多层机制叠加的结果。任何一层被削弱整体安全性都会下降。共享网络命名空间这件事表面上看只是网络配置实际上它打破了 namespace 隔离的完整性让原本不可见的资源变得可见。我在实际环境里做容器加固时会遵循几个原则。第一默认拒绝能用默认网络就不用 host 网络能用非 root 用户就不用 root。第二最小权限capability 按需添加不要图省事直接--privileged。第三及时更新containerd、runc、Docker 这些底层组件要纳入常规补丁管理。第四纵深防御不指望某一个机制能挡住所有攻击而是让攻击者即使突破一层也还有下一层拦着。这套思路不只适用于这一个漏洞对大多数容器相关的安全问题都通用。容器安全没有银弹靠的是一层一层把口子收窄。