本文概述 Cloudflare 安全架构,并解答两个常见问题:V8 漏洞和 Spectre。
自 Workers 项目启动以来,安全一直是高优先级——早期就存在担忧:在共享基础设施上托管大量租户时,各种类型的侧信道可能构成威胁。Cloudflare Workers 运行时经过精心设计,以防御侧信道攻击。
为此,Workers 的设计使代码无法在本地测量自身执行时间。例如,Date.now() 返回的值在代码执行期间被锁定。不提供其他计时器。此外,Cloudflare 不提供对并发(例如多线程)的访问,因为攻击者可能利用它构建临时计时器。这些设计选择无法追溯性地引入其他平台——例如 Web 浏览器——因为它们移除了现有应用所依赖的 API。Workers 之所以能做到这一点,是因为从一开始的运行时就做出了这些设计选择。
虽然这些早期设计决策已被证明有效,Cloudflare 仍在继续增加纵深防御,包括通过重新调度 Workers 来创建额外隔离层,以隔离可疑 Workers 与高价值 Workers。
Workers 的方法与行业大多数做法截然不同。它能抵御整个 Spectre 类攻击 ↗范围,无需对每个攻击单独关注,也无需阻止一般推测。然而,由于 Workers 方法不同,需要仔细研究。Cloudflare 目前正与格拉茨技术大学(TU Graz)的研究人员合作研究已完成的工作。这些研究人员包括最初发现 Spectre 的部分人员。Cloudflare 将在研究结果可用时发布。
如需更多详情,请参阅 Cloudflare Workers 架构师 Kenton Varda 的演讲 ↗。Spectre 相关内容在末尾附近。
首先简要概述 Workers 运行时架构:
设计代码沙箱有两个基本部分:安全隔离和 API 设计。
首先,需要创建一个安全的执行环境,使代码无法访问其不应访问的任何内容。
为此,主要工具是 V8——Google 为 Chrome 开发的 JavaScript 引擎。V8 在 isolates 内执行代码,防止该代码访问 isolate 之外的内存——即使在同一进程内。重要的是,这意味着 Cloudflare 可以在单个进程中运行多个 isolates。这对于像 Workers 这样的边缘计算平台至关重要,Cloudflare 必须在每台机器上托管数千个 guest 应用,并以最小开销在这些 guest 之间每秒切换数千次。如果 Cloudflare 必须为每个 guest 运行单独的进程,Cloudflare 能支持的租户数量将大幅减少,边缘计算将仅限于少数大型企业客户。借助 isolate 技术,Cloudflare 可以让边缘计算面向所有人。
不过,有时 Cloudflare 确实决定将 Worker 调度到其自己的私有进程中。当 Worker 使用需要额外隔离层的某些功能时,Cloudflare 会这样做。例如,当开发者使用 devtools 调试器检查 Worker 时,Cloudflare 会在单独进程中运行该 Worker。这是因为在浏览器中,inspector 协议历来仅可供浏览器的受信任操作员使用,因此未像 V8 其余部分那样受到严格的安全审查。为对冲 inspector 协议中增加的风险,Cloudflare 将被检查的 Workers 移至具有进程级沙箱的单独进程。Cloudflare 还使用进程隔离作为针对 Spectre 的额外防御。
此外,即使在与其他 isolates 共享进程的 isolates 中,Cloudflare 也在每台机器上运行多个完整运行时实例,称为 cordons。Workers 通过为每个 Worker 分配信任级别并将低信任 Workers 与更受信任的 Workers 分离,在 cordons 之间分配。此机制运作的一个例子:注册 Free 计划的客户不会被调度到与 Enterprise 客户相同的进程中。这在 V8 发现零日安全漏洞的情况下提供了一些纵深防御。
在整进程级别,Cloudflare 应用另一层沙箱以实现纵深防御。第 2 层沙箱使用 Linux namespaces 和 seccomp 禁止所有对文件系统和网络的访问。Namespaces 和 seccomp 通常用于实现容器。然而,Cloudflare 对这些技术的使用比容器引擎通常可能的要严格得多,因为 Cloudflare 在进程启动之后、任何 isolates 加载之前配置 namespaces 和 seccomp。这意味着,例如 Cloudflare 可以(并且确实)使用完全空的文件系统(mount namespace),并使用 seccomp 阻止所有与文件系统相关的系统调用。容器引擎通常无法禁止所有文件系统访问,因为这样做将无法使用 exec() 从磁盘启动 guest 程序。在 Workers 情况下,Cloudflare 的 guest 程序不是原生二进制文件,Workers 运行时本身在 Cloudflare 阻止文件系统访问之前已完成加载。
第 2 层沙箱还完全禁止网络访问。相反,进程被限制为仅通过本地 UNIX domain socket 与同一系统上的其他进程通信。任何与外界的通信必须由沙箱外的其他本地进程中介。
一个称为 supervisor 的进程负责从磁盘或其他内部服务获取 Worker 代码和配置。supervisor 确保沙箱进程无法读取与其应运行的 Workers 无关的任何配置。
例如,当沙箱进程收到其尚未见过的 Worker 请求时,该请求包括该 Worker 代码的加密密钥,包括附加的 secrets。沙箱可以将该密钥传递给 supervisor 以请求代码。沙箱无法请求其未收到相应密钥的任何 Worker。它无法枚举已知 Workers。它也无法请求不需要的配置;例如,它无法请求用于 Worker HTTPS 流量的 TLS 密钥。
除了读取配置之外,沙箱与系统上其他进程通信的另一个原因是实现暴露给 Workers 的 API。
有句谚语:如果一棵树在森林中倒下,但没有人听到,它会发出声音吗?Cloudflare 的谚语:如果 Worker 在完全隔离的环境中执行,完全被阻止与外界通信,它实际上在运行吗?
完全代码隔离实际上毫无用处。为使 Workers 做任何有用的事,它们必须被允许与用户通信。至少,Worker 需要能够接收请求并响应。为使 Workers 安全地向世界发送请求,需要 API。
在沙箱上下文中,API 设计承担了新的责任级别。Cloudflare API 精确定义 Worker 可以做什么和不可以做什么。Cloudflare 必须非常谨慎地设计每个 API,使其只能表达允许的操作,不能更多。例如,Cloudflare 希望允许 Workers 发出和接收 HTTP 请求,同时不允许它们访问本地文件系统或内部网络服务。
目前,Workers 不允许任何对本地文件系统的访问。因此,Cloudflare 根本不暴露文件系统 API。没有 API 意味着没有访问。
但是,想象一下如果 Workers 将来想要支持本地文件系统访问。如何实现?Workers 不应看到整个文件系统。想象一下,如果每个 Worker 在文件系统上有自己的私有目录,可以在其中存储任何内容。
为此,Workers 将使用基于基于能力的安全 ↗的设计。能力是一个大话题,但在这种情况下,这意味着 Cloudflare 将给予 Worker 一个 Directory 类型的对象,代表文件系统上的目录。此对象将具有允许创建和打开文件及子目录的 API,但不允许遍历上级目录。实际上,每个 Worker 将看到其私有 Directory,就好像它是自己文件系统的根目录。
这样的 API 如何实现?如上所述,沙箱进程无法访问真实文件系统。相反,文件访问将由 supervisor 进程中介。沙箱使用 Cap'n Proto RPC ↗(一种基于能力的 RPC 协议)与 supervisor 通信。(Cap'n Proto 是由 Cloudflare Workers 团队维护的开源项目。)此协议使实现基于能力的 API 非常容易,因此 Cloudflare 可以严格限制沙箱仅访问属于其正在运行的 Workers 的文件。
那么网络访问呢?今天,Workers 仅允许通过 HTTP 与世界通信——入站和出站。没有其他形式网络访问的 API,因此被禁止;不过,Cloudflare 计划将来支持其他协议。
如前所述,沙箱进程无法直接连接到网络。相反,所有出站 HTTP 请求通过 UNIX domain socket 发送到本地代理服务。该服务对请求实施限制。例如,它验证请求是发往公共 Internet 服务还是 Worker zone 自己的源站,而不是本地机器或网络上可能可见的内部服务。它还为每个请求添加 header,标识请求来源的 Worker,以便追踪和阻止滥用请求。一切就绪后,请求发送到 Cloudflare 网络的 HTTP 缓存层,然后发送到 Internet。
类似地,入站 HTTP 请求不会直接到达 Workers 运行时。它们首先由入站代理服务接收。该服务负责 TLS 终止(Workers 运行时从不看到 TLS 密钥),以及识别特定请求 URL 应运行的正确 Worker 脚本。一切就绪后,请求通过 UNIX domain socket 传递给沙箱进程。
每个非平凡的软件都有 bug,沙箱技术也不例外。虚拟机、容器和 isolates——Workers 使用的——也有 bug。
Workers 大量依赖 V8 提供的隔离,V8 是 Google 为 Chrome 构建的 JavaScript 引擎。这有利有弊。一方面,V8 是极其复杂的技术,比虚拟机创建更宽的攻击面。更多复杂性意味着更多出错机会。然而,由于 V8 可以说是世界上最流行的沙箱技术,大量精力用于查找和修复 V8 bug。Google 定期向发现 V8 沙箱逃逸的人支付五位数赏金。Google 还运营模糊测试基础设施,比大多数人类更快地自动发现 bug。Google 的投资在很大程度上最小化了 V8 零日漏洞的危险——被恶意行为者发现而 Google 不知道的 bug。
但是,发现 bug 并报告后会发生什么?V8 是开源的,因此安全 bug 的修复是公开开发的,并同时发布给所有人。重要的是,在任何补丁发布到生产环境之前,恶意行为者可能开发 exploit,因此必须尽快推出任何补丁。
发布修复与部署之间的时间称为 patch gap。Google 此前宣布 Chrome 的 patch gap 已从 33 天缩短到 15 天 ↗。
幸运的是,Cloudflare 直接控制 Workers 运行时运行的机器。几乎整个构建和发布过程都已自动化,因此 V8 补丁发布的时刻,Cloudflare 系统自动构建 Workers 运行时的新版本,并在必要(人工)审查者一键批准后,自动将该版本推送到生产环境。
因此,Workers 的 patch gap 现在不到 24 小时。V8 团队在慕尼黑工作时间发布的补丁通常会在美国工作时间结束前进入生产环境。
Google 的 V8 团队表示 V8 本身无法防御 Spectre ↗。Workers 不需要依赖 V8 来实现这一点。Workers 环境提供了许多替代方法来缓解 Spectre。
Spectre 是一类攻击,恶意程序可以欺骗 CPU 推测性地使用程序不应访问的数据进行计算。CPU 最终发现问题,不允许程序看到推测性计算的结果。然而,程序可能通过观察计算的微妙副作用(例如对 cache 的影响)来推导秘密数据的位。
有关 Spectre 的更多信息,请参阅 Learning Center 上的专题页面 ↗。
Spectre 涵盖现代 CPU 中存在的各种漏洞。具体漏洞因架构和型号而异,很可能存在许多尚未发现的漏洞。
这些漏洞对每个云计算平台都是问题。每当有多于一个租户在同一机器上运行代码时,Spectre 攻击都是可能的。然而,租户越接近,缓解特定漏洞可能越困难。许多已知问题可以在内核级别(保护进程彼此)或 hypervisor 级别(保护 VM)缓解,通常借助 CPU 微码更新和各种防御(其中许多可能带来严重的性能影响)。
在 Cloudflare Workers 中,租户使用 V8 isolates 彼此隔离——而非进程或 VM。这意味着 Workers 不一定能依赖 OS 或 hypervisor 补丁来防止 Spectre。Workers 需要自己的策略。
Cloudflare Workers 设计为在每个 Cloudflare 位置运行您的代码。
Workers 设计为面向所有人的平台。它需要处理大量租户,其中许多租户流量很少。
结合这两点,规划变得困难。
典型的非边缘 serverless 提供商可以通过将该租户的所有流量发送到单台机器来处理低流量租户,因此只需加载一份应用副本。如果机器可以处理,比如说,十几个租户,那就足够了。该机器可以托管在拥有数百万台机器的大型数据中心,实现规模经济。然而,当用户不在附近时,这种集中化会产生延迟和全球带宽成本。
另一方面,使用 Workers,每个租户,无论流量水平,目前都在每个 Cloudflare 位置运行。在尽可能接近最终用户的过程中,Cloudflare 有时选择只有有限机器空间的位置。净结果是 Cloudflare 需要能够在每台机器上托管数千个活跃租户,并能够按需快速启动非活跃租户。这意味着每个 guest 不能超过几兆字节内存——连 call stack 的空间都不够,更不用说进程所需的其他一切。
此外,Cloudflare 需要上下文切换在计算上高效。内存中的许多 Workers 只会偶尔处理一个事件,许多 Workers 在任何特定事件上花费不到一毫秒。在这种环境中,单个核心可以轻松发现自己每秒在数千个不同租户之间切换。要处理一个事件,guest 应用与其 host 之间需要大量通信,意味着更多的切换和通信开销。如果每个租户生活在自己的进程中,所有这些开销比许多租户生活在单个进程中要大几个数量级。在 Workers 中使用严格进程隔离时,CPU 成本可能达到共享进程的 10 倍。
为保持 Workers 低成本、快速且面向所有人,Cloudflare 需要找到在单个进程中托管多个租户的方法。
Spectre 没有官方解决方案。即使使用重量级虚拟机也是如此。每个人都仍然容易受到攻击。
行业遇到新的 Spectre 攻击。每隔几个月,研究人员发现新的 Spectre 漏洞,CPU 供应商发布新的微码,OS 供应商发布内核补丁。每个人都必须继续更新。
但仅仅部署最新补丁是否足够?
更多漏洞存在但尚未公开。要防御 Spectre,Cloudflare 需要采取不同的方法。阻止个别已知漏洞是不够的。相反,必须一次性解决整个漏洞类别。
不太可能找到 Spectre 的全面修复。然而,以下思想实验提出了需要考虑的观点:
从根本上说,所有 Spectre 漏洞都使用侧信道来检测隐藏的处理器状态。侧信道,根据定义,涉及观察系统的某些非确定性行为。方便的是,大多数软件执行环境努力消除非确定性,因为非确定性执行使应用不可靠。
然而,仍有几种非确定性相当常见。其中最明显的是计时。行业很久以前就放弃了程序每次运行应花费相同时间的想法,因为确定性计时与启发式性能优化从根本上相冲突。大多数 Spectre 攻击专注于计时,以此检测 CPU 隐藏的微架构状态。
有人提议可以通过使计时器不准确或添加随机噪声来解决。然而,事实证明这不会阻止攻击;只会使其变慢。如果计时器以任何方式跟踪真实时间,那么您为使其不准确所做的任何事情都可以通过多次运行攻击并使用统计过滤不一致性来克服。
许多安全研究人员将此视为故事的结尾。如果攻击仍然可能,减慢攻击有什么好处?
然而,减慢攻击的措施可能非常有效。
关键洞察是:随着攻击变慢,新技术变得可行,以使其进一步变慢。因此,目标是链接足够多的技术,使攻击慢到无意义。
毕竟,密码学的大部分内容在技术上容易受到暴力破解攻击——从技术上讲,有足够的时间,您可以破解它。但当所需时间是数千年(甚至数十亿年)时,这就是充分的防御。
可以做些什么将 Spectre 攻击减慢到毫无意义的程度?
Workers 不允许客户上传原生代码二进制文件在 Cloudflare 网络上运行——仅 JavaScript 和 WebAssembly。许多其他语言,如 Python、Rust 甚至 Cobol,可以编译或转译为这两种格式之一。两者都通过 V8 将这些格式转换为真正的原生代码。
这本身不一定使 Spectre 攻击更难。然而,这作为步骤 0 呈现,因为它是启用以下步骤的基础。
接受原生代码程序意味着受制于现有 CPU 架构(通常是 x86)。为了以合理性能执行代码,通常需要在真实硬件上直接运行代码,严重限制 host 对执行过程的控制。例如,内核或 hypervisor 无法禁止应用调用 CLFLUSH 指令,该指令在侧信道攻击中有用 ↗,几乎别无他用。
此外,支持原生代码通常意味着支持整个现有操作系统和软件栈,它们带来关于架构如何在它们之下工作的数十年期望。例如,x86 CPU 允许内核或 hypervisor 禁用 RDTSC 指令,该指令读取高精度计时器。现实地说,禁用它会破坏许多程序,因为它们在需要知道当前时间时会使用 RDTSC。
支持原生代码会限制未来缓解技术的选择。使用抽象中间格式有更大的自由度。
在 Workers 中,您可以使用 JavaScript Date API 通过调用 Date.now() 获取当前时间。然而,返回的时间值不是当前时间。Date.now() 返回最后一次 I/O 的时间。它在代码执行期间不会前进。例如,如果攻击者编写:
let start = Date.now();
for (let i = 0; i < 1e6; i++) {
doSpectreAttack();
}
let end = Date.now();start 和 end 的值将始终完全相同。攻击者无法使用 Date 测量其代码的执行时间,而这是他们进行攻击所需的。
类似地,Workers 中不允许多线程和共享内存。与处理一个事件相关的一切都在同一线程上发生。否则,可以通过竞态线程来猜测和检查底层计时器。不允许多个 Workers 并发处理同一请求。例如,如果您在 zone 上安装了使用 Workers 实现的 Cloudflare App,且您的 zone 本身也使用 Workers,则对您 zone 的请求可能实际上由两个 Workers 顺序处理。这些在同一线程中运行。
此时,本地测量代码执行时间已被阻止。然而,仍可以远程测量。例如,触发 Worker 执行的 HTTP 客户端可以测量 Worker 响应所需的时间。这样的测量可能非常嘈杂,因为它必须穿越 Internet 并产生一般网络成本。理论上,可以通过多次执行攻击并取平均值来克服这种噪声。
在对抗性测试和顶尖 Spectre 专家的帮助下,Cloudflare 未能开发出在生产环境中有效的远程计时攻击。然而,缺乏可行的攻击并不意味着 Workers 应停止构建防御。相反,Workers 团队目前正在测试一些更高级的措施。
如果攻击有可能成功,运行需要很长时间——至少数小时,也许长达数周。但一旦攻击运行哪怕一秒钟,就有大量新数据可用于触发进一步措施。
Spectre 攻击表现出异常行为,正常程序通常不会看到。这些攻击故意尝试创建病态性能场景以放大微架构效果。当攻击已被迫在循环中运行数十亿次以克服上述其他缓解措施时,这尤其如此。这往往出现在 CPU 性能计数器等指标中。
现在,使用性能指标检测 Spectre 攻击的常见问题是有时候会出现误报。有时合法程序表现不佳。运行时无法关闭每个性能不佳的应用。
相反,运行时选择将任何具有可疑性能指标的 Worker 重新调度到其自己的进程中。如上所述,运行时无法对每个 Worker 都这样做,因为开销会太高。然而,将少数 Worker 进程隔离作为防御机制是可以接受的。如果 Worker 是合法的,它将继续运行,只是开销稍多。幸运的是,Cloudflare 基本上可以随时将 Worker 迁移到其自己的进程中。
事实上,这里可能甚至不需要基于性能计数器的复杂触发机制。如果 Worker 每个事件使用大量 CPU 时间,将其隔离在单独进程中的开销相对较小,因为它切换上下文的频率较低。因此,运行时不妨对任何 CPU 密集型 Worker 使用进程隔离。
一旦 Worker 被隔离,Cloudflare 可以依赖操作系统的 Spectre 防御,如大多数桌面 Web 浏览器所做的。
Cloudflare 一直与 Graz Technical University 的专家合作开发此方法。TU Graz 的团队共同发现了 Spectre 本身,并负责此后大量后续发现。Cloudflare 已开发动态隔离 Workers 的能力,并确定了可靠检测攻击的指标。
如前所述,进程隔离不是完整的防御。随着时间推移,Spectre 攻击往往执行更慢,这意味着 Cloudflare 有能力合理猜测和识别恶意行为者。隔离进程进一步减慢潜在攻击。
此时,所有已知攻击已被阻止。这使 Workers 容易受到未来未知攻击,与所有其他基于 CPU 的系统一样。然而,所有新攻击通常都会非常慢,需要数天或更长时间,给 Cloudflare 时间准备防御。
例如,每天重启整个 Workers 运行时是合理的。这将重置内存中所有内容的位置,迫使攻击重新开始发现密钥位置的过程。Cloudflare 还可以跨物理机器或 cordons 重新调度 Workers,因此攻击任何特定邻居的窗口是有限的。
总的来说,因为 Workers 在根本上可抢占(与 containers 或 VM 不同),Cloudflare 有大量自由度来挫败攻击。
Cloudflare 将此视为持续投资——这项工作永远不会真正结束。