博客

哪些客户真的需要自己的 VM?分层 Docker 隔离方案

为每个客户都提供 VM 是过度的。以下是如何决定每个租户需要多少隔离,并自动化这个决策。

摘要

当客户问起他们的数据与其他租户的隔离程度时,代理商常常会感到恐慌。Docker 的命名空间和 cgroups 提供了真正的隔离,但它们并不等同于硬件边界。与其为每个客户都运行一台 VM——或者更糟,对所有客户一视同仁——不如建立一小套隔离层级,并根据数据敏感性、信任度和合规要求将每个客户匹配到相应的层级。一个加固的容器(非 root、丢弃 capabilities、seccomp、只读根文件系统)可以覆盖大多数网站;受监管或恶意负载则使用 VM 或容器-在-VM 中的混合方案。这篇文章提供了一个可重复的决策流程、一个对比表,以及对于何时更多隔离是过度设计的诚实看法。

你是否正处于销售电话中的某个时刻,新客户说“我们是医疗机构,给我看看我们的数据与你的其他客户是隔离的”,而你宁愿谈论其他任何事情?

这就是代理商的问题:不是要有一个完美的部署,而是要在十几个预算、风险概况和合规要求各不相同的客户中重复相同的可靠部署。这是诚实的版本。Docker 隔离是真实的,但它是具体的。命名空间为每个容器提供自己的进程、网络和文件系统视图;cgroups 限制 CPU、内存和磁盘 I/O,使租户无法相互饿死。但这并不会给你带来容器与宿主机内核之间的硬件隔离墙。如果攻击者逃出容器,他们就进入了你唯一的那个内核。本文的其余部分将这个令人不安的事实变成一个可重复的决策:根据数据敏感性和信任度对每个客户进行分类,应用基线加固配置,并且只有当泄露的成本高于 VM 的成本时,才选择使用 VM。

等等,容器不是已经隔离了吗?

Docker 运行在 Linux 命名空间和 cgroups 之上,这些关键词在做实际工作。命名空间将进程 ID、网络栈、挂载点和用户分开,使得一个容器中的进程看不到另一个容器的进程表。cgroups 设置限制:给一个容器 0.5 CPU、512 MB 内存和固定的块 I/O 权重,它就会得到这些。一个失控的循环在一个租户中会被限制,而不是拖垮邻居。如果你没有配置限制,你就跳过了 cgroups 最基本的用途。

以容器 A 中一个简单的 PHP 应用为例。它看到自己的文件系统、自己的网络接口、自己的 PID 1。容器 B 也有同样的情况,但视图不同。这就是命名空间。现在走开并跳过内存限制:容器 A 可以填满宿主机的 RAM,使容器 B 变得缓慢。这就是 cgroups 存在的原因。但两个容器可以通过命名空间彼此隔离,但仍然共享宿主机内核,这就是每个容器逃逸故事所涉及的部分。一个到达内核的漏洞利用可能到达该宿主机上的每个租户。

“Docker 是隔离的”这句话只说对了一半。准确的版本是“Docker 使用命名空间和 cgroups 进行隔离,而内核漏洞就是爆炸半径。”在你信任一个租户运行不受信任的代码之前,先仔细想想这句话。答案不是“绝不要使用容器”——那是简单的恐慌。答案是分层系统。

那么为什么有些客户需要的不只是命名空间?

诚实的答案是,隔离不是一个开关,而是一个频谱。在一端,你有一个完全共享的容器,每个人实际上都在一个应用中。在另一端,你为每个租户提供一台独立的 VM,拥有自己的内核。大多数代理商的工作处于令人不安的中间地带,而这个中间地带并不是“Docker 没问题”和“为所有人运行 VM”之间的二元选择。

把客户推向右侧的不是他们的规模,而是四个问题:

  • 他们是否存储受监管数据?健康记录、支付卡详细信息,以及任何监管机构会称之为敏感的信息。
  • 他们租户的泄露是否有切实的路径通向另一个租户?如果他们能运行任意代码,那么是的。
  • 你是否信任代码和部署它的人?雇佣最便宜的自由职业者的客户,与你了解其开发团队的客户,信任度是不同的。
  • 他们的合同是否写明“专用”、“隔离”或“私有”?如果是,你已经承诺了一个层级;现在唯一的工作是选择正确的层级。

如果你还无法回答这些问题,就把客户放在一个基线层级,并写下假设。这不是安全审计;这是你在每次入职时重复进行的合理性检查。

如何在每次不运行安全审计的情况下为每个客户做决定?

制作一个小表格并坚持下去。你不需要一个有四十个单元格的矩阵。四个层级就足以覆盖代理商遇到的几乎所有客户。

客户层级真正将什么分开使用时机
层级 1:共享应用/容器仅应用逻辑内部工具、低风险数据、每个人都明确处于同一登录系统的项目
层级 2:同一宿主机,独立容器命名空间和 cgroups大多数营销网站、联系表单、无敏感数据
层级 3:加固的容器层级 2 + 非 root、丢弃 capabilities、seccomp、只读根文件系统、网络分段电子商务、PII、你不完全信任的自定义代码
层级 4:每租户一台 VM虚拟机监控器和独立内核医疗、金融、合规文书、不受信任的代码、吵闹的邻居

以下是它在实践中的表现。一家有联系表单和 Instagram 链接的面包店客户进入层级 2:共享宿主机上的一个容器,默认 Docker 网络,资源限制,完成。一个存储客户姓名、地址和支付重定向的在线商店进入层级 3:相同的共享宿主机,但容器以非 root 用户运行,没有额外的内核 capabilities,使用 seccomp 配置,并且只暴露 443 端口。一个存储受保护健康信息的医疗接收门户进入层级 4:每个租户一台 VM,因为泄露的成本不是“我们会清理干净”,而是“我们不能向客户展示我们认真对待他们了”。

整个诀窍在于,你不是为每个客户重新思考架构。你只是从已经达成一致的表格中选择一行。这就是为什么一个五人代理商可以运行一百个网站,而不需要一百个单独的安全狂热。这也意味着下一个客户不会得到一个取决于哪个团队成员接电话的答案。关于这些选择背后的更深层架构讨论,这篇关于设计多租户隔离级别的指南 更详细地介绍了权衡。

加固的容器到底长什么样?

我们停止说“加固”并具体来看。这就是层级 3 对典型的 WordPress 或 PHP 客户意味着什么。

首先,更换用户。大多数官方镜像默认仍以 root 运行;在你的 Dockerfile 中,创建一个非 root 用户并以该用户身份运行应用。这会立即消除容器沦陷变为宿主机沦陷的最常见方式。其次,丢弃你不需要的 capabilities。使用 --cap-drop ALL 运行,并且只加回一个,通常是 NET_BIND_SERVICE,以便应用可以监听 80 端口。仅这一步就是一个比大多数人预期更大的改变。第三,使用 --read-only 使根文件系统只读,并将可写目录(上传、数据库数据目录)挂载为卷或 tmpfs。第四,应用 seccomp 配置,如果宿主机支持,还要应用 AppArmor 或 SELinux。最后,将容器放在专用的 Docker 网络上,并且只暴露实际需要可达的端口。

让我们通过一个 WordPress 示例来说明。基础镜像可能以 root 运行,所以你添加一个 useradd 步骤和一个 USER 指令。你使用内存限制和 CPU 限制运行容器,这样插件流量的突发不会伤害邻居。你将 /var/www/html/wp-content/uploads 挂载为可写卷。你设置 --read-only。你将它附加到一个附近没有任何 --privileged 标志的网络。结果是一个过去是“一个 WordPress 站点”的容器,现在变成了“一个碰巧比大多数虚拟专用服务器更加加固的 WordPress 站点。”

如果手动完成所有这些感觉脆弱,还有一条更容易的中间路径:Docker 的增强型容器隔离,它使用用户命名空间隔离和安全容器运行时。这是一个合理的捷径,但它不是跳过非 root 或丢弃 capabilities 的免费通行证。租户仍然需要合理的镜像。区别在于,面向内核的攻击面变得更小,而你不必一夜之间成为 seccomp 专家。如果你想要单个租户的确切顺序,逐步隔离加固指南 将这一部分变成了可复制粘贴的命令。

我什么时候停止层层叠加,直接给他们一台 VM?

这里是反直觉的部分:更多的隔离并不自动意味着更好。VM 为你提供硬件级隔离、独立的内核,以及如果客户机内核沦陷时小得多的攻击面。这正是医疗和金融客户在说“我们想要被隔离”时所期望的。但每台 VM 都会增加修补、备份和计算成本,并且使保持整个机群更新的工作量成倍增加。如果你因为一个客户曾告诉你 Docker 让他们害怕而给每个客户都上一台 VM,你就是用真金白银买了一场安全表演。

当每个租户的风险高于每租户 VM 的运营成本时,VM 是正确的答案。这意味着受监管数据、书面的合规要求、不受信任的第三方代码,或者一个需要移除吵闹邻居的客户。当客户的合同字面上承诺专用环境时,这也是正确的答案,因为当他们签署“专用”时,他们想象的并不是“容器”。

但 VM 并不能为粗糙的容器开脱。一个常见的陷阱是把客户放在 VM 里,然后因为“VM 保护他们”而跳过加固。VM 保护宿主机免受租户的侵害,而不是保护租户免受自己糟糕镜像的侵害。你仍然希望 VM 内部是非 root、丢弃 capabilities 和 seccomp。混合方法——VM 内的容器——通常是甜点区:VM 为合规对话提供边界,而容器为你提供已经熟悉的部署工作流。关于这个争论的更详细版本见每个租户都应该有自己的 VM 吗?,但简短的回答是,VM 是为了合同,而不是为了恐惧。

我如何在每个客户中使这个可重复?

你通过将分层系统作为模板而不是记忆来使其可重复。保留一个 Compose 文件目录,每个层级一个:tier2-baselinetier3-lockedtier4-vm-hybrid。当新客户出现时,复制模板,更改环境变量,你在编写一行新基础设施之前就已经知道了隔离的形状。

然后写下决定。不是一份 400 页的安全报告,而是在客户存储库中写一段简短的段落:他们存储什么数据,他们处于哪个层级,为什么,以及什么会使他们升级。这一段比一百条防火墙规则更有价值,因为它是你可以展示给下一个审计员或下一个担忧客户的東西。它也让你不必在最初的销售电话淡去后,还要记住为什么面包店得到层级 2,而电子商务商店得到层级 3。

自动化那些无聊的检查。让你的 CI 扫描每个客户镜像,如果它以 root 运行、拥有所有 capabilities,或者试图发布层级允许之外的端口,就使构建失败。这些都不是稀奇的东西;这只是确保模板不会被一个善意的开发人员意外破坏。如果你无论如何都在构建周围的托管工作流,生产就绪的 Docker 托管策略文章 涵盖了容器定义之后的部分。

这些都不光鲜。没有一篇博文会让“租户隔离”听起来像绿地架构图那样令人兴奋。但这是一个代理商之间差异所在:一个用交叉手指回答“我们有多隔离?”为“完全隔离”,而另一个可以展示层级、配置和原因。容器不是魔法墙。VM 不是灵丹妙药。分层系统只是一个你写下并重复使用的决定——而对于代理商来说,可重复才是整个游戏。

Sources (5)