容器运行时、引擎、构建器与沙箱
Tip
学习和实践 AWS 黑客技术:
HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
容器安全中最常见的混淆来源之一是多个完全不同的组件经常被同一个词覆盖。“Docker” 这个词可能指一个镜像格式、一个 CLI、一个守护进程、一个构建系统、一个运行时栈,或者干脆就是容器这个概念本身。对于安全工作来说,这种歧义是个问题,因为不同层负责不同的防护。由错误的 bind mount 导致的突破并不等同于由低级运行时漏洞导致的突破,也不同于 Kubernetes 中的集群策略错误。
本页通过按角色划分生态系统,以便后续章节可以精确地讨论保护或弱点实际存在于何处。
OCI 作为通用语言
现代 Linux 容器栈之所以经常互操作,是因为它们遵循一组 OCI 规范。OCI Image Specification 描述了镜像和层如何表示。OCI Runtime Specification 描述了运行时应如何启动进程,包括 namespaces、mounts、cgroups 和安全设置。OCI Distribution Specification 标准化了注册表如何暴露内容。
这很重要,因为它解释了为什么用一个工具构建的容器镜像通常可以用另一个工具运行,以及为什么若干引擎可以共享相同的低级运行时。它也解释了为什么不同产品的安全行为看起来相似:很多产品都在构造相同的 OCI 运行时配置并把它交给同一小集合的运行时。
低级 OCI 运行时
低级运行时是最接近内核边界的组件。它是实际创建 namespaces、写入 cgroup 设置、应用 capabilities 和 seccomp 过滤器,并最终 execve() 容器进程的部分。当人们在机械层面讨论“容器隔离”时,通常指的就是这一层,尽管他们不一定会明确说明。
runc
runc 是参考实现的 OCI 运行时,也是最知名的实现。它在 Docker、containerd 和许多 Kubernetes 部署中被广泛使用。大量的公开研究和利用材料针对 runc 风格的环境,仅仅因为它们常见,而且 runc 定义了许多人在想象 Linux 容器时的基线。因此理解 runc 会为读者提供经典容器隔离的强烈心智模型。
crun
crun 是另一个 OCI 运行时,用 C 编写,在现代 Podman 环境中被广泛使用。它常因对 cgroup v2 支持良好、rootless 体验强、开销较低而受到赞扬。从安全角度看,重要的并不是它使用了不同的语言,而是它仍然扮演相同的角色:它是将 OCI 配置转换为在内核下运行的进程树的组件。一个 rootless 的 Podman 工作流常常让人感觉更安全,不是因为 crun 能神奇地修复一切,而是因为围绕它的整体栈更倾向于使用 user namespaces 和最小权限。
runsc(来自 gVisor)
runsc 是 gVisor 使用的运行时。在这里,边界发生了实质性变化。gVisor 不像通常那样将大多数 syscall 直接传递给主机内核,而是在用户态插入一个内核层来模拟或中介 Linux 接口的大部分。其结果不是带有几个额外标志的普通 runc 容器;而是一种不同的沙箱设计,目的是减少主机内核的攻击面。兼容性和性能的折衷是该设计的一部分,因此使用 runsc 的环境应与普通 OCI 运行时环境区别开来记录。
kata-runtime
Kata Containers 通过在轻量虚拟机内启动工作负载将边界进一步推进。从管理上看,这仍可能看起来像是容器部署,编排层也可能仍将其视为容器,但底层的隔离边界更接近虚拟化而非经典的主机内核共享容器。当需要比传统容器更强的租户隔离但又不放弃以容器为中心的工作流时,Kata 就非常有用。
引擎与容器管理器
如果低级运行时是直接与内核通信的组件,那么引擎或管理器就是用户和运维人员通常交互的组件。它处理镜像拉取、元数据、日志、网络、卷、生命周期操作和 API 暴露。这一层非常重要,因为许多现实世界的妥协就发生在这里:访问运行时 socket 或守护进程 API 即便在低级运行时本身完全健康的情况下,也可能等同于对主机的妥协。
Docker Engine
Docker Engine 是开发者最熟悉的容器平台,也是容器词汇之所以如此“Docker 化”的原因之一。典型路径是 docker CLI 到 dockerd,后者又协调诸如 containerd 和 OCI 运行时等低级组件。历史上,Docker 部署通常是以 root 运行(rootful),因此访问 Docker socket 一直是非常强大的原语。这就是为什么大量实际的提权材料关注 docker.sock:如果一个进程可以要求 dockerd 创建特权容器、挂载主机路径或加入主机 namespace,那么它可能根本不需要内核漏洞。
Podman
Podman 的设计围绕更无守护进程模型(daemonless)展开。从运维角度看,这有助于强化这样一个想法:容器只是通过标准 Linux 机制管理的进程,而不是通过一个长期运行的特权守护进程。与许多人最初接触到的传统 Docker 部署相比,Podman 在 rootless 方面也有更强的故事。这并不意味着 Podman 自动安全,但它显著改变了默认风险特征,尤其是在与 user namespaces、SELinux 和 crun 结合时。
containerd
containerd 是许多现代栈中的核心运行时管理组件。它被用于 Docker 之下,也是主导的 Kubernetes 运行时后端之一。它暴露强大的 API,管理镜像和快照,并将最终的进程创建委托给低级运行时。围绕 containerd 的安全讨论应强调,访问 containerd socket 或 ctr/nerdctl 功能可能与访问 Docker 的 API 一样危险,即使接口和工作流感觉不那么“面向开发者”。
CRI-O
CRI-O 的关注点比 Docker Engine 更窄。它并不是一个通用的开发者平台,而是围绕清晰实现 Kubernetes Container Runtime Interface 构建的。这使得它在 Kubernetes 发行版和如 OpenShift 这样的 SELinux 密集型生态中尤为常见。从安全角度看,这种更狭窄的范围是有用的,因为它减少了概念上的混乱:CRI-O 很明显是“为 Kubernetes 运行容器”那一层的一部分,而不是一个全能平台。
Incus、LXD 与 LXC
Incus/LXD/LXC 系统值得与 Docker 风格的应用容器区分开来,因为它们经常被用作系统容器(system containers)。系统容器通常预期看起来更像一个轻量级的机器,拥有更完整的 userspace、长期运行的服务、更丰富的设备暴露和更广泛的主机集成。隔离机制仍然是内核原语,但运维期望不同。因此,这里的配置错误通常不像“糟糕的应用容器默认值”,更像是轻量虚拟化或主机委派方面的错误。
systemd-nspawn
systemd-nspawn 占据了一个有趣的位置,因为它是 systemd 原生的,并且在测试、调试和运行类操作系统环境时非常有用。它不是云原生生产环境的主导运行时,但在实验室和面向发行版的环境中出现的频率足以值得一提。对安全分析而言,它再次提醒我们“容器”这个概念跨越多个生态系统和运维风格。
Apptainer / Singularity
Apptainer(前称 Singularity)在研究和 HPC 环境中很常见。它的信任假设、用户工作流和执行模型与以 Docker/Kubernetes 为中心的堆栈在重要方面不同。特别是,这些环境通常非常重视让用户在不授予他们广泛特权的容器管理能力的情况下运行打包的工作负载。如果审查者假定每个容器环境基本上都是“服务器上的 Docker”,他们将严重误解这些部署。
构建时工具链
许多安全讨论只关注运行时,但构建时工具链也很重要,因为它决定了镜像内容、构建密钥的曝光,以及多少受信任的上下文被嵌入到最终制品中。
BuildKit 和 docker buildx 是支持缓存、secret 挂载、SSH 转发和多平台构建等功能的现代构建后端。这些是有用的功能,但从安全角度看,它们也会形成秘密可能 leak 到镜像层的场所,或者过于宽泛的构建上下文可能暴露不应包含的文件。Buildah 在 OCI 原生生态中扮演类似角色,尤其是在 Podman 周围,而 Kaniko 常用于不愿意在 CI 管道中授予特权 Docker 守护进程的环境。
关键教训是镜像创建和镜像执行是不同阶段,但脆弱的构建管道可以在容器启动之前很久就制造出脆弱的运行时姿态。
编排是另一层,不是运行时
不应将 Kubernetes 心理上等同于运行时本身。Kubernetes 是编排器。它调度 Pods、存储期望状态,并通过工作负载配置表达安全策略。kubelet 然后与诸如 containerd 或 CRI-O 的 CRI 实现通信,后者又调用诸如 runc、crun、runsc 或 kata-runtime 之类的低级运行时。
这种分离很重要,因为许多人错误地将某项保护归功于“Kubernetes”,而实际上它是由节点运行时强制执行的,或者他们将行为归咎于“containerd 默认值”,而实际上行为来自 Pod 规范。实际上,最终的安全姿态是一个组合:编排器提出请求,运行时栈进行翻译,内核最终强制执行。
为什么在评估时识别运行时很重要
如果你及早识别出引擎和运行时,许多后续观察会更容易解释。一个 rootless 的 Podman 容器暗示 user namespaces 很可能是故事的一部分。一个 Docker socket 被挂载到工作负载中暗示基于 API 的提权是切实可行的路径。一个 CRI-O/OpenShift 节点应立即让你想到 SELinux 标签和受限工作负载策略。一个 gVisor 或 Kata 环境应让你更加谨慎,不要假定经典的 runc 越狱 PoC 会以相同方式工作。
这就是为什么容器评估的第一步之一应该总是回答两个简单问题:到底是哪个组件在管理容器,以及到底哪个运行时实际启动了进程。一旦这些答案明确,其余环境通常就更容易推理。
运行时漏洞
并非每次容器逃逸都来自运维错误配置。有时运行时本身就是易受攻击的组件。这很重要,因为某个工作负载可能在看似谨慎的配置下运行,仍然因为低级运行时缺陷而暴露。
经典示例是 runc 中的 CVE-2019-5736,恶意容器可以覆盖主机上的 runc 二进制文件,然后等待后续的 docker exec 或类似运行时调用触发攻击者控制的代码。利用路径与简单的 bind-mount 或 capability 错误完全不同,因为它滥用了运行时在处理 exec 时重新进入容器进程空间的方式。
从 red-team 的角度,一个最小的复现工作流是:
go build main.go
./main
然后,从宿主机:
docker exec -it <container-name> /bin/sh
关键教训不是确切的历史漏洞利用实现,而是评估含义:如果运行时版本存在漏洞,普通的 in-container code execution 可能足以在可见的容器配置看起来并不明显薄弱的情况下危及主机。
最近的 runtime CVEs,例如 CVE-2024-21626 在 runc 中、BuildKit 的 mount races,以及 containerd 的 parsing bugs,都进一步强化了同一点。运行时版本和补丁级别是安全边界的一部分,而不仅仅是维护琐事。
Tip
学习和实践 AWS 黑客技术:
HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。


