- 容器与主机共享内核,因此它们的隔离性取决于命名空间、cgroups 和系统加固。
- 与传统的 Docker 相比,Podman 通过避免使用中央 root 守护进程并实现无 root 执行,提供了额外的安全性。
- Podman 中的桥接网络、主机网络和无根网络(slirp4netns、paste)允许您调整连接性和隔离性之间的平衡。
- 结合 KVM、容器和最佳实践(无根管理、MAC 地址管理、安全计算、镜像扫描),为生产环境提供了一个强大的平台。

当你在 Linux 上搭建一个成熟的容器环境时,首先会遇到一个问题:完全依赖容器的安全性究竟有多高?何时应该升级到 KVM 或微型虚拟机?在企业环境中,Docker、Podman、LXC、KVM 虚拟机以及各种虚拟机管理程序混杂使用,因此,对隔离模型的深入理解是避免出错的关键。
此外,一些较为复杂的场景也并不少见,例如在 VirtualBox 或 KVM 等虚拟机中使用 Docker Desktop 或 Podman,其中涉及多层虚拟化(嵌套或非嵌套),很容易遇到“主机上未启用 KVM”之类的错误。让我们把容器、虚拟机、Podman、KVM、网络和安全等所有组件梳理清楚,看看如何将它们结合起来,实现真正安全实用的虚拟化。
使用 KVM 和虚拟机的经典虚拟化
整个架构的基础仍然是使用诸如 KVM、Xen 或 ESXi 之类的虚拟机管理程序对操作系统进行完全虚拟化。在这种架构中,物理硬件运行着一个虚拟机管理程序,该程序负责将机器“分割”成多个独立的虚拟机。
在这种模式下,每个虚拟机都启动自己的内核和客户操作系统,与宿主机完全分离。虚拟机管理程序(例如,集成在 Linux 内核中的 KVM)充当硬件和虚拟机之间的守门人,负责实现它们之间的严格隔离。
我们通常将虚拟机管理程序分为两类:第一类(裸机虚拟机管理程序),直接运行在硬件上;第二类(虚拟机管理程序),运行在现有操作系统之上。VirtualBox或 VMware Workstation 属于第二类,而 Linux 服务器上由 libvirt 管理的 KVM 虚拟机则更接近第一类,即使它与宿主机共享资源。
这种方法最大的优势在于每个虚拟机都拥有自己完整的堆栈:内核、用户空间、服务、网络和存储。这提供了极大的灵活性,并允许在同一台物理机上运行不同的操作系统和内核版本。
在 GNU/Linux 中管理这些虚拟化平台时,几乎总是使用 libvirt作为编排 API,在此基础上支持图形和命令行工具,或者支持像 OpenStack 这样的大型项目,后者通过结合虚拟机、裸机和容器来构建完整的云。
KVM的硬件要求和常见问题
在运行 KVM 或使用依赖于 KVM 的解决方案(例如某些 Docker Desktop 或微型虚拟机配置)之前,您需要确保处理器支持硬件虚拟化(Intel 上的 VT-x 或 AMD 上的 SVM/AMD-V),并且已在 BIOS/UEFI 中启用该功能。
在 Linux 系统中,我们有一个快速检查方法:如果命令`grep -E 'svm|vmx' /proc/cpuinfo`没有返回任何结果,或者固件中该选项仍然被禁用,则 KVM 将无法工作。此外,在需要将虚拟机嵌套在另一个虚拟机中运行(嵌套虚拟化)的情况下,顶层虚拟机管理程序必须显式地将这些扩展暴露给虚拟机。
当出现问题时,会出现一些常见的错误信息,例如在 VirtualBox 中运行 RHEL 9 时,Docker Desktop 会弹出一个臭名昭著的提示框:“主机上未启用 KVM ”。问题不在于 Docker 或 RHEL 本身,而在于 VirtualBox 虚拟机没有获得运行 KVM 所需的虚拟化扩展。
类似`modprobe kvm`或`modprobe kvm_amd / kvm_intel`的命令应该能够加载内核模块,且不会出现错误信息。如果 `dmesg` 没有显示任何相关信息,或者`lsmod | grep kvm`只显示 `kvm` 和 `irqbypass`,并且缺少 CPU 特定的模块,则很可能是硬件虚拟化没有对客户机系统开放,或者在固件中被禁用。
在这种情况下,即使虚拟机中启用了所有可能的功能,如果 VirtualBox 不支持嵌套虚拟化或配置错误,KVM 也无法正常工作。解决方法是检查宿主机管理程序(VirtualBox、VMware 等)的配置,并在可用时启用嵌套虚拟化。
容器:操作系统级别的轻量级虚拟化
与传统虚拟机相比,容器提供了一种不同的方法:它们不模拟所有硬件,而是重用主机内核,并使用 Linux 内核函数隔离进程和资源。
容器本质上是一组使用命名空间和 cgroups 封装的进程。在这个“环境”中,应用程序认为它运行在自己的机器上,拥有自己的文件系统、网络接口和进程 ID (PID),但实际上,它与宿主机共享内核。
容器能够提供接近原生系统的性能,因为它无需第二个内核,也无需完整的硬件模拟。其开销极小:仅足以挂载命名空间、使用 cgroups 限制资源以及强制执行安全策略(seccomp、AppArmor、SELinux 等)。
然而,这种设计存在一个重大后果:所有工作负载共享同一个物理内核。该内核中的一个严重漏洞可能突然成为运行在该节点上的所有容器的潜在逃逸途径。
因此,人们常说容器隔离不像虚拟机那样“绝对”。如果有人成功利用了严重的内核漏洞,命名空间就不再是有效的屏障,容器和主机之间的边界就可能被打破。
命名空间和 cgroups:容器隔离的基础
在 Linux 中,容器隔离建立在两个主要支柱之上:命名空间用于分离系统视图,cgroups 用于限制和统计资源。
命名空间隔离了进程ID (PID)、网络、挂载点、进程间通信 (IPC)、主机名(统一主机空间,UTS)和用户等资源。这样,进程只能“看到”自身命名空间内的进程、网络接口、文件系统和主机名,使其感觉像是在不同的系统中运行。
cgroups 允许您指定容器可以使用的 CPU、内存、磁盘 I/O、进程数等资源。这可以防止不受控制的负载压垮整个主机,这在团队或项目之间共享节点时至关重要。
然而,必须明确的是,命名空间和 cgroups 本身并不能构成完整的安全系统。它们无法阻止内核漏洞的利用、过滤系统调用或管理细粒度的访问策略。它们是基础,但需要进一步加强。
加固环境的合理方法是将系统调用过滤器与 seccomp、MAC 策略(AppArmor 或 SELinux)以及大幅降低进程功能相结合,理想情况下,最好再结合用户命名空间和无根模式,以最大限度地减少有人设法逃离容器时造成的损害。
容器平台的安全威胁
在真正的容器平台中,攻击面分布在多个层面:内核和运行时(runc、containerd、CRI-O)、镜像供应链、容器/编排器配置以及底层基础设施(云、存储、IAM、网络)。
常见的攻击途径包括内核或容器运行时中的软件漏洞、使用不安全或未维护的镜像、在 Kubernetes 或 Docker 中出现配置错误(特权容器、危险的主机挂载、过度开放的网络模式)以及注册表或 CI/CD 管道中的安全缺陷。
在许多现实事件中,并不存在单一的“神奇漏洞”,而是已知漏洞与宽松配置的组合:容器以 root 用户身份运行、使用 --privileged 标志、在容器内挂载主机文件系统以及缺少 seccomp 或 MAC。
除此之外,供应链问题也不容忽视:公开的基础镜像通常包含漏洞代码 (CVE)、过时的软件包以及管理不善的依赖项。恶意软件很容易绕过这些限制,或者被入侵的库可以保持休眠状态,直到合适的条件出现才执行。
因此,在生产环境中,这不是可选项:您必须使用 Trivy、Clair 或 Grype 等工具扫描镜像是否存在漏洞,控制使用哪些注册表(Harbor、Quay、GHCR 及其策略),并在将镜像部署到集群之前对其进行签名/验证。
Docker 与 Podman:对威胁模型的影响
在应用容器领域,Docker 和 Podman 涵盖了类似的功能,但它们的架构却大相径庭,这改变了我们在主机上使用的安全模型。
Docker 在其经典的 root 模式下,依赖于一个以 root 用户身份运行的中央守护进程 (dockerd)。CLI 通过 /var/run/docker.sock 或 TCP 与该守护进程通信,正是这个特权进程负责创建和销毁容器、管理镜像、网络和卷,以及与镜像仓库交互。
这意味着任何能够访问 Docker 套接字的人实际上都拥有主机的 root 权限,因为他们可以启动特权容器、挂载任意文件系统并修改关键配置。守护进程实际上变成了一个单点故障。
Podman 的诞生理念是成为一个尽可能“原生”于 Linux 的无守护进程容器引擎。它没有永久的中央进程;容器由用户或 systemd 控制,并且与 systemd 的集成允许通过服务单元来控制容器。
这种方法更符合 Unix 的“每个进程都属于启动它的用户”的模式,避免了依赖高权限守护进程。此外,Podman 还兼容 Docker 命令,简化了迁移过程(您甚至可以在不想安装 Docker CE 的环境中为 `docker=podman` 设置别名)。
无根模式和用户命名空间
降低容器逃逸潜在影响的一项重大实践进展是将无根模式与用户命名空间相结合。它旨在回答的问题是:“如果容器真的逃逸了会发生什么?”
用户命名空间允许您将容器的 UID 0 重新映射到主机上的非特权 UID。换句话说,在容器内部,应用程序认为自己是 root 用户,但从主机的角度来看,该进程实际上是一个具有已分配子 UID/子组 ID 范围的普通用户。
这样一来,即使攻击者在容器内获得了 root 权限并成功链接了内核或运行时漏洞利用程序,返回主机后,他们也只能以非特权用户的身份进行操作。他们无法覆盖 root 二进制文件、挂载敏感文件系统,也无法在缺乏必要权限的情况下操作设备。
Podman 从一开始就以这种模型为设计理念:尽可能默认使用无根容器化,即使没有 root 权限也使用 SELinux/AppArmor 和 cgroups,并着重最小化结构权限。
Docker 现在也提供了真正的无根模式,在这种模式下,dockerd 和容器都位于用户命名空间中,这与旧版的 userns-remap 不同,旧版的 userns-remap 中守护进程仍然以 root 用户身份运行。从安全角度来看,这可以防止针对 dockerd 的漏洞利用自动授予主机 root 权限。
使用 Podman 的容器网络:桥接、主机、macvlan 和无根接口
容器网络层是安全性和实际运行的另一个重要因素。Podman 提供了多种连接机制,尤其关注无根部署场景。
Podman 4 引入了netavark,这是一个遵循 CNI 模型的网络驱动程序,用于为容器提供桥接网络、macvlan 等上的 IP 地址。有了 netavark,rootful 容器通常默认连接到名为“podman”的网络,该网络与主机上的 Linux 网桥 (podman0) 相关联,地址为 10.88.0.0/16。
此默认桥接网络通过 SNAT 规则提供基本的互联网连接,并允许您使用 `-po --publish` 将容器端口暴露给主机,该命令会在 iptables/nftables 中生成 DNAT 规则。为了与 Docker 兼容,此网络不运行内部 DNS 服务器。
我们还可以创建用户自定义桥接网络,将容器组彼此隔离并提供内部 DNS。这使得容器能够在私有网络内按名称解析,更容易分离不应相互访问的服务,并能更好地控制 MTU、防火墙和其他功能。
在更高级的环境中,可以使用macvlan 或 ipvlan 网络将容器直接连接到主机的物理网络。macvlan 允许容器之间相互通信,而 ipvlan 则倾向于更好地隔离容器;在部署过程中,如果每个容器都需要被网络中的其他设备视为一个真正的主机,那么这些选项就非常有用。
无根网络:slirp4netns 和 pasta
当用户不是 root 用户时,情况会变得复杂:没有权限的用户无法自由修改全局网络命名空间或创建任意接口,因此 Podman 必须求助于用户空间解决方案来提供连接。
这时就需要用到 slirp4netns 了,它是 Podman rootless 使用的经典机制。该项目在容器内创建一个隔离的网络环境,并使用内核的 slirp 模块执行地址转换 (NAT),从而允许容器通过主机网络访问互联网。
实际上,slirp4netns会在容器的网络命名空间内创建一个 TAP 接口(例如,IP 地址为 10.0.2.100、网关为 10.0.2.2 的 tap0),并通过用户空间中实现的 TCP/IP 协议栈路由流量。虽然这种方法可行,但会增加一些开销并存在一些限制。
其中一项限制是,非特权用户不能使用 1024 以下的端口。sysctl参数 net.ipv4.ip_unprivileged_port_start 标记了用户被视为“非特权”的端口(默认情况下为 1024),尽管如果安全模型允许,可以对其进行仔细调整。
在 Podman 5 中,slirp4netns 被pasta 取代,成为默认的无根机制。Pasta 也完全在用户空间运行,但由于采用了 tap 接口和零拷贝技术,它实现了接近原生网络的性能,与 slirp4netns 相比,显著降低了对延迟和吞吐量的影响。
在无根容器中桥接和托管网络
即使在非 root 模式下,我们也可以使用 `--network=podman`将无 root 容器连接到默认的“podman”桥接网络。这利用了 netavark 的功能,并使用 `-p` 将端口映射到主机,就像在 root 模式下一样。
在无根环境中也可以创建用户自定义的桥接网络。在这种情况下,桥接网络和相关设备不会创建在主机的全局网络命名空间中,而是创建在用户的命名空间中,因此这些接口无法通过简单的 `sudo ip a` 命令从主机上看到。
为了检查这些无根网桥,Podman 提供了命令`podman unshare --rootless-netns`,该命令会在用户的网络命名空间中打开一个 shell。通过该 shell,可以观察内部网桥,并验证与容器的直接连接。
这种隔离产生了一种奇特的效应: 主机与这些无根容器的 IP 地址之间可能没有直接连接。但它确实可以与映射到主机自身 IP 地址的端口一起使用,这些端口充当服务的入口点。
对于容器之间的通信,集成到用户自定义桥接网络中的 DNS 服务器允许它们按名称进行通信,这大大简化了分布式应用程序的配置。
另一端是主机网络(--network=host),容器与主机共享网络栈,但没有自己的 IP 地址。在这种模式下,如果一个无根容器在 8080/tcp 端口启动了一个服务器,该服务将直接在主机的 8080 端口上可用,这样就省去了端口映射,但降低了隔离性。
Podman桌面、映像工具和本地编排
对于那些更喜欢可视化界面的用户来说,Podman Desktop 提供了一个图形化的环境,用于管理开发机器上的容器、Pod、镜像和网络配置,它可作为 Docker Desktop 的直接替代方案,但依赖于无守护进程引擎。
Podman使用 `podman pull` 命令获取镜像,该命令会从 `/etc/containers/registries.conf` 文件中定义的镜像仓库下载镜像。如果指定镜像时未指定镜像仓库名称,Podman 会尝试在配置的镜像仓库顺序中查找该镜像,默认情况下使用 `latest` 标签。
除了 Podman CLI 之外,我们还可以使用skopeo 作为专用工具来管理、检查镜像以及在镜像仓库和本地仓库之间复制镜像。诸如 copy、delete 和 sync 之类的命令允许我们自动执行仓库之间的同步,将镜像标记为需要垃圾回收,以及在不同环境之间移动镜像文件。
镜像下载完成后,可以使用 `podman run` 命令启动容器,只需指定传输方式和路径即可(默认情况下使用 Docker 传输,并在已配置的镜像仓库中搜索)。常用的选项(-d、-p、--name、--pod 等)既适用于实验室环境,也适用于更正式的部署。
日常管理使用`podman ps` 列出容器,`podman stop/start` 停止或启动现有实例,以及 `podman rm` 删除已停止的容器。如果容器已被修改,并且我们想要将其转换为新镜像,则可以使用 `podman commit` 将这些更改保存到新名称中。
使用 Podman 的 Pod 和高级执行模型
受 Kubernetes 的启发,Podman 允许你将容器分组到共享同一网络栈和特定资源的 Pod 中。这种模式非常适合数据库及其客户端,或者需要低延迟通信的多个微服务等场景。
命令`podman pod create` 创建一个新的 pod并返回其 ID,但默认情况下,pod 会保持停止状态,直到关联的容器启动或使用 `podman pod start` 显式启动它。
要查看系统上有哪些 Pod,请使用`podman pod ls` 命令,该命令还会列出与每个 Pod 关联的 INFRA 容器。这个特殊的容器负责维护 Pod 的网络和其他共享资源,因此容器的数量永远不会为零。
Podman 提供了启动、停止或重启整个 pod 的特定命令(podman pod start/stop/restart),但我们也可以只对 pod 内的特定容器进行操作,而不修改其余部分,从而在应用程序级别保持进程隔离。
要向现有 Pod 添加更多容器,请使用`podman run --pod Pod_NAME` 命令,语法与添加单个容器相同。您无法在不销毁容器的情况下将其从 Pod 中移除,因为 Pod 成员资格与其生命周期绑定。
使用podman ps –pod 或特定命令来监控所有 pod 中所有容器的进程,快速识别哪些属于哪个组以及服务是如何分布的。
容器、LXC 和应用程序虚拟化
尽管 Docker 和 Podman 占据了讨论的主导地位,但它们并非 GNU/Linux 中唯一的容器化选项。LXC/LXD、systemd-nspawn 和 Kata Containers 等解决方案从略有不同的角度解决了这个问题。
LXC 提供非常轻量级的系统容器,每个容器都有自己的主机名、IP 地址、文件系统和完整的初始化系统。结合作为容器和虚拟机管理程序的 LXD,它能够提供接近多台轻量级机器的体验,并具备接近裸机的性能。
Docker/Podman 专注于符合 OCI 标准的应用程序容器,而 LXC 在需要具有多个服务和进程的持久环境时表现出色,其行为类似于虚拟机,但开销更小,并且与 Linux 内核的集成非常自然。
从性能角度来看,LXC 可以运行数据密集型企业应用程序,几乎没有任何性能损失,因此对于对延迟敏感且不需要传统虚拟机强隔离性的工作负载来说,它是一个强大的选择。
与此同时,面向微型虚拟机和增强隔离性的技术也越来越受欢迎,例如 Kata Containers,它将每个 pod 启动在一个拥有独立内核的小型虚拟机中;又如 gVisor,它在应用程序和 Linux 之间插入一个用 Go 语言编写的“内核沙箱”。两者都降低了对共享内核的依赖,并在保持容器用户友好性的同时,更接近虚拟机的安全模型。
强化 Docker 和 Podman 的最佳实践
综上所述,构建一个强大的容器平台的合理方法是分层工作:使用命名空间/cgroups进行分段,使用seccomp和MAC进行过滤策略,并通过无根模式进行结构化权限限制。
在实践中,建议尽可能采用以非 root 用户身份运行容器的标准做法,尤其是在多租户环境或共享云环境中。Podman 默认支持这种做法,而 Docker rootless 在需要兼容性时也遵循相同的理念。
主机内核必须及时更新安全补丁。诸如 Dirty COW、Dirty Pipe 等漏洞都需要通过更新来解决,如果我们不想让容器逃逸有机可乘,就必须尽快应用这些更新。
另一种非常有效的措施是使用不可变的、面向容器的发行版,例如 openSUSE MicroOS 或 Flatcar Linux。它们的只读 root 权限和原子更新系统大大减少了攻击面,并且如果打补丁后出现问题,也便于回滚。
关于配置:生产环境中除极少数严格控制的情况外,不应使用特权容器;不要在容器内以 root 用户身份运行应用程序;尽可能使用只读文件系统;并将功能限制在最低限度。SELinux/AppArmor 和针对应用程序实际调用定制的 seccomp 配置几乎是必需的。
最后,我们不能忽视运行时监控的重要性。像 Falco、Sysdig Secure 或 Aqua 这样的工具可以帮助检测异常情况、逃逸尝试(例如对 /proc 或 /sys 的异常访问)、非典型网络流量或可疑命令,从而为攻击发生前的反应争取时间。
通过将传统虚拟化技术与 KVM、Podman 管理的容器(最好采用无根模式)、分段良好的网络以及强大的安全加固措施相结合,可以构建一个高度灵活且相当安全的虚拟化平台。理解共享内核是关键所在,在风险或监管合规要求时依赖虚拟机或微型虚拟机,并精心管理镜像供应链,是区分实验室环境和生产就绪型基础设施的关键所在。