构建安全可靠的系统:第八章到第十章

217 阅读1小时+

第八章:面向弹性的设计

原文:8. Design for Resilience

译者:飞龙

协议:CC BY-NC-SA 4.0

维塔利·希皮茨因、米奇·阿德勒、佐尔坦·埃吉德和保罗·布兰金希普

与耶稣·克利门特、杰西·杨、道格拉斯·科利什和克里斯托夫·科恩一起

作为系统设计的一部分,“弹性”描述了系统抵抗重大故障或中断的能力。具有弹性的系统可以自动从系统部分的故障中恢复,甚至可能是整个系统的故障,并在问题得到解决后恢复正常运行。弹性系统中的服务理想情况下在事故期间始终保持运行,可能是以降级模式。在系统设计的每一层中设计弹性有助于防御系统不可预期的故障和攻击场景。

为弹性设计系统与为恢复设计系统有所不同(在第九章中深入讨论)。弹性与恢复密切相关,但是恢复侧重于在系统发生故障后修复系统的能力,而弹性是关于设计延迟承受故障的系统。注重弹性和恢复的系统更能够从故障中恢复,并且需要最少的人为干预。

弹性设计原则

系统的弹性属性建立在本书第 II 部分中讨论的设计原则之上。为了评估系统的弹性,您必须对系统的设计和构建有很好的了解。您需要与本书中涵盖的其他设计特性密切配合——最小特权、可理解性、适应性和恢复——以加强系统的稳定性和弹性属性。

以下方法是本章深入探讨的,它们表征了一个具有弹性的系统:

  • 设计系统的每一层都具有独立的弹性。这种方法在每一层中构建了深度防御。

  • 优先考虑每个功能并计算其成本,以便了解哪些功能足够关键,可以尝试在系统承受多大负载时维持,哪些功能不那么重要,可以在出现问题或资源受限时进行限制或禁用。然后确定在哪里最有效地应用系统有限的资源,以及如何最大化系统的服务能力。

  • 将系统分隔成清晰定义的边界,以促进隔离功能部分的独立性。这样,也更容易构建互补的防御行为。

  • 使用隔舱冗余来防御局部故障。对于全局故障,一些隔舱提供不同的可靠性和安全性属性。

  • 通过自动化尽可能多的弹性措施来减少系统的反应时间。努力发现可能受益于新自动化或改进现有自动化的新故障模式。

  • 通过验证系统的弹性属性来保持系统的有效性——包括其自动响应和系统的其他弹性属性。

深度防御

深度防御通过建立多层防御边界来保护系统。因此,攻击者对系统的可见性有限,成功利用更难发动。

特洛伊木马

特洛伊木马的故事,由维吉尔在《埃涅阿斯纪》中讲述,是一个关于不足防御危险的警示故事。在围困特洛伊城十年无果之后,希腊军队建造了一匹巨大的木马,作为礼物送给特洛伊人。木马被带进特洛伊城墙内,藏在木马里的攻击者突然冲出来,从内部利用了城市的防御,然后打开城门让整个希腊军队进入,摧毁了城市。

想象一下,如果这个城市计划了深度防御,这个故事的结局会是什么样子。首先,特洛伊的防御力量可能会更仔细地检查特洛伊木马并发现欺骗。如果攻击者设法进入城门,他们可能会面对另一层防御,例如,木马可能被封闭在一个安全的庭院里,无法进入城市的其他地方。

一个 3000 年前的故事告诉我们关于规模安全甚至安全本身的什么?首先,如果你试图了解你需要防御和遏制系统的策略,你必须首先了解攻击本身。如果我们把特洛伊城看作一个系统,我们可以按照攻击者的步骤(攻击的阶段)来发现深度防御可能解决的弱点。

在高层次上,我们可以将特洛伊木马攻击分为四个阶段:

  1. 威胁建模和漏洞发现——评估目标并专门寻找防御和弱点。攻击者无法从外部打开城门,但他们能从内部打开吗?

  2. 部署——为攻击设置条件。攻击者构建并交付了一个特洛伊最终带进城墙内的物体。

  3. 执行——执行实际的攻击,利用之前阶段的攻击。士兵们从特洛伊木马中出来,打开城门让希腊军队进入。

  4. 妥协——在成功执行攻击后,损害发生并开始减轻。

特洛伊人在妥协之前的每个阶段都有机会阻止攻击,并因错过这些机会而付出了沉重的代价。同样,你系统的深度防御可以减少如果你的系统被攻击的话可能需要付出的代价。

威胁建模和漏洞发现

攻击者和防御者都可以评估目标的弱点。攻击者对他们的目标进行侦察,找到弱点,然后模拟攻击。防御者应该尽力限制在侦察期间向攻击者暴露的信息。但是因为防御者无法完全阻止这种侦察,他们必须检测到它并将其用作信号。在特洛伊木马的情况下,防御者可能会因为陌生人询问城门的防御方式而保持警惕。鉴于这种可疑活动,当他们在城门口发现一个大木马时,他们会更加谨慎。

注意这些陌生人的询问相当于收集威胁情报。有许多方法可以为你自己的系统做到这一点,你甚至可以选择外包其中的一些。例如,你可以做以下事情:

  • 监视你的系统进行端口和应用程序扫描。

  • 跟踪类似你的 URL 的 DNS 注册情况——攻击者可能会利用这些注册进行钓鱼攻击。

  • 购买威胁情报数据。

  • 建立一个威胁情报团队来研究和被动监视已知和可能对你基础设施构成威胁的活动。虽然我们不建议小公司投入资源进行这种方法,但随着公司的发展,这可能会变得具有成本效益。

作为对你系统内部了解的防御者,你的评估可以比攻击者的侦察更详细。这是一个关键点:如果你了解你系统的弱点,你可以更有效地防御它们。而且你了解攻击者目前正在使用或有能力利用的方法越多,你就越能放大这种效果。一个警告:要小心对你认为不太可能或不相关的攻击向量产生盲点。

攻击部署

如果你知道攻击者正在对你的系统进行侦察,那么检测和阻止攻击的努力就至关重要。想象一下,如果特洛伊人决定不允许木马进入城门,因为它是由他们不信任的人创建的。相反,他们可能会在允许它进入之前彻底检查特洛伊木马,或者可能会将其点燃。

在现代,你可以使用网络流量检查、病毒检测、软件执行控制、受保护的沙箱¹和适当的特权配置来检测潜在的攻击。

攻击的执行

如果你无法阻止对手的所有部署,你需要限制潜在攻击的影响范围。如果防御者将特洛伊木马圈起来,从而限制了他们的暴露,攻击者将会更难从他们的藏身之处不被察觉地前进。网络战将这种策略称为沙盒化(在“运行时层”中有更详细的描述)。

妥协

当特洛伊人醒来发现敌人站在他们的床边时,他们知道他们的城市已经被妥协了。这种意识是在实际妥协发生之后才出现的。许多不幸的银行在 2018 年面临了类似的情况,因为他们的基础设施被EternalBlueWannaCry污染了。

你如何从这一点做出回应,将决定你的基础设施被妥协的时间有多长。

Google App Engine 分析

让我们考虑深度防御如何应用到一个更现代的案例:Google App Engine。Google App Engine 允许用户托管应用程序代码,并在负载增加时进行扩展,而无需管理网络、机器和操作系统。图 8-1 显示了 App Engine 早期的简化架构图。保护应用程序代码是开发者的责任,而保护 Python/Java 运行时和基本操作系统是 Google 的责任。

Google App Engine 架构的简化视图

图 8-1:Google App Engine 架构的简化视图

Google App Engine 的原始实现需要特殊的进程隔离考虑。当时,Google 使用传统的 POSIX 用户隔离作为默认策略(通过不同的用户进程),但我们决定在计划的采用程度上,将每个用户的代码运行在独立的虚拟机中效率太低。我们需要找出如何以与 Google 基础设施中的任何其他作业相同的方式运行第三方、不受信任的代码。

风险的 API

App Engine 的初始威胁建模发现了一些令人担忧的领域:

  • 网络访问存在问题。在那之前,所有在 Google 生产网络中运行的应用程序都被认为是受信任的和经过身份验证的基础设施组件。由于我们在这个环境中引入了任意的、不受信任的第三方代码,我们需要一种策略来将 App Engine 的内部 API 和网络暴露与其隔离开。我们还需要记住,App Engine 本身是运行在同一基础设施上的,因此依赖于对这些 API 的访问。

  • 运行用户代码的机器需要访问本地文件系统。至少这种访问被限制在属于特定用户的目录中,这有助于保护执行环境,并减少用户提供的应用程序对同一台机器上其他用户的应用程序的干扰的风险。

  • Linux 内核意味着 App Engine 暴露在了大规模攻击的表面上,我们希望将其最小化。例如,我们希望尽可能防止许多本地权限提升的类别。

为了解决这些挑战,我们首先检查了限制用户对每个 API 的访问。我们的团队在运行时删除了用于网络和文件系统交互的内置 API。我们用“安全”版本替换了内置 API,这些版本调用其他云基础设施,而不是直接操作运行时环境。

为了防止用户重新引入解释器中故意删除的功能,我们不允许用户提供的编译字节码或共享库。用户必须依赖我们提供的方法和库,以及各种可能需要的允许的仅运行时开源实现。

运行时层

我们还对运行时基本数据对象实现进行了广泛的审计,以查找可能导致内存损坏错误的功能。这次审计在我们推出的每个运行时环境中产生了一些上游错误修复。

我们假设至少一些这些防御措施会失败,因为我们不太可能找到和预测所选择运行时中的每个可利用条件。我们决定将 Python 运行时专门适应编译为 Native Client (NaCL) 位码。NaCL 允许我们防止许多类内存损坏和控制流颠覆攻击,这些攻击我们深度代码审计和加固都错过了。

我们并不完全满意 NaCL 能够完全包含所有风险代码突破和错误,因此我们添加了第二层 ptrace 沙盒,以过滤和警报意外的系统调用和参数。对这些期望的任何违反立即终止运行时,并以高优先级发送警报,以及相关活动的日志。

在接下来的五年里,团队发现了一些异常活动的案例,这是由于其中一个运行时中的可利用条件。在每种情况下,我们的沙盒层都给我们带来了明显的优势,使我们能够控制他们的活动在设计参数内。

功能上,App Engine 中的 Python 实现具有 图 8-2 中显示的沙盒层。

App Engine 中 Python 实现的沙盒层

图 8-2:App Engine 中 Python 实现的沙盒层

App Engine 的各层是互补的,每一层都预期了前一层的弱点或可能的失败。随着防御激活穿过各层,对妥协的信号变得更强,使我们能够集中精力应对可能的攻击。

尽管我们对 Google App Engine 的安全性采取了彻底和分层的方法,但我们仍然受益于在保护环境方面的外部帮助。除了我们的团队发现异常活动外,外部研究人员还发现了几种可利用的向量。我们对发现并披露这些漏洞的研究人员表示感激。

控制退化

在深度防御设计时,我们假设系统组件甚至整个系统都可能失败。失败可能由许多原因引起,包括物理损坏、硬件或网络故障、软件配置错误或错误,或安全妥协。当组件失败时,影响可能会扩展到依赖它的每个系统。类似资源的全局池也变得更小 - 例如,磁盘故障会减少整体存储容量,网络故障会减少带宽并增加延迟,软件故障会降低整个系统的计算能力。故障可能会相互叠加 - 例如,存储空间不足可能导致软件故障。

这些资源短缺,或者像Slashdot 效应所引起的突然请求激增,错误配置,或者拒绝服务攻击,都可能导致系统超载。当系统负载超过其容量时,其响应必然开始退化,这可能导致一个完全破碎的系统,没有可用性。除非您事先计划了这种情况,否则您不知道系统可能会在哪里崩溃,但这很可能是系统最薄弱的地方,而不是最安全的地方。

为了控制退化,当出现严重情况时,您必须选择禁用或调整哪些系统属性,同时尽一切可能保护系统的安全性。如果您故意为这些情况设计多个响应选项,系统可以利用受控的断点,而不是经历混乱的崩溃。您的系统可以通过优雅地退化来响应,而不是触发级联故障并处理随之而来的混乱。以下是一些实现这一目标的方法:

  • 通过禁用不经常使用的功能、最不重要的功能或高成本的服务功能,释放资源并减少失败操作的频率。然后,您可以将释放的资源应用于保留重要功能和功能。例如,大多数接受 TLS 连接的系统都支持椭圆曲线(ECC)和 RSA 加密系统。根据您系统的实现,其中一个将更便宜,同时提供可比较的安全性。在软件中,ECC 对私钥操作的资源消耗较少。³当系统资源受限时,禁用对 RSA 的支持将为 ECC 的更低成本提供更多连接空间。

  • 目标是使系统响应措施能够快速自动地生效。这在您直接控制的服务器上最容易,您可以任意切换任何范围或粒度的操作参数。用户客户端更难控制:它们具有较长的发布周期,因为客户端设备可能推迟或无法接收更新。此外,客户端平台的多样性增加了由于意外不兼容性而导致响应措施回滚的机会。

  • 了解哪些系统对公司的使命至关重要,以及它们的相对重要性和相互依赖性。您可能需要按照它们的相对价值保留这些系统的最小功能。例如,谷歌的 Gmail 有一个“简单的 HTML 模式”,它禁用了花哨的 UI 样式和搜索自动完成,但允许用户继续打开邮件。如果网络故障限制了某个地区的带宽,甚至可以降低这种模式的优先级,如果这样可以让网络安全监控继续保护该地区的用户数据。

如果这些调整能够显著提高系统吸收负载或故障的能力,它们将为所有其他弹性机制提供关键的补充,并为事件响应者提供更多的响应时间。最好是提前做出必要和困难的选择,而不是在事件发生时承受压力。一旦个别系统制定了明确的退化策略,就更容易在更大范围内优先考虑退化,跨多个系统或产品领域。

区分故障成本

任何失败操作都会有一定的成本,例如,从移动设备上传数据到应用后端的失败数据上传会消耗计算资源和网络带宽来设置 RPC 并推送一些数据。如果您可以重构您的流程以便早期或廉价地失败,您可能能够减少或避免一些与失败相关的浪费。

对于故障成本的推理:

识别个别操作的总成本。

例如,您可以在对特定 API 进行负载测试期间收集 CPU、内存或带宽影响指标。如果时间紧迫,首先专注于最具影响力的操作,无论是通过关键性还是频率。

确定在操作的哪个阶段产生了这些成本。

您可以检查源代码或使用开发人员工具来收集内省数据(例如,Web 浏览器提供请求阶段的跟踪)。您甚至可以在不同阶段的代码中加入故障模拟。

利用您收集的有关操作成本和故障点的信息,您可以寻找可以推迟高成本操作的变化,直到系统更进一步朝着成功发展。

计算资源

从操作开始到失败期间消耗的计算资源对任何其他操作都是不可用的。如果客户端在失败时进行积极的重试,这种影响会成倍增加,甚至可能导致系统级联故障。通过在执行流程的早期检查错误条件,您可以更快地释放计算资源,例如,您可以在系统分配内存或启动数据读取/写入之前检查数据访问请求的有效性。SYN cookies可以让您避免为源自伪造 IP 地址的 TCP 连接请求分配内存。CAPTCHA 可以帮助保护最昂贵的操作免受自动滥用。

更广泛地说,如果服务器可以得知其健康状况正在下降(例如,来自监控系统的信号),您可以让服务器切换到“残废鸭”模式:它继续提供服务,但让其调用者知道要减少或停止发送请求。这种方法提供了更好的信号,整体环境可以适应,同时最小化了用于提供错误的资源。

也可能由于外部因素,多个服务器实例变得未被使用。例如,它们运行的服务可能因安全妥协而被“排空”或隔离。如果您监视这种情况,服务器资源可以暂时释放以供其他服务重用。然而,在重新分配资源之前,您应该确保保护任何可能对法医调查有帮助的数据。

用户体验

系统与用户的交互在降级条件下应具有可接受的行为水平。理想的系统会通知用户其服务可能出现故障,但允许他们继续与保持功能的部分进行交互。系统可能尝试不同的连接、认证和授权协议或端点以保持功能状态。由于故障造成的任何数据陈旧或安全风险应清楚地向用户传达。不再安全使用的功能应明确禁用。

例如,向在线协作应用添加离线模式可以在临时丢失在线存储、显示他人更新或集成聊天功能的情况下保留核心功能。在端到端加密的聊天应用中,用户可能偶尔更改用于保护通信的加密密钥。这样的应用将保持所有先前的通信可访问,因为它们的真实性不受此更改的影响。

相比之下,一个糟糕的设计示例是整个 GUI 变得无响应,因为其后端的 RPC 之一已超时。想象一下,设计为在启动时连接到后端以仅显示最新内容的移动应用。后端可能无法访问,仅仅是因为设备的用户有意禁用了连接;尽管如此,用户仍然看不到以前缓存的数据。

可能需要用户体验(UX)研究和设计工作,以找到在降级模式下提供可用性和生产力的 UX 解决方案。

减轻速度

系统在失败后的恢复速度会影响失败的成本。此响应时间包括人类或自动化进行减轻变化的时间以及最后一个受影响的组件实例更新和恢复的时间。避免将关键故障点放入像客户端应用这样更难控制的组件中。

回到之前的例子,移动应用在启动时发起新鲜度更新的设计选择将连接性转变为关键依赖。在这种情况下,初始问题会因应用程序更新的缓慢和不可控速度而被放大。

部署响应机制

理想情况下,系统应该通过安全的、预先编程的措施积极应对恶化的条件,以最大限度地提高响应的效果,同时最大限度地减少对安全性和可靠性的风险。自动化措施通常比人类表现更好——人类反应较慢,可能没有足够的网络或安全访问权限来完成必要的操作,并且在解决多个变量时不如机器。然而,人类应该保持在循环中,以提供检查和平衡,并在意外或非平凡情况下做出决策。

让我们详细考虑管理过度负载的问题——无论是由于服务能力的丧失、良性流量峰值,还是 DoS 攻击。人类可能反应不够快,流量可能会压倒服务器,导致级联故障和最终全局服务崩溃。通过永久超额配置服务器来创建保障会浪费金钱,并不能保证安全响应。相反,服务器应根据当前条件调整它们对负载的响应方式。在这里可以使用两种具体的自动化策略:

  • 负载放弃是通过返回错误而不是提供请求来实现的。

  • 客户端的限流是通过延迟响应直到接近请求截止日期来实现的。

图 8-3 说明了超出容量的流量峰值。图 8-4 说明了使用负载放弃和限流来管理负载峰值的效果。请注意以下内容:

  • 曲线代表每秒请求,曲线下方代表总请求量。

  • 空白表示处理流量而没有失败。

  • 反斜线区域代表受损的流量(一些请求失败)。

  • 斜线区域代表被拒绝的流量(所有请求失败)。

  • 斜线区域代表受优先处理的流量(重要请求成功)。

图 8-3 显示了系统可能实际崩溃,导致在请求数量和停机时间方面产生更大的影响。图 8-3 还区分了系统崩溃前的受控流量的不受控制的性质(反斜线区域)。图 8-4 显示了负载放弃的系统拒绝的流量明显少于图 8-3 中的流量(斜线区域),其余流量要么在没有失败的情况下被处理(空白区域),要么如果优先级较低则被拒绝(斜线区域)。

完全停机和负载峰值可能引发级联故障

图 8-3:完全停机和负载峰值可能引发级联故障

使用负载放弃和限流来管理负载峰值

图 8-4:使用负载放弃和限流来管理负载峰值

负载放弃

负载分担的主要弹性目标(在SRE 书籍的第 22 章中描述)是将组件稳定在最大负载,这对于保护安全关键功能尤为有益。当组件的负载开始超过其容量时,您希望组件为所有过多的请求提供错误响应,而不是崩溃。崩溃会使所有组件的容量不可用——不仅仅是用于过多请求的容量。当这种容量消失时,负载会转移到其他地方,可能导致级联故障。

负载分担允许您在服务器负载达到容量之前释放服务器资源,并使这些资源可用于更有价值的工作。为了选择要分担的请求,服务器需要具有请求优先级和请求成本的概念。您可以定义一个策略,根据请求优先级、请求成本和当前服务器利用率来确定每种请求类型要分担多少。根据请求的业务关键性或其依赖关系分配请求优先级(安全关键功能应该获得高优先级)。您可以测量或经验估计请求成本。⁵无论哪种方式,这些测量应该与服务器利用率测量相当,例如 CPU 和(可能)内存使用。当然,计算请求成本应该是经济的。

限流

限流(在SRE 书籍的第二十一章中描述)通过延迟当前操作以推迟未来操作,间接修改客户端的行为。服务器收到请求后,可能会在处理请求之前等待,或者在处理完请求后,在向客户端发送响应之前等待。这种方法减少了服务器从客户端接收的请求的速率(如果客户端按顺序发送请求),这意味着您可以在等待时间内重定向所节省的资源。

类似于负载分担,您可以定义策略将限流应用于特定的有问题的客户端,或者更普遍地应用于所有客户端。请求优先级和成本在选择要限流的请求时起着作用。

自动响应

服务器利用统计数据可以帮助确定何时考虑应用诸如负载分担和限流之类的控制。服务器负载越重,它能处理的流量或负载就越少。如果控制需要太长时间才能激活,优先级较高的请求可能最终会被丢弃或限流。

为了有效地管理这些降级控制,您可能需要一个中央内部服务。您可以将关于使命关键功能和故障成本的业务考虑转化为该服务的策略和信号。这个内部服务还可以聚合关于客户端和服务的启发式信息,以便向几乎实时地所有服务器分发更新的策略。然后服务器可以根据基于服务器利用率的规则应用这些策略。

一些自动响应的可能性包括以下内容:

  • 实现能够响应限流信号并尝试将流量转移到负载较低的服务器的负载平衡系统

  • 提供可以在限流无效或有害时协助应对恶意客户端的 DoS 保护

  • 使用关于关键服务的大规模停电报告来触发备用组件的故障转移准备(这是我们在本章后面讨论的一种策略)

您还可以使用自动化进行自主故障检测:确定无法为某些或所有类别的请求提供服务的服务器可以将自身降级为完全的负载分担模式。自包含或自托管的检测是可取的,因为您不希望依赖外部信号(可能是攻击者模拟的)来迫使整个服务器群陷入故障。

在实现优雅降级时,重要的是确定和记录系统降级的级别,无论是什么触发了问题。这些信息对诊断和调试很有用。报告实际的负载分担或限制(无论是自我施加的还是指导的)可以帮助您评估全局健康和容量,并检测错误或攻击。您还需要这些信息来评估当前剩余的系统容量和用户影响。换句话说,您想知道各个组件和整个系统的降级程度,以及您可能需要采取的手动操作。事件发生后,您将希望评估您的降级机制的有效性。

负责任地自动化

在创建自动响应机制时要谨慎,以防止它们使系统的安全性和可靠性降低到意外程度。

安全失败与安全失败

在设计处理故障的系统时,您必须在优化可靠性的失败开放(安全)和优化安全性的失败关闭(安全)之间取得平衡:^(6)

  • 为了最大程度地提高可靠性,系统应该抵抗故障,并在面对不确定性时尽可能提供服务。即使系统的完整性没有得到保证,只要其配置是可行的,一个针对可用性进行优化的系统将提供其所能提供的服务。如果 ACL 加载失败,则假定默认 ACL 为“允许所有”。

  • 为了最大程度地提高安全性,系统在面对不确定性时应完全封锁。如果系统无法验证其完整性——无论是由于故障的磁盘带走了其配置的一部分,还是攻击者更改了配置以进行利用——它就不能被信任运行,应尽可能保护自己。如果 ACL 加载失败,则假定默认 ACL 为“拒绝所有”。

这些可靠性和安全性原则显然是相互矛盾的。为了解决这种紧张局势,每个组织必须首先确定其最低不可妥协的安全姿态,然后找到提供所需安全服务关键特性的可靠性的方法。例如,配置为丢弃低 QoS(服务质量)数据包的网络可能需要为安全导向的 RPC 流量标记特殊的 QoS,以防止数据包丢失。安全导向的 RPC 服务器可能需要特殊标记,以避免被工作负载调度程序耗尽 CPU。

人类的立足点

有时人类必须参与服务降级决策。例如,基于规则的系统进行判断的能力受到预定义规则的固有限制。当自动化面临不符合系统任何预定义响应的未预见情况时,自动化不会起作用。由于编程错误,自动化响应也可能产生未预见的情况。允许适当的人类干预来处理这些和类似情况需要在系统设计中进行一些事先考虑。

首先,您应该防止自动化禁用员工用于恢复基础设施的服务(请参阅“紧急访问”)。重要的是为这些系统设计保护,以便即使是 DoS 攻击也不能完全阻止访问。例如,SYN 攻击不应该阻止响应者为 SSH 会话打开 TCP 连接。确保实现低依赖性的替代方案,并持续验证这些替代方案的能力。

此外,不要允许自动化进行大规模(例如,单个服务器放弃所有RPC)或大范围(所有服务器放弃一些 RPC)的无监督策略更改。考虑实现变更预算。当自动化耗尽该预算时,不会发生自动刷新。相反,必须由人类增加预算或做出不同的判断。请注意,尽管有人类干预,自动化仍然存在。

控制爆炸半径

通过限制系统的每个部分的范围,您可以为您的深度防御策略增加另一层。例如,考虑网络分段。过去,组织通常拥有一个包含所有资源(机器、打印机、存储、数据库等)的单一网络。这些资源对网络上的任何用户或服务都是可见的,并且访问由资源本身控制。

今天,改善安全性的常见方法是对网络进行分段,并为特定类别的用户和服务授予对每个段的访问权限。您可以通过使用具有网络 ACL 的虚拟局域网(VLAN)来实现这一点,这是一种易于配置的行业标准解决方案。您可以控制进入每个段的流量,并控制允许通信的段。您还可以限制每个段对“需要知道”的信息的访问。

网络分段是隔离的一般概念的一个很好的例子,我们在第六章中讨论过。隔离涉及有意地创建小的个体操作单元(隔间),并限制对每个隔间的访问和来自每个隔间的访问。对系统的大多数方面进行隔间化是一个好主意,包括服务器、应用程序、存储等等。当您使用单一网络设置时,入侵者如果窃取了用户的凭据,可能会访问网络上的每个设备。然而,当您使用隔间化时,一个隔间中的安全漏洞或流量过载并不会危及所有隔间。

控制爆炸半径意味着将事件的影响分隔开,类似于船舶上的隔舱可以使整艘船免于沉没。在设计弹性时,您应该创建约束攻击者和意外故障的隔舱壁垒。这些隔离壁垒可以让您更好地定制和自动化您的响应。您还可以使用这些边界来创建故障域,提供组件冗余和故障隔离,如在“故障域和冗余”中所讨论的那样。

隔间还有助于隔离努力,减少了响应者需要积极平衡防御和保留证据的需求。一些隔间可以被隔离和冻结以进行分析,而其他隔间则可以被恢复。此外,隔间在事件响应期间为更换和修复创建了自然边界,一个隔间可能被舍弃以拯救系统的其余部分。

要控制入侵的爆炸半径,您必须有一种建立边界并确保这些边界安全的方法。将生产中运行的作业视为一个隔间。这个作业必须允许一些访问(您希望这个隔间有用),但不能是无限制的访问(您希望保护这个隔间)。限制谁可以访问作业取决于您识别生产中的端点并确认其身份的能力。

您可以通过使用经过身份验证的远程过程调用来实现这一点,该调用可以在一个连接中识别双方。为了保护各方的身份免受欺骗,并将其内容隐藏在网络中,这些 RPC 使用相互认证的连接,可以证明连接到服务的双方的身份。为了让端点对其他隔间做出更明智的决定,您可以添加端点与其身份一起发布的附加信息。例如,您可以向证书添加位置信息,以便拒绝非本地请求。

一旦建立隔间的机制到位,您将面临一个艰难的抉择:您需要通过足够的分离来限制您的操作,以提供有用大小的隔间,但又不会创建太多的分离。例如,隔间化的一个平衡方法是将每个 RPC 方法视为单独的隔间。这样可以沿着逻辑应用边界对齐隔间,而隔间的数量与系统功能的数量成正比。

控制 RPC 方法可接受参数值的隔间分离需要更加谨慎的考虑。虽然这会创建更严格的安全控制,但每个 RPC 方法可能的违规次数与 RPC 客户端的数量成正比。这种复杂性会在系统的所有功能中累积,并需要协调客户端代码和服务器策略的更改。另一方面,无论 RPC 服务或其方法如何,包装整个服务器的隔间要容易得多,但提供的价值相对较少。在权衡这种权衡时,有必要与事件管理和运营团队协商,以考虑隔间类型的选择,并验证您的选择的效用。

不完美的隔间化并不能完全覆盖所有边缘情况,但也可以提供价值。例如,寻找边缘情况的过程可能会导致攻击者犯错,从而提醒您他们的存在。攻击者逃离隔间所需的任何时间都是您的事件响应团队有机会做出反应的额外时间。

事件管理团队必须计划和实践封锁隔间以遏制入侵或不良行为的策略。关闭生产环境的一部分是一个戏剧性的举措。设计良好的隔间给事件管理团队提供了执行与事件成比例的操作的选项,因此他们不一定要将整个系统下线。

当您实现隔间化时,您面临着一个抉择:让所有客户共享给定服务的单个实例,或者运行支持单个客户或客户子集的单独服务实例。

例如,在同一硬件上运行由不同互不信任的实体控制的两个虚拟机(VM)存在一定风险:可能会暴露在虚拟化层的零日漏洞,或者存在微妙的跨 VM 信息泄漏。一些客户可能会选择通过基于物理硬件对其部署进行隔间化来消除这些风险。为了促进这种方法,许多云提供商提供基于每个客户专用硬件的部署。在这种情况下,减少资源利用的成本反映在定价溢价中。

隔间分离为系统增加了韧性,只要系统有机制来维持这种分离。困难的任务是跟踪这些机制并确保它们保持在位。为了防止退化,验证跨隔间边界禁止的操作失败是有价值的。方便的是,因为运营冗余依赖于隔间化(在“故障域和冗余”中介绍),您的验证机制可以涵盖被禁止和预期的操作。

谷歌通过角色、位置和时间进行隔间化。当攻击者试图破坏隔间化系统时,任何单次攻击的潜在范围都大大减少。如果系统受到攻击,事件管理团队可以选择仅禁用部分系统以清除受攻击的影响,同时保持其他部分运行。接下来的部分将详细探讨不同类型的隔间化。

角色分离

大多数现代微服务架构系统允许用户以特定的角色服务账户运行作业。然后,这些作业将获得凭据,允许它们以特定角色在网络上进行身份验证。如果对手损害了一个作业,他们将能够在网络上冒充该作业对应的角色。因为这允许对手访问其他作业作为该角色可以访问的所有数据,这实际上意味着对手也损害了其他作业。

为了限制这种损害的影响范围,不同的作业通常应该以不同的角色运行。例如,如果您有两个需要访问两种不同类型数据(比如照片和文本聊天)的微服务,将这两个微服务作为不同的角色运行可以增加系统的弹性,即使这两个微服务由同一个团队开发和运行。

位置分离

位置分离有助于限制攻击者的影响的另一个维度:微服务运行的位置。例如,您可能希望防止已经物理损害了一个数据中心的对手能够读取所有其他数据中心的数据。同样,您可能希望您最有权力的管理用户的访问权限仅限于特定地区,以减轻内部风险。

实现位置分离的最明显方法是在不同位置(如数据中心或云区域,通常对应不同的物理位置)运行相同的微服务作为不同的角色。然后,您可以使用正常的访问控制机制来保护不同位置的相同服务实例,就像您会保护不同角色的不同服务一样。

位置分离有助于抵抗从一个位置转移到另一个位置的攻击。基于位置的加密隔间可以让您限制对特定位置的应用程序和其存储的数据的访问,从而限制本地攻击的影响范围。

物理位置是自然的隔间边界,因为许多不利事件与物理位置有关。例如,自然灾害局限于一个地区,其他局部事件如光纤切断、停电或火灾也是如此。需要攻击者在物理上出现的恶意攻击也局限于攻击者实际可以到达的位置,而除了最有能力的(例如国家级攻击者)可能没有能力同时派遣攻击者到多个位置。

同样,风险暴露的程度可能取决于物理位置的性质。例如,特定类型的自然灾害风险随地理区域而异。此外,入侵者尾随进入建筑并找到一个开放的网络端口插入的风险在员工和访客流量大的办公地点要高于对物理访问严格受控的数据中心。

有了这个想法,您在设计系统时需要考虑位置,以确保局部影响保持在该地区的系统中,同时让您的多区域基础设施继续运行。例如,重要的是要确保由几个地区的服务器提供的服务不会对单一数据中心中的后端产生关键依赖。同样,您需要确保一个位置的物理损害不会让攻击者轻易损害其他位置:尾随进入办公室并插入会议室的开放端口不应该让入侵者轻易获得对数据中心生产服务器的网络访问。

调整物理和逻辑架构

当将架构分隔为逻辑故障和安全域时,将相关的物理边界与逻辑边界对齐是有价值的。例如,将网络分割为网络级风险(如受恶意互联网流量影响的网络与受信任的内部网络)和物理攻击风险是有用的。理想情况下,您应该在不同的建筑物中为公司和生产环境之间进行网络隔离。此外,您可能会进一步将公司网络细分,以隔离访客流量较大的区域,如会议和会议区域。

在许多情况下,物理攻击,比如窃取或在服务器上植入后门,可能会使攻击者获得重要的机密、加密密钥或凭证,从而可能允许他们进一步渗透您的系统。考虑到这一点,将机密、密钥和凭证的分发在逻辑上隔离到物理服务器上是一个好主意,以最小化物理妥协的风险。

例如,如果您在几个物理数据中心位置运行 Web 服务器,将为每台服务器部署单独的证书或仅在一个位置的服务器之间共享证书,而不是在所有服务器上共享单个证书,这可能是有利的。这可以使您对一个数据中心的物理妥协做出更灵活的响应:您可以将其流量排空,仅撤销部署到该数据中心的证书,并将数据中心下线进行事件响应和恢复,同时从其余数据中心提供流量。如果您在所有服务器上部署了单个证书,您将不得不非常快速地替换所有证书——甚至那些实际上没有受到威胁的证书。

信任隔离

虽然服务可能需要跨位置边界进行通信以正常运行,但服务也可能希望拒绝来自其不希望通信的位置的请求。为了做到这一点,您可以默认限制通信,并只允许跨位置边界的预期通信。任何服务上的所有 API 都不太可能使用相同的位置限制集。用户面向的 API 通常是全球开放的,而控制平面 API 通常是受限的。这使得对允许位置的细粒度(每个 API 调用)控制成为必要。创建工具,使得任何给定服务能够测量、定义和强制执行对个别 API 的位置限制,使团队能够利用其对每个服务的知识来实现位置隔离。

限制基于位置的通信,每个身份都需要包含位置元数据。谷歌的作业控制系统对生产中的作业进行认证和运行。当系统认证一个作业在特定隔间运行时,它会用该隔间的位置元数据注释作业的证书。每个位置都有自己的作业控制系统,用于认证在该位置运行的作业,并且该位置的机器只接受该系统的作业。这旨在防止攻击者突破隔间边界并影响其他位置。与单一的集中管理机构相比,这种方法有所不同——如果谷歌只有一个作业控制系统,那么它的位置对攻击者来说将是非常有价值的。

一旦信任隔离就位,我们可以扩展存储数据的 ACL,以包括位置限制。这样,我们可以将存储位置(放置数据的地方)与访问位置(谁可以检索或修改数据)分开。这也打开了信任物理安全与信任 API 访问的可能性——有时,物理操作员的额外要求是值得的,因为它消除了远程攻击的可能性。

为了帮助控制隔间违规行为,谷歌在每个位置都设有一个信任根,并将受信任的根和它们代表的位置的列表分发给机群中的所有机器。这样,每台机器都可以检测跨位置的欺骗行为。我们还可以通过向所有机器分发更新后的列表来撤销某个位置的身份,宣布该位置不可信任。

基于位置的信任的局限性

在谷歌,我们选择设计我们的企业网络基础设施,使位置不意味着任何信任。相反,在我们的 BeyondCorp 基础设施的零信任网络范式下(参见第五章),工作站是基于颁发给个体机器的证书以及关于其配置的断言(如最新软件)而受信任的。将一个不受信任的机器插入办公楼网络端口将其分配到一个不受信任的访客 VLAN。只有经过授权的工作站(通过 802.1x 协议进行身份验证)才会被分配到适当的工作站 VLAN。

我们还选择不依赖物理位置来建立数据中心服务器的信任。一个激励性的经验来自于对数据中心环境的红队评估。在这次演习中,红队在机架顶部放置了一个无线设备,并迅速将其插入一个开放端口,以便从建筑外部进一步渗透数据中心的内部网络。当他们回来清理演习后,他们发现一个细心的数据中心技术人员已经整齐地用拉链扎紧了接入点的电缆——显然对不整洁的安装工作感到不满,并假设该设备一定是合法的。这个故事说明了基于物理位置归因信任的困难,即使在一个物理安全的区域内也是如此。

在谷歌的生产环境中,类似于 BeyondCorp 设计,生产服务之间的身份验证是基于每台机器凭据的机器间信任。未经授权设备上的恶意植入物将不会受到谷歌生产环境的信任。

保密隔离

一旦我们有了一个隔离信任的系统,我们需要隔离我们的加密密钥,以确保通过一个位置的加密根保护的数据不会因为在另一个位置泄露加密密钥而受损。例如,如果公司的一个分支遭到攻击,攻击者不应该能够从公司的其他分支读取数据。

谷歌拥有保护密钥树的基本加密密钥。这些密钥最终通过密钥封装和密钥派生来保护静态数据。

为了将加密和密钥封装隔离到一个位置,我们需要确保该位置的根密钥只能被正确的位置访问。这需要一个分发系统,只将根密钥放置在正确的位置。密钥访问系统应该利用信任隔离,确保这些密钥不能被不在适当位置的实体访问。

使用这些原则,给定位置允许在本地密钥上使用 ACLs,以防止远程攻击者解密数据。即使攻击者可以访问加密数据(通过内部妥协或外泄),也可以防止解密。

从全局密钥树过渡到本地密钥树应该是逐渐的。虽然树的任何部分可以独立地从全局转移到本地,但在所有上面的密钥都转移到本地密钥之前,对于给定的叶子或分支,隔离并不完整。

时间分离

最后,限制对手随时间的能力是有用的。在这里要考虑的最常见情况是,对手已经破坏了系统并窃取了密钥或凭证。如果您随时间旋转您的密钥和凭证,并使旧的失效,对手必须保持他们的存在以重新获取新的秘密,这给了您更多的机会来检测窃取。即使您从未检测到窃取,旋转仍然至关重要,因为您可能会在正常的安全卫生工作中关闭对手用来获取密钥或凭证的途径(例如,通过修补漏洞)。

正如我们在第九章中讨论的那样,可靠地进行密钥和凭证旋转和失效需要仔细权衡。例如,如果存在阻止在旧凭证失效之前将其旋转到新凭证的故障,那么基于壁钟的凭证失效可能会有问题。提供有用的时间分隔需要在旋转频率与旋转机制失败时的停机风险或数据丢失风险之间进行平衡。

深入探讨:故障域和冗余

到目前为止,我们已经讨论了如何设计系统以响应攻击并通过隔离来包含攻击后果。为了解决系统组件的完全故障,系统设计必须包含冗余和不同的故障域。这些策略可以希望限制故障的影响并避免完全崩溃。减轻关键组件的故障尤为重要,因为任何依赖于失败的关键组件的系统也面临完全失败的风险。

与其旨在始终防止所有故障,不如通过结合以下方法为您的组织创建一个平衡的解决方案:

  • 将系统分解为独立的故障域。

  • 旨在降低单一根本原因影响多个故障域中元素的概率。

  • 创建可以替代失败部分的冗余资源、组件或程序。

故障域

故障域是一种爆炸半径控制。与按角色、位置或时间进行结构分离不同,故障域通过将系统分区为多个等效但完全独立的副本来实现功能隔离。

功能隔离

对其客户端来说,故障域看起来像一个单一的系统。必要时,任何单个分区都可以在停机期间接管整个系统。因为分区只有系统资源的一部分,它只能支持系统容量的一部分。与管理角色、位置和时间分离不同,操作故障域并保持其隔离需要持续的努力。作为交换,故障域增加了系统的弹性,其他爆炸半径控制无法做到。

故障域有助于保护系统免受全局影响,因为单个事件通常不会同时影响所有故障域。然而,在极端情况下,重大事件可能会破坏多个,甚至所有故障域。例如,您可以将存储阵列的基础设备(HDD 或 SSD)视为故障域。尽管任何一个设备可能会失败,但整个存储系统仍然正常运行,因为它在其他地方创建了一个新的数据副本。如果大量存储设备失败,并且没有足够的备用设备来维护数据副本,进一步的故障可能导致存储系统中的数据丢失。

数据隔离

您需要为数据源或单个故障域内可能存在错误数据的可能性做好准备。因此,每个故障域实例都需要其自己的数据副本,以便在功能上独立于其他故障域。我们建议采取双重方法来实现数据隔离。

首先,您可以限制数据更新进入故障域的方式。系统只有在通过了所有典型和安全更改的验证检查后才接受新数据。一些异常情况需要进行理由说明,并且可以通过紧急机制¹⁰允许新数据进入故障域。因此,您更有可能防止攻击者或软件错误造成破坏性的更改。

例如,考虑 ACL 更改。人为错误或 ACL 生成软件中的错误可能会产生一个空的 ACL,这可能导致拒绝所有人的访问。¹¹这样的 ACL 更改可能导致系统故障。同样,攻击者可能会尝试通过向 ACL 添加“允许所有”条款来扩大其影响范围。

在谷歌,个别服务通常具有用于接收新数据和信令的 RPC 端点。编程框架,如第十二章中介绍的那些,包括用于对数据快照进行版本控制和评估其有效性的 API。客户端应用程序可以利用编程框架的逻辑来确定新数据是否安全。集中式数据推送服务实现数据更新的质量控制。数据推送服务检查数据的获取位置、打包方式以及何时推送打包的数据。为了防止自动化导致广泛的故障,谷歌使用每个应用程序的配额来限制全局更改的速度。我们禁止同时更改多个应用程序或在一段时间内过快地更改应用程序容量的操作。

其次,使系统能够将最后已知的良好配置写入磁盘,使系统能够抵御失去对配置 API 的访问:它们可以使用保存的配置。谷歌的许多系统在有限的时间内保留旧数据,以防最新数据因任何原因而损坏。这是深度防御的另一个例子,有助于提供长期的弹性。

实际方面

即使将系统分割成只有两个故障域也会带来实质性的好处:

  • 拥有两个故障域提供了 A/B 回归的能力,并将系统更改的影响范围限制在单个故障域内。为了实现这种功能,使用一个故障域作为金丝雀,并制定一个政策,不允许同时更新两个故障域。

  • 地理上分离的故障域可以为自然灾害提供隔离。

  • 您可以在不同的故障域中使用不同的软件版本,从而减少单个错误破坏所有服务器或损坏所有数据的机会。

将数据和功能隔离相结合可以增强整体的弹性和事件管理。这种方法限制了未经理由接受的数据更改的风险。当问题出现时,隔离会延迟其传播到个别的功能单元。这给其他防御机制更多的时间来检测和反应,这在繁忙和时间紧迫的事件响应过程中尤为有益。通过并行地将多个候选修复方案推送到不同的故障域中,您可以独立地评估哪些修复方案产生了预期的效果。这样,您就可以避免意外地全局推送一个匆忙的更新,进一步降低整个系统的稳定性。

故障域会产生运营成本。即使是一个简单的服务,也需要您维护由故障域标识符键入的多个服务配置的副本。这需要以下工作:

  • 确保配置的一致性

  • 保护所有配置免受同时损坏

  • 隐藏故障域的分离,以防止客户系统意外地耦合到特定的故障域

  • 潜在地分区所有的依赖关系,因为一个共享的依赖关系变化可能会意外地传播到所有的故障域

值得注意的是,如果故障域的任何一个关键组件发生故障,故障域可能会遭受完全故障。毕竟,您最初将原始系统划分为故障域,以便在故障域的副本完全失败时系统仍然可以保持运行。然而,故障域只是将问题向下移动了一个级别。接下来的部分将讨论如何使用替代组件来减轻所有故障域完全失败的风险。

组件类型

故障域的韧性质量表现为其组件和它们的依赖关系的综合可靠性。整个系统的韧性随着故障域的数量增加而增加。然而,这种增加的韧性被维护更多故障域的操作开销所抵消。

通过减缓或停止新功能开发,您可以进一步提高韧性,以换取更稳定性。如果您避免添加新的依赖关系,您也将避免其潜在的故障模式。如果停止更新代码,新错误的速率会减少。然而,即使停止所有新功能的开发,您仍然需要对状态的偶发变化做出反应,比如安全漏洞和用户需求的增加。

显然,停止所有新功能的开发对大多数组织来说并不是一种可行的策略。在接下来的章节中,我们将介绍一系列替代方法来平衡可靠性和价值。一般来说,服务的可靠性有三种广泛的类别:高容量、高可用和低依赖。

高容量组件

您在日常业务中构建和运行的组件构成了您的高容量服务。这是因为这些组件构成了为用户提供服务的主要车队。这是您的服务吸收用户请求或由于新功能而导致的资源消耗激增的地方。高容量组件还吸收 DoS 流量,直到 DoS 缓解生效或优雅降级生效。

因为这些组件对于您的服务非常重要,您应该首先在这里集中精力,例如,遵循容量规划、软件和配置部署的最佳实践,如SRE 书的第三部分和SRE 工作手册的第二部分所涵盖的内容。

高可用组件

如果您的系统有组件的故障会影响所有用户,或者具有重大广泛影响的其他故障后果——在前一节中讨论的高容量组件——您可以通过部署这些组件的副本来减轻这些风险。如果这些组件的副本提供了更低的故障概率,那么这些组件的副本就是高可用的。

为了降低故障概率,这些副本应该配置更少的依赖关系和有限的变更速率。这种方法减少了基础设施故障或操作错误破坏组件的机会。例如,您可以做以下事情:

  • 使用本地存储中缓存的数据,避免依赖远程数据库。

  • 使用旧代码和配置,避免新版本中的最近错误。

运行高可用组件几乎没有操作开销,但需要额外的资源,其成本与车队规模成比例。确定高可用组件是否应该支撑整个用户群或仅支撑部分用户群是一个成本/效益决策。在每个高容量和高可用组件之间以相同的方式配置优雅降级功能。这使您可以用更积极的降级来交换更少的资源。

低依赖组件

如果高可用性组件的故障是不可接受的,低依赖性服务是下一个弹性级别。低依赖性需要具有最小依赖关系的替代实现。这些最小依赖关系也是低依赖性的。可能失败的服务、进程或作业的总集合与业务需求和成本相匹配。高容量和高可用性的服务可以为大型用户群提供服务并提供丰富的功能,因为它们具有多层合作平台(虚拟化、容器化、调度、应用框架)。虽然这些层通过允许服务快速添加或移动节点来帮助扩展,但它们也会导致合作平台的错误预算累积率更高。相比之下,低依赖性服务必须简化其服务堆栈,直到能够接受堆栈的总体错误预算。反过来,简化服务堆栈可能导致必须删除功能。

低依赖性组件要求您确定是否可能为关键组件构建替代方案,其中关键组件和替代组件不共享任何故障域。毕竟,冗余的成功与相同根本原因影响两个组件的概率成反比。

将存储空间视为分布式系统的基本构建块——当数据存储的 RPC 后端不可用时,您可能希望存储本地数据副本作为备用。然而,通常存储本地数据副本的一般方法并不总是切实可行。支持冗余组件的运营成本增加,而额外组件提供的好处通常为零。

实际上,您最终会得到一组低依赖性组件,用户、功能和成本有限,但可以自信地用于临时负载或恢复。虽然大多数有用的功能通常依赖于多个依赖项,但严重受损的服务总比不可用的服务好。

举个小例子,想象一台设备,假定可以通过网络进行只写或只读操作。在家庭安全系统中,这些操作包括记录事件日志(只写)和查找紧急电话号码(只读)。入侵者的入侵计划包括禁用家庭的互联网连接,从而破坏安全系统。为了对抗这种类型的故障,您配置安全系统还使用一个实现与远程服务相同的 API 的本地服务器。本地服务器将事件日志写入本地存储,更新远程服务,并重试失败的尝试。本地服务器还会响应紧急电话号码查找请求。电话号码列表会定期从远程服务刷新。从家庭安全控制台的角度来看,系统正在按预期工作,记录日志并访问紧急号码。此外,一个低依赖性的隐藏座机可能作为备用提供拨号功能,以应对禁用的无线连接。

作为业务规模的例子,全球网络故障是最可怕的故障类型之一,因为它影响了服务功能和响应者修复故障的能力。大型网络是动态管理的,更容易发生全球性故障。构建一个完全避免重复使用主网络中相同网络元素的备用网络-链接、交换机、路由器、路由域或SDN软件-需要仔细设计。这种设计必须针对特定和狭窄的用例和操作参数,使您能够专注于简单和可理解性。为这种很少使用的网络追求最小的资本支出也自然地导致限制可用功能和带宽。尽管存在限制,结果是足够的。目标是仅支持最关键的功能,并且仅为通常带宽的一小部分。

控制冗余

冗余系统配置为每个依赖项都有多个选项。管理这些选项之间的选择并不总是直截了当的,攻击者可能会潜在地利用冗余系统之间的差异-例如,通过将系统推向较不安全的选项。请记住,弹性设计实现了安全性可靠性,而不是牺牲其中一个。如果有的话,当低依赖性的替代方案具有更强的安全性时,这可以作为攻击者考虑削弱您的系统的一种不利因素。

故障转移策略

通过负载平衡技术通常提供一组后端,可以在后端故障时增加弹性。例如,依赖单个 RPC 后端是不切实际的。每当该后端需要重新启动时,系统都会挂起。为简单起见,系统通常将冗余后端视为可互换,只要所有后端提供相同的功能行为。

需要不同可靠性行为的系统(对于相同的功能行为集)应依赖于提供所需可靠性行为的一组可互换的后端。系统本身必须实现逻辑来确定使用哪组行为以及何时使用它们-例如,通过标志。这使您完全控制系统的可靠性,特别是在恢复期间。将此方法与从相同的高可用性后端请求低依赖性行为进行对比。使用 RPC 参数,您可以阻止后端尝试联系其不可用的运行时依赖项。如果运行时依赖项也是启动依赖项,则您的系统仍然离灾难只有一次进程重启。

何时转移到具有更好稳定性的组件取决于特定情况。如果自动故障转移是目标,您应该通过使用“控制退化”中涵盖的方法来解决可用容量的差异。故障转移后,这样的系统会切换到使用针对备用组件调整的限流和负载放弃策略。如果您希望系统在失败的组件恢复后进行故障返回,请提供一种禁用故障返回的方法-在某些情况下,您可能需要稳定波动或精确控制故障转移。

常见陷阱

我们观察到一些常见的操作备用组件的常见陷阱,无论它们是高可用性还是低依赖性。

例如,随着时间的推移,您可能会开始依赖备用组件进行正常操作。任何开始将备用系统视为备份的依赖系统在故障期间可能会过载它们,使备用系统成为拒绝服务的意外原因。相反的问题是备用组件通常不会被常规使用,导致腐烂和意外故障每当它们需要时。

另一个陷阱是对其他服务或所需计算资源的依赖未经检查地增长。随着用户需求的变化和开发人员添加功能,系统往往会发展。随着时间的推移,依赖关系和依赖方增加,系统可能使用资源的效率降低。高可用副本可能落后于高容量舰队,或者低依赖服务可能在其预期的操作约束条件未经持续监控和验证时失去一致性和可重现性。

重要的是,备用组件的故障转移不会损害系统的完整性或安全性。考虑以下情景,正确的选择取决于您组织的情况:

  • 出于安全原因(防御最近的漏洞),您的高可用服务运行了六周前的代码。然而,同一服务需要紧急安全修复。您会选择哪种风险:不应用修复,还是可能通过修复破坏代码?

  • 一个远程密钥服务的启动依赖于获取解密数据的私钥,可以通过将私钥存储在本地存储中来降低依赖性。这种方法是否会给这些密钥带来无法接受的风险,或者增加密钥轮换频率是否足以抵消这种风险?

  • 您确定可以通过减少不经常更改的数据(例如 ACL、证书吊销列表或用户元数据)的更新频率来释放资源。即使这样做可能会给攻击者更多时间进行更改或使更改未被检测到更长时间,这样做是否值得?

最后,您需要确保系统不会在错误的时间自动恢复。如果弹性措施自动限制了系统的性能,那么这些措施自动解除限制是可以的。然而,如果您应用了手动故障转移,请不要允许自动化覆盖故障转移——因为受到安全漏洞的影响,被排除的系统可能被隔离,或者您的团队可能正在减轻级联故障。

深入探讨:持续验证

从可靠性和安全性的角度来看,我们希望确保我们的系统在正常和意外情况下都能如预期般运行。我们还希望确保新功能或错误修复不会逐渐削弱系统的分层弹性机制。实际运行系统并验证其按预期工作是无法替代的。

验证侧重于观察系统在真实受控的情况下,针对单个系统或跨多个系统的工作流程。与混沌工程不同,后者是探索性质的,验证确认了本章和第 5、6 和 9 章中涵盖的特定系统属性和行为。定期进行验证可以确保结果仍然符合预期,并且验证实践本身保持正常运行。

在使验证有意义方面有一些技巧。首先,您可以使用第十五章中涵盖的一些概念和实践,例如如何选择要验证的内容,以及如何衡量有效的系统属性。然后,您可以逐渐扩展验证范围,创建、更新或删除检查。您还可以从实际事件中提取有用的细节——这些细节是系统行为的最终真相,并经常突出了需要的设计更改或验证范围的空白。最后,重要的是要记住,随着业务因素的变化,个别服务往往也会发展和变化,可能导致不兼容的 API 或意想不到的依赖关系。

一般的验证维护策略包括以下内容:

  1. 发现新的故障

  2. 为每个故障实现验证器

  3. 反复执行所有验证器

  4. 当相关功能或行为不再存在时淘汰验证器

要发现相关故障,请依赖以下来源:

  • 用户和员工的定期错误报告

  • 模糊测试和类似模糊测试的方法(在第十三章中描述)

  • 故障注入方法(类似于混沌猴工具

  • 操作您系统的主题专家的分析判断。

构建自动化框架可以帮助您安排不兼容的检查在不同时间运行,以避免它们之间的冲突。您还应该监视并定期审计自动化,以捕捉破损或受损的行为。

验证重点领域

验证整个系统以及其服务之间的端到端协作是有益的。但是,由于验证为真实用户提供服务的整个系统的故障响应是昂贵且风险的,因此您必须做出妥协。验证较小的系统副本通常更为经济实惠,并且仍然提供了通过验证单独的系统组件无法获得的见解。例如,您可以执行以下操作:

  • 告诉调用者如何应对响应缓慢或变得无法访问的 RPC 后端。

  • 查看资源短缺时会发生什么,以及在资源消耗激增时是否可获取紧急资源配额。

另一个实用的解决方案是依靠日志来分析系统和/或其组件之间的交互。如果您的系统实现了分隔,那么试图跨越角色、位置或时间分隔的操作应该失败。如果您的日志记录了意外的成功,那么这些成功应该被标记。日志分析应始终处于活动状态,让您在验证过程中观察实际的系统行为。

您应该验证安全设计原则的属性:最小特权、可理解性、适应性和恢复能力。验证恢复尤为关键,因为恢复工作必然涉及人类行为。人类是不可预测的,单元测试无法检查人类的技能和习惯。在验证恢复设计时,您应该审查恢复说明的可读性以及不同恢复工作流程的效力和互操作性。

验证安全属性意味着不仅要确保系统正确响应,还要检查代码或配置是否存在已知的漏洞。对部署系统进行主动渗透测试可以黑盒视角地查看系统的弹性,通常会突出开发人员未考虑的攻击向量。

与低依赖组件的交互值得特别关注。根据定义,这些组件部署在最关键的情况下。除了这些组件之外没有后备。幸运的是,设计良好的系统应该具有有限数量的低依赖组件,这使得为所有关键功能和交互定义验证器的目标成为可能。只有在需要时这些低依赖组件才能发挥作用。您的恢复计划通常应依赖于低依赖组件,并且您应该验证人类在系统降级到该级别时的使用情况。

实践中的验证

本节介绍了谷歌用于展示持续验证方法的几种验证场景。

注入预期的行为变化

您可以通过向服务器注入行为变化来验证系统对负载分担和限流的响应,然后观察所有受影响的客户端和后端是否适当地响应。

例如,Google 实现了服务器库和控制 API,允许我们向任何 RPC 服务器添加任意延迟或故障。我们在定期的灾难准备演习中使用这些功能,团队可以随时轻松地进行实验。使用这种方法,我们研究隔离的 RPC 方法、整个组件或更大的系统,并专门寻找级联故障的迹象。从延迟的小幅增加开始,我们构建一个阶跃函数,以模拟完全的故障。监控图表清楚地反映了响应延迟的变化,就像在真正的问题中一样,在阶跃点。将这些时间轴与客户端和后端服务器的监控信号相关联,我们可以观察效果的传播。如果错误率与我们在早期步骤中观察到的模式不成比例地飙升,我们就知道要退后一步,暂停,并调查行为是否出乎意料。

快速而安全地取消注入行为的可靠机制非常重要。如果发生故障,即使原因似乎与验证无关,正确的决定是首先中止实验,然后评估何时可以安全地再次尝试。

将紧急组件作为正常工作流程的一部分

当我们观察系统执行其预期功能时,我们可以确信低依赖或高可用系统正在正确运行并准备投入生产。为了测试准备就绪,我们将真实流量的一小部分或真实用户的一小部分推送到我们正在验证的系统。

高可用系统(有时是低依赖系统)通过镜像请求进行验证:客户端发送两个相同的请求,一个发送到高容量组件,另一个发送到高可用组件。通过修改客户端代码或注入一个可以将一个输入流量复制成两个等效输出流的服务器,您可以比较响应并报告差异。当响应差异超出预期水平时,监控服务会发送警报。一些差异是可以预期的;例如,如果备用系统具有较旧的数据或功能。因此,客户端应该使用高容量系统的响应,除非发生错误或客户端明确配置为忽略该系统——这两种情况都可能在紧急情况下发生。镜像请求不仅需要在客户端进行代码更改,还需要能够自定义镜像行为。因此,这种策略更容易部署在前端或后端服务器上,而不是在最终用户设备上。

低依赖系统(有时是高可用系统)更适合通过真实用户而不是请求镜像进行验证。这是因为低依赖系统在功能、协议和系统容量方面与其更不可靠的对应系统有很大不同。在 Google,值班工程师将低依赖系统作为值班职责的一个重要部分。我们之所以使用这种策略,有几个原因:

  • 许多工程师参与值班轮班,但一次只有少部分工程师值班。这自然限制了参与验证的人员范围。

  • 当工程师值班时,他们可能需要依赖紧急路径。熟练使用低依赖系统可以减少值班工程师在真正紧急情况下切换到使用这些系统所需的时间,并避免意外的配置错误风险。

逐步将值班工程师转换为仅使用低依赖系统可以逐步实现,并且可以通过不同的方式实现,具体取决于每个系统的业务关键性。

当无法镜像流量时进行分割

作为请求镜像的替代方案,您可以将请求分割到不相交的服务器集之间。如果请求镜像不可行,例如当您无法控制客户端代码,但在请求路由级别进行负载均衡是可行的。因此,只有在替代组件使用相同的协议时,请求分割才有效,这在高容量和高可用性版本的组件中经常发生。

这种策略的另一个应用是将流量分布到一组故障域中。如果您的负载均衡针对单个故障域,您可以针对该域运行集中的实验。由于故障域的容量较低,攻击它并引发弹性响应需要更少的负载。您可以通过比较其他故障域的监控信号来量化实验的影响。通过增加负载放置和限流,您可以进一步提高实验的输出质量。

超额预订但防止自满

分配给客户但未使用的配额是资源的浪费。因此,为了最大限度地利用资源,许多服务通过一定合理的边际进行资源超额预订。资源的边际调用可能随时发生。弹性系统跟踪优先级,以便释放较低优先级的资源以满足对较高优先级资源的需求。但是,您应该验证系统是否能够可靠地释放这些资源,并且在可接受的时间内释放这些资源。

谷歌曾经有一个需要大量磁盘空间进行批处理的服务。用户服务优先于批处理,并为使用高峰分配了大量磁盘保留空间。我们允许批处理服务利用用户服务未使用的磁盘,但有一个特定条件:特定集群中的任何磁盘必须在X小时内完全释放。我们开发的验证策略包括定期将批处理服务从集群中移出,测量移动所需的时间,并在每次尝试中修复发现的任何新问题。这不是模拟。我们的验证确保承诺X小时 SLO 的工程师既有真实的证据又有真实的经验。

这些验证成本高,但大部分成本都由自动化吸收。负载均衡将成本限制在管理资源在源和目标位置的配置上。如果资源配置大部分是自动化的,例如云服务的情况,那么只需要运行脚本或 playbook 来执行一系列请求即可。

对于较小的服务或公司,定期执行再平衡的策略同样适用。对应用负载变化做出可预测响应的信心是可以为全球用户基础提供服务的软件架构的基础之一。

测量关键旋转周期

关键旋转在理论上很简单,但在实践中可能会带来令人不快的惊喜,包括完全的服务中断。在验证关键旋转是否有效时,您应该寻找至少两个不同的结果:

关键旋转延迟

完成单个旋转周期所需的时间

验证失去访问权限

确保旧密钥在旋转后完全无用

我们建议定期旋转密钥,以便它们保持随时准备好应对由安全威胁引发的不可协商的紧急密钥旋转。这意味着即使不必旋转密钥,也要进行旋转。如果旋转过程成本高,可以寻找降低成本的方法。

在谷歌,我们已经经历了测量关键旋转延迟对多个目标有所帮助:

  • 您可以了解使用该密钥的每个服务是否能够更新其配置。也许某个服务并不是为密钥旋转而设计的,或者虽然设计了但从未经过测试,或者更改破坏了以前的工作。

  • 您可以了解每项服务旋转密钥所需的时间。密钥旋转可能只是一个文件更改和服务器重启,也可能是逐步在所有世界地区推出。

  • 您会发现其他系统动态如何延迟密钥旋转过程。

测量密钥旋转延迟有助于我们形成对整个周期的现实预期,无论是在正常情况下还是在紧急情况下。考虑可能由密钥旋转或其他事件引起的回滚,服务超出错误预算的更改冻结,以及由于故障域而产生的序列化推出。

如何验证通过旧密钥丧失访问的可能情况特定。很难证明旧密钥的所有实例都被销毁,因此理想情况下,您可以证明尝试使用旧密钥失败,之后您可以销毁旧密钥。当这种方法不切实际时,您可以依赖密钥拒绝列表机制(例如 CRL)。如果您有一个中央证书颁发机构和良好的监控,您可能能够在任何 ACL 列出旧密钥的指纹或序列号时创建警报。

实用建议:从何处开始

设计弹性系统并不是一项微不足道的任务。这需要时间和精力,并且会转移其他有价值的工作。您需要根据您想要的弹性程度仔细考虑权衡,然后从我们描述的广泛选项中选择——适合您需求的少数或多数解决方案。

按成本顺序:

  1. 故障域和冲击半径控制具有较低的成本,因为它们的相对静态性质,但提供了显著的改进。

  2. 高可用性服务是成本效益的下一个解决方案。

考虑接下来的这些选项:

  1. 如果您的组织规模或风险规避需要投资于弹性的主动自动化,考虑部署负载分担和限流能力。

  2. 评估您的防御措施对抗 DoS 攻击的有效性(参见第十章)。

  3. 如果构建低依赖解决方案,请引入一个过程或机制,以确保它随着时间的推移保持低依赖性。

克服对投资于弹性改进的抵制可能很困难,因为好处表现为问题的缺失。以下论点可能有所帮助:

  • 部署故障域和冲击半径控制将对未来系统产生持久影响。隔离技术可以鼓励或强制执行操作故障域的良好分离。一旦建立,它们将不可避免地使设计和部署不必要耦合或脆弱的系统变得更加困难。

  • 定期更改和旋转密钥的技术和练习不仅确保了安全事件的准备,还为您提供了一般的加密灵活性,例如,知道您可以升级加密原语。

  • 部署高可用性服务实例的相对较低额外成本提供了一种廉价的方法,可以检查您可能能够提高服务可用性的程度。放弃也很便宜。

  • 负载分担和限流能力以及“控制退化”中涵盖的其他方法,减少了公司需要维护的资源成本。由此产生的用户可见改进通常适用于最受重视的产品特性。

  • 控制退化对于在防御 DoS 攻击时的第一反应的速度和有效性至关重要。

  • 低依赖解决方案相对昂贵,但在实践中很少被使用。要确定它们是否值得成本,可以帮助了解启动业务关键服务的所有依赖项需要多长时间。然后可以比较成本,并得出是否最好将时间投资在其他地方的结论。

无论您组合了什么样的弹性解决方案,都要寻找经济实惠的方式来持续验证它们,并避免削减成本,从而影响它们的有效性。投资于验证的好处在于,长期来看,可以锁定所有其他弹性投资的复合价值。如果您自动化这些技术,工程和支持团队可以专注于提供新的价值。自动化和监控的成本理想情况下将分摊到公司正在追求的其他努力和产品中。

您可能会定期耗尽可以投入弹性的金钱或时间。下一次有机会花更多这些有限资源时,首先考虑简化已部署的弹性机制的成本。一旦您对它们的质量和效率有信心,再尝试更多的弹性选项。

结论

在本章中,我们讨论了从设计阶段开始将弹性构建到系统的安全性和可靠性中的各种方法。为了提供弹性,人类需要做出选择。我们可以通过自动化优化一些选择,但对于其他选择,我们仍然需要人类。

可靠性属性的弹性有助于保持系统最重要的功能,使系统在负载过重或大规模故障的情况下不会完全崩溃。如果系统出现故障,此功能将延长响应者组织、防止更多损害或必要时进行手动恢复的时间。弹性有助于系统抵御攻击,并防御长期访问的企图。如果攻击者侵入系统,设计功能如爆炸半径控制将限制损害。

将设计策略基于深度防御。以与可用性和可靠性相同的方式检查系统的安全性。在本质上,深度防御就像您的防御的N+1 冗余。您不会把所有的网络容量都信任一个路由器或交换机,那么为什么要信任一个防火墙或其他防御措施呢?在设计深度防御时,始终假设并检查不同安全层的故障:外围安全的故障,端点的妥协,内部人员的攻击等。计划进行横向移动,以阻止它们的意图。

即使您设计系统具有弹性,也有可能在某个时刻弹性不足,系统会崩溃。下一章将讨论发生这种情况后会发生什么:如何恢复崩溃的系统,以及如何最小化崩溃造成的损害?

¹ 受保护的沙箱为不受信任的代码和数据提供了隔离的环境。

² 谷歌运行漏洞赏金奖励计划。

³ 请参阅 Singh, Soram Ranbir, Ajoy Kumar Khan, and Soram Rakesh Singh. 2016. “Performance Evaluation of RSA and Elliptic Curve Cryptography.” Proceedings of the 2nd International Conference on Contemporary Computing and Informatics: 302–306. doi:10.1109/IC3I.2016.7917979。

⁴ 这在《SRE 书》的第二十章中有描述。

⁵ 请参阅《SRE 书》第二十一章。

⁶ “开放式失败”和“关闭式失败”的概念指的是服务保持运行(可靠)或关闭(安全),分别。术语“开放式失败”和“关闭式失败”通常与“故障安全”和“故障安全”互换使用,如第一章所述。

⁷ 有关 Google 生产环境的描述,请参阅SRE 书籍第二章

⁸ 通常,该服务的实例仍然由基础服务器的许多副本提供,但它将作为单个逻辑隔间运行。

⁹ 例如,Google Cloud Platform 提供所谓的独占租户节点

¹⁰ 突发情况处理机制是一种可以绕过策略以允许工程师快速解决故障的机制。请参阅“突发情况处理”

¹¹ 使用 ACL 的系统必须失败关闭(安全),只有 ACL 条目明确授予访问权限。

¹² 请参阅SRE 书籍第三章

¹³ 这与单元测试、集成测试和负载测试不同,这些内容在第十三章中有介绍。

¹⁴ 这类似于 Unix 命令teestdin的操作。

第九章:面向恢复的设计

原文:9. Design for Recovery

译者:飞龙

协议:CC BY-NC-SA 4.0

由 Aaron Joyner,Jon McCune 和 Vitaliy Shipitsyn

与 Constantinos Neophytou,Jessie Yang 和 Kristina Bennett

现代分布式系统面临许多类型的故障,这些故障是由无意错误和蓄意恶意行为造成的。当暴露于不断累积的错误、罕见的故障模式或攻击者的恶意行为时,人类必须介入,以恢复甚至最安全和最有弹性的系统。

将失败或受损的系统恢复到稳定和安全状态可能会以意想不到的方式变得复杂。例如,回滚一个不稳定的发布版本可能会重新引入安全漏洞。推出新版本以修补安全漏洞可能会引入可靠性问题。这些风险性的缓解措施充满了更微妙的权衡。例如,在决定多快部署更改时,快速推出更有可能赢得与攻击者的竞赛,但也限制了您能够对其进行的测试量。您可能最终会广泛部署具有关键稳定性错误的新代码。

在紧张的安全或可靠性事件中开始考虑这些微妙之处以及您的系统缺乏处理它们的准备是远非理想的。只有有意识的设计决策才能使您的系统具备所需的可靠性和灵活性,以本能地支持各种恢复需求。本章涵盖了一些我们发现在准备我们的系统以促进恢复工作方面非常有效的设计原则。这些原则中的许多原则适用于各种规模,从全球范围的系统到单个机器内的固件环境。

我们正在从什么中恢复?

在我们深入探讨促进恢复的设计策略之前,我们将介绍一些导致系统需要恢复的情景。这些情景可以分为几个基本类别:随机错误、意外错误、恶意行为和软件错误。

随机错误

所有分布式系统都是由物理硬件构建的,所有物理硬件都会出现故障。物理设备的不可靠性以及它们运行的不可预测的物理环境导致了随机错误。随着支持系统的物理硬件数量的增加,分布式系统遭遇随机错误的可能性也增加。老化的硬件也会导致更多的错误。

一些随机错误比其他错误更容易恢复。系统的某个部分的完全故障或隔离,例如电源供应或关键网络路由器,是最简单的故障处理之一。[1]解决由意外位翻转引起的短暂损坏,或者由多核 CPU 中一个核上的故障指令引起的长期损坏则更为复杂。当这些错误悄无声息地发生时,它们尤其阴险。

系统外的基本不可预测事件也可能在现代数字系统中引入随机错误。龙卷风或地震可能会导致您突然永久地失去系统的特定部分。发电站或变电站故障或 UPS 或电池异常可能会影响向一个或多个机器提供电力。这可能会引入电压下降或波动,导致内存损坏或其他瞬态错误。

意外错误

所有分布式系统都是由人类直接或间接操作的,而所有人类都会犯错误。我们将意外错误定义为由怀着良好意图的人类造成的错误。人为错误率根据任务类型而异。粗略地说,任务复杂度增加,错误率也会增加。[3]

人类可能在系统的任何部分出现错误,因此你需要考虑人为错误如何在系统工具、系统和作业流程的整个堆栈中发生。意外错误也可能以系统外的随机方式影响你的系统,例如,如果用于无关施工的挖掘机切断了光纤电缆。

软件错误

迄今为止,我们已经讨论过的错误类型可以通过设计更改和/或软件来解决。用一个经典的引用和其推论来解释,所有的错误都可以通过软件来解决……除了软件中的错误。软件错误实际上只是意外错误的一个特殊延迟案例:在软件开发过程中发生的错误。你的代码会有 bug,你需要修复这些 bug。一些基本且广泛讨论的设计原则,例如模块化软件设计、测试、代码审查以及验证依赖 API 的输入和输出,可以帮助你解决 bug。第六章和第十二章更深入地讨论了这些主题。

在某些情况下,软件 bug 会模仿其他类型的错误。例如,缺乏安全检查的自动化可能会对生产进行突然而剧烈的更改,模仿恶意行为者。软件错误也会放大其他类型的错误,例如,传感器错误返回意外值,软件无法正确处理,或者在用户正常工作过程中绕过故障机制时出现的意外行为,看起来像是恶意攻击。

恶意行为

人类也可能故意对抗你的系统。这些人可能是有特权和高度了解的内部人员,有意为之。恶意行为者指的是一整类人积极地试图颠覆你的系统安全控制和可靠性,或者可能试图模仿随机、意外或其他类型的错误。自动化可以减少但无法消除对人类参与的需求。随着你的分布式系统规模和复杂性的增加,维护它的组织规模也必须与系统规模相适应(理想情况下,以次线性方式)。与此同时,组织中的人员违反你对他们的信任的可能性也在增加。

这些信任的违反可能来自滥用其对系统的合法权威,读取与其工作无关的用户数据,泄露或暴露公司机密,甚至积极致使系统长时间宕机的内部人员。这个人可能会短暂地做出错误的决定,有真正的恶意,成为社会工程攻击的受害者,甚至被外部行为者胁迫。

系统的第三方妥协也可能引入恶意错误。第二章深入介绍了恶意行为者的范围。在系统设计方面,无论恶意行为者是内部人员还是第三方攻击者,都采取相同的缓解策略。

恢复的设计原则

以下部分提供了一些基于我们多年分布式系统经验的恢复设计原则。这个列表并不是详尽无遗的,我们将提供进一步阅读的建议。这些原则也适用于各种组织,不仅仅是谷歌规模的组织。总的来说,在设计恢复时,重要的是要对可能出现的问题的广度和多样性持开放态度。换句话说,不要花时间担心如何对错误的微妙边缘情况进行分类;专注于准备从中恢复。

尽快进行设计(受策略保护)

在妥协或系统故障期间,有很大的压力要尽快恢复系统到预期的工作状态。然而,你用来快速更改系统的机制本身可能会冒着太快做出错误更改的风险,加剧问题。同样,如果你的系统被恶意入侵,过早的恢复或清理行动可能会引发其他问题,例如,你的行动可能会让对手知道他们已经被发现。我们发现在设计支持可变恢复速率的系统时,平衡涉及的权衡的一些方法是有效的。

要从我们的四类错误中恢复你的系统,或者更好的是,避免需要恢复,你必须能够改变系统的状态。在构建更新机制(例如软件/固件推出过程、配置更改管理程序或批处理调度服务)时,我们建议设计更新系统以尽可能快地运行(或更快,以符合实际限制)。然后,添加控件来限制变更速率,以匹配你当前的风险和干扰政策。将执行推出的能力与推出的速率和频率政策分离有几个优点。

任何组织的推出需求和政策随着时间而改变。例如,在早期,一家公司可能每月进行推出,而且从不在夜间或周末进行。如果推出系统是围绕政策变化设计的,政策的变化可能需要进行困难的重构和侵入性的代码更改。如果推出系统的设计清楚地将变更的时间和速率与变更的行为和内容分开,那么就更容易调整不可避免的管理时间和变更速率的政策变化。

有时,你在推出过程中收到的新信息会影响你的应对方式。想象一下,作为对内部发现的安全漏洞的响应,你正在部署一个内部开发的补丁。通常情况下,你不需要以足够快的速度部署这个变更,以至于冒着破坏你的服务的风险。然而,你的风险计算可能会因为景观的变化而改变(参见第七章):如果你在推出过程中发现漏洞现在已经是公开的信息,并且正在野外积极利用,你可能希望加快程序。

必然会有一个时刻,突然或意外的事件会改变你愿意接受的风险。因此,你希望非常非常快地推出变更。例子可以从安全漏洞(ShellShock,Heartbleed 等)到发现活跃的妥协。*我们建议设计你的紧急推送系统只是将你的常规推送系统调到最大。*这也意味着你的正常推出系统和紧急回滚系统是一样的。我们经常说,未经测试的紧急实践在你需要它们时不起作用。使你的常规系统能够处理紧急情况意味着你不必维护两个单独的推送系统,并且你经常练习你的紧急发布系统。

如果应对紧张局势只需要你修改速率限制以便快速推出变更,你就会更有信心你的推出工具能够按预期工作。然后你可以把精力集中在其他不可避免的风险上,比如快速部署变更中的潜在错误,或者确保关闭攻击者可能用来访问你的系统的漏洞。

我们从我们内部 Linux 发行版的部署演变中学到了这些教训。直到最近,Google 在数据中心安装所有机器时都使用“基础”或“黄金”镜像,其中包含已知的静态文件集。每台机器有一些特定的定制,如主机名、网络配置和凭据。我们的政策是每个月在整个机群中部署新的“基础”镜像。多年来,我们围绕这一政策和工作流程构建了一套工具和软件更新系统:将所有文件捆绑成压缩存档,由高级 SRE 审查一组更改,然后逐渐更新机群中的机器到新镜像。

我们围绕这一政策构建了我们的部署工具,并设计了工具来将特定基础镜像映射到一组机器。我们设计了配置语言来表达如何在几周的时间内改变该映射,然后使用一些机制在基础镜像之上添加异常。一个异常包括越来越多的单个软件包的安全补丁:随着异常列表变得更加复杂,我们的工具遵循每月模式就变得不那么合理了。

作为回应,我们决定放弃每月更新基础镜像的假设。我们设计了更精细的发布单元,对应于每个软件包。我们还构建了一个干净的新 API,指定了要安装的精确软件包集,一次一个机器,放在现有的部署机制之上。如图 9-1 所示,这个 API 解耦了定义几个不同方面的软件:

  • 部署和每个软件包应该更改的速率

  • 定义所有机器当前配置的配置存储

  • 管理将更新应用到每台机器的部署执行器

将软件包部署到机器的工作流程的演变

图 9-1:将软件包部署到机器的工作流程的演变

因此,我们可以独立开发每个方面。然后,我们重新利用现有的配置存储来指定应用于每台机器的所有软件包的配置,并构建了一个部署系统来跟踪和更新每个软件包的独立部署。

通过将镜像构建与每月部署策略解耦,我们可以为不同软件包启用更广泛的发布速度范围。同时,虽然仍然保持对机群中大多数机器的稳定和一致的部署,一些测试机器可以跟随所有软件的最新构建。更好的是,解耦策略解锁了整个系统的新用途。我们现在使用它定期向整个机群分发一部分经过仔细审核的文件。我们还可以通过调整一些速率限制并批准某种类型的软件包的发布比正常情况下更快地进行,简单地使用我们的正常工具进行紧急发布。最终结果更简单,更有用,更安全。

限制对外部时间概念的依赖

时间——即由手表和挂钟等设备报告的普通时间——是一种状态。因为通常无法改变系统体验时间流逝的方式,您的系统融入挂钟时间的任何位置都可能威胁到您完成恢复的能力。在您进行恢复工作的时间和系统上次正常运行的时间之间的不匹配可能导致意想不到的系统行为。例如,涉及重放数字签名交易的恢复可能会失败,如果一些交易是由过期证书签名的,除非您在验证证书时考虑原始交易日期。

如果您的系统时间依赖性更有可能引入安全或可靠性问题,如果它依赖于您无法控制的外部时间概念。这种模式以多种类型的错误形式出现,例如软件错误,如Y2KUnix 时代翻转,或者开发人员选择将证书到期时间设置得太远,以至于“这不再是他们的问题”。明文或未经身份验证的NTP连接也会引入风险,如果攻击者能够控制网络。代码中的固定日期或时间偏移展现出一种代码异味,表明您可能正在制造一个定时炸弹。

注意

将事件与挂钟时间绑定通常是一种反模式。我们建议使用以下之一,而不是挂钟时间:

  • 费率

  • 手动推进前进的概念,如时代编号或版本编号

  • 有效性列表

如第八章中所述,谷歌的 ALTS 传输安全系统在数字证书中不使用到期时间,而是依赖于撤销系统。活动撤销列表由定义证书序列号有效与撤销范围的向量组成,并且不依赖于挂钟时间。您可以通过定期健康地推送更新的撤销列表来实现隔离目标,以创建时间区段。如果您怀疑对手可能已经获得了基础密钥的访问权限,您可以紧急推送新的撤销列表来撤销证书,并且您可以在异常情况下停止定期推送以进行调试或取证。有关该特定主题的更多讨论,请参见“使用显式撤销机制”。

依赖挂钟时间的设计选择也可能导致安全弱点。由于可靠性约束,您可能会有诱惑力禁用证书有效性检查以执行恢复。然而,在这种情况下,治疗比疾病更糟糕——最好是省略证书到期时间(从允许登录到一组服务器的 SSH 密钥对)而不是跳过有效性检查。要提供一个显着的例外,挂钟时间用于有意限时访问。例如,您可能希望要求大多数员工每天重新进行身份验证。在这种情况下,重要的是要有一条修复系统的路径,而不依赖于挂钟时间。

依赖绝对时间也可能导致问题,当您尝试从崩溃中恢复,或者期望单调递增时间的数据库尝试从损坏中恢复时。恢复可能需要详尽的事务重放(随着数据集的增长,这很快变得不可行),或者尝试在多个系统之间协调地回滚时间。举个简单的例子:在具有不准确时间概念的系统之间进行日志相关会给您的工程师增加不必要的间接层,使意外错误更加普遍。

您还可以通过使用时代或版本的推进来消除对挂钟时间的依赖,这需要系统的所有部分围绕一个整数值进行协调,该值表示“有效”与“过期”的前进。时代可以是存储在分布式系统组件中的整数,例如锁定服务,或者是根据策略向前(只能向前移动)推进的机器本地状态。为了使您的系统能够尽快进行发布,您可能会设计它们以允许快速的时代推进。单个服务可能负责宣布当前时代或启动时代推进。在遇到问题时,您可以暂停时代的推进,直到您理解并纠正问题。回到我们先前的公钥示例:尽管证书可能会过期,但您不会因为可以停止时代的推进而被诱使完全禁用证书验证。时代与“最低可接受安全版本号”中讨论的 MASVN 方案有一些相似之处。

注意

过于激进地增加时代值可能会导致翻转或溢出。要谨慎对待系统部署更改的速度,以及您可以容忍地跳过多少中间时代或版本值。

暂时控制您系统的对手可能通过大幅加速时代的推进或导致时代翻转来对系统造成持久性的损害。解决这个问题的常见方法是选择具有足够大范围的时代值,并建立一个基础的后备速率限制,例如,将 64 位整数速率限制为每秒增加一次。硬编码后备速率限制是我们先前设计建议的一个例外,即尽快推出更改并添加策略以指定更改的速率。然而,在这种情况下,很难想象有理由改变系统状态超过一秒一次,因为您将要处理数十亿年的时间。这种策略也是合理的,因为 64 位整数在现代硬件上通常是廉价的。

即使在等待经过的挂钟时间是可取的情况下,也要考虑仅仅测量经过的时间,而不需要实际的日期时间。即使系统不知道挂钟时间,后备速率限制也能起作用。

深入探讨:回滚代表了安全之间的权衡

在事件响应期间恢复的第一步是通过安全地回滚任何可疑更改来减轻事件。需要人工注意的生产问题的大部分是自我造成的(参见“意外错误”和“软件错误”),这意味着对系统的预期更改包含错误或其他错误配置,导致了事件。当这种情况发生时,可靠性的基本原则要求系统尽快而安全地回滚到上一个已知的良好状态。

在其他情况下,您需要防止回滚。在修补安全漏洞时,您经常在与攻击者竞赛,试图在攻击者利用漏洞之前部署补丁。一旦成功部署补丁并显示稳定,您需要防止攻击者应用可能重新引入漏洞的回滚,同时仍然保留自己自愿回滚的选项——因为安全补丁本身是代码更改,它们可能包含自己的错误或漏洞。

考虑到这些因素,确定回滚的适当条件可能会很复杂。应用层软件是一个更为直接的情况。系统软件,如操作系统或特权包管理守护程序,可以轻松终止和重新启动任务或进程。您可以将不良版本的名称(通常是唯一的标签字符串、数字或哈希¹⁰)收集到一个拒绝列表中,然后将其纳入部署系统的发布策略中。或者,您可以管理一个允许列表,并构建您的自动化以包括部署的应用软件在该列表中。

负责处理自己更新的特权或低级系统组件更具挑战性。我们称这些组件为自更新。例如,包管理守护程序通过覆盖自己的可执行文件并重新执行自己来更新自己,或者固件图像(如 BIOS)会在自身上重新刷写一个替换图像,然后强制重新启动。如果这些组件被恶意修改,它们可能会主动阻止自己被更新。硬件特定的实现要求增加了挑战。您需要回滚控制机制,即使对于这些组件,但是所需的行为本身可能很难定义。让我们考虑两个示例策略及其缺陷,以更好地理解问题:

允许任意回滚

这种解决方案并不安全,因为导致您执行回滚的任何因素都可能重新引入已知的安全漏洞。漏洞越老或越明显,稳定的、武器化的利用这些漏洞的可能性就越大。

永远不要允许回滚

这种解决方案消除了返回到已知稳定状态的路径,只允许您向前移动到更新的状态。这是不可靠的,因为如果更新引入了错误,您将无法回滚到上一个已知的良好版本。这种方法隐含地要求构建系统生成新版本,以便您可以向前滚动,这会给构建和发布工程基础设施增加时间和可避免的依赖关系。

除了这两种极端方法之外,还有许多其他实用的权衡方案。这些包括:

  • 使用拒绝列表

  • 使用安全版本号(SVNs)和最低可接受安全版本号(MASVNs)

  • 轮换签名密钥

在接下来的讨论中,我们假设在所有情况下,更新都经过了加密签名,并且签名覆盖了组件图像及其版本元数据。

在这里讨论的三种技术的组合可能最好地管理自更新组件的安全性/可靠性权衡。然而,这种组合的复杂性,以及它对ComponentState的依赖,使这种方法成为一项巨大的工作。我们建议逐步引入一个功能,并允许足够的时间来识别您引入的每个组件的任何错误或特殊情况。最终,所有健康的组织都应该使用密钥轮换,但拒绝列表和 MASVN 功能对于高速响应是有用的。

拒绝列表

当您在发布版本中发现错误或漏洞时,您可能希望构建一个拒绝列表,以防止已知的不良版本被(重新)激活,也许可以通过在组件本身中硬编码拒绝列表来实现。在下面的示例中,我们将其写为Release[DenyList]。在组件更新到新发布的版本后,它会拒绝更新到拒绝列表中的版本。


def IsUpdateAllowed(self, Release) -> bool:
  return Release[Version] not in self[DenyList]

不幸的是,这个解决方案只解决了意外错误,因为硬编码的拒绝列表呈现了一个无法解决的安全/可靠性权衡。如果拒绝列表总是留有至少一个较旧的已知良好的镜像的回滚空间,该方案就容易受到解压的攻击——攻击者可以逐步回滚版本,直到他们到达包含已知漏洞的较旧版本,然后利用这些漏洞。这种情况基本上就是之前描述的“允许任意回滚”的极端情况,中间还有一些跳跃。或者,将拒绝列表配置为完全阻止关键安全更新的回滚会导致“永远不允许回滚”的极端情况,伴随着可靠性的缺陷。

如果您正在从安全或可靠性事件中恢复,当您的系统可能在整个系统中进行多个更新时,硬编码的拒绝列表是一个很好的选择,可以避免意外错误。向列表中追加一个版本是快速且相对容易的,因为这样做对任何其他版本的有效性几乎没有影响。然而,您需要一个更强大的策略来抵抗恶意攻击。

更好的拒绝列表解决方案将拒绝列表编码在自更新组件之外。在下面的示例中,我们将其写为ComponentState[DenyList]。这个拒绝列表在组件升级和降级时都会保留下来,因为它独立于任何单个发布,但组件仍然需要逻辑来维护拒绝列表。每个发布可能合理地编码在其发布时已知的最全面的拒绝列表:Release[DenyList]。然后,维护逻辑将这些列表合并并在本地存储(请注意,我们写self[DenyList]而不是Release[DenyList],以表明“self”已安装并且正在运行):


ComponentState[DenyList] = ComponentState[DenyList].union(self[DenyList))

检查临时更新的有效性,拒绝拒绝列表中的更新(不要明确引用“self”,因为它对拒绝列表的贡献已经反映在ComponentState中,即使未来版本被安装后仍然存在):


def IsUpdateAllowed(self, Release, ComponentState) -> bool:
  return Release[Version] not in ComponentState[DenyList]

现在,您可以有意地进行安全/可靠性权衡,作为一种政策。当您决定在Release[DenyList]中包含什么时,您可以权衡解压攻击的风险与不稳定发布的风险。

即使您将拒绝列表编码在一个ComponentState数据结构中,该数据结构在自更新组件之外维护,这种方法也有缺点:

  • 即使拒绝列表存在于集中式部署系统的配置意图之外,您仍然需要监视和考虑它。

  • 如果一个条目被意外添加到拒绝列表中,您可能希望将该条目从列表中删除。然而,引入删除功能可能会打开解压攻击的大门。

  • 拒绝列表可能会无限增长,最终达到存储的大小限制。您如何管理拒绝列表的垃圾收集

最低可接受的安全版本号

随着时间的推移,拒绝列表会变得庞大而难以控制,因为条目被追加。您可以使用一个单独的安全版本号,写成Release[SVN],从拒绝列表中删除较旧的条目,同时防止它们被安装。因此,您减少了对系统负责的人的认知负担。

保持Release[SVN]独立于其他版本概念,可以以一种紧凑且在数学上可比较的方式逻辑地跟踪大量发布,而无需像拒绝列表那样需要额外的空间。每当您应用关键安全修复并证明其稳定性时,您都会增加Release[SVN],标记一个安全里程碑,用于调整回滚决策。因为您对每个版本的安全状态都有一个简单的指示器,所以您可以灵活地进行普通的发布测试和资格认证,并且有信心在发现错误或稳定性问题时迅速而安全地做出回滚决策。

请记住,您还希望防止恶意行为者以某种方式将系统回滚到已知的不良或易受攻击的版本。为了防止这些行为者在您的基础设施中立足并利用该立足点阻止恢复,您可以使用 MASVN 来定义一个低水位标记,低于该标记,您的系统不应该运行。这必须是一个有序值(而不是加密哈希),最好是一个简单的整数。您可以像管理拒绝列表一样管理 MASVN:

  • 每个发布版本都包括一个 MASVN 值,反映了其发布时的可接受版本。

  • 您在部署系统之外维护一个全局值,写为ComponentState[MASVN]

作为应用更新的先决条件,所有发布都包括逻辑验证试探性更新的Release[SVN]至少与ComponentState[MASVN]一样高。它被表达为伪代码如下:


def IsUpdateAllowed(self, Release, ComponentState) -> bool:
  return Release[SVN] >= ComponentState[MASVN]

全球ComponentState[MASVN]的维护操作不是部署过程的一部分。相反,维护是在新版本初始化时进行的。您需要在每个发布中硬编码一个目标 MASVN - 您希望在创建该发布时强制执行该组件的 MASVN,写为Release[MASVN]

当部署和执行新版本时,它会将其Release[MASVN](写为self[MASVN],以引用已安装和运行的版本)与ComponentState[MASVN]进行比较。如果Release[MASVN]高于现有的ComponentState[MASVN],那么ComponentState[MASVN]将更新为新的更大值。实际上,这个逻辑每次组件初始化时都会运行,但ComponentState[MASVN]只有在成功更新为更高的Release[MASVN]后才会更改。它被表达为伪代码如下:


ComponentState[MASVN] = max(self[MASVN], ComponentState[MASVN])

这个方案可以模拟前面提到的两种极端政策中的任何一种:

  • 通过永远不修改Release[MASVN]来允许任意回滚

  • 通过将Release[MASVN]Release[SVN]同步修改,永远不允许回滚

实际上,Release[MASVN]通常在第i+1 个版本中提高,之前的版本解决了安全问题。这确保了i–1 或更旧的版本永远不会再次执行。由于ComponentState[MASVN]是外部的,版本i在安装i+1 后不再允许降级,即使它最初允许这样的降级。图 9-2 说明了三个发布及其对ComponentState[MASVN]的影响的示例值。

三个发布及其对 ComponentState[MASVN]的影响的序列

图 9-2:三个发布及其对 ComponentState[MASVN]的影响的序列

为了减轻第i–1 个版本中的安全漏洞,第i个版本包括安全补丁和递增的Release[SVN]Release[MASVN]在第i个版本中不会改变,因为即使安全补丁也可能存在错误。一旦第i个版本在生产中被证明是稳定的,下一个版本,i+1,会递增 MASVN。这表示安全补丁现在是强制性的,没有它的版本是不允许的。

与“尽快进行”设计原则保持一致,MASVN 方案将合理的回滚目标策略与执行回滚的基础设施分开。在自更新组件中引入特定的 API 并从集中式部署管理系统接收命令来增加ComponentState[MASVN]是技术上可行的。通过该命令,您可以在部署管道的后期对接收更新的组件提高ComponentState[MASVN],在足够多的设备上验证了发布版本后,您对其能够按计划工作有很高的信心。在响应活动妥协或特别严重的漏洞时,这样的 API 可能非常有用,其中速度至关重要,风险容忍度比正常情况下更高。

到目前为止,这个例子避免了引入专门的 API 来改变ComponentStateComponentState是一组影响您通过更新或回滚来恢复系统能力的敏感值。它是组件本地的,并且与配置的意图外部的一个集中式自动化直接控制。在面对并行开发、测试、金丝雀分析和部署的情况下,每个个体组件经历的软件/固件版本序列可能会在类似或相同设备的群集中有所不同。一些组件或设备可能会经历完整的发布版本集合,而其他可能会经历许多回滚。还有一些可能经历最小的变化,并直接从有缺陷或有漏洞的版本跳转到下一个稳定的、合格的发布版本。

因此,使用 MASVN 是一种与拒绝列表结合使用的有用技术,用于自更新组件。在这种情况下,您可能会非常迅速地执行拒绝列表操作,可能是在事件响应条件下。然后在更平静的情况下执行 MASVN 维护,以清除拒绝列表并永久排除(基于每个组件实例)任何容易受到攻击或足够旧的发布版本,并且永远不打算在给定的组件实例上再次执行。

旋转签名密钥

许多自更新组件包括支持对临时更新进行加密认证的功能,换句话说,组件的发布周期的一部分包括对该发布进行加密签名。这些组件通常包括已知公钥的硬编码列表,或作为ComponentState的一部分支持独立密钥数据库。例如:


def IsUpdateAllowed(self, Release, KeyDatabase) -> bool:
  return VerifySignature(Release, KeyDatabase)

您可以通过修改组件信任的一组公钥来防止回滚,通常是为了删除旧的或受损的密钥,或者引入一个新的密钥来签署未来的发布版本。较旧的发布版本将失效,因为较新的发布版本不再信任用于验证较旧发布版本上的签名的公共签名验证密钥。然而,您必须小心地管理密钥轮换,因为突然从一个签名密钥更改到另一个可能会使系统面临可靠性问题。

或者,您可以通过引入一个新的更新签名验证密钥k+1,同时与旧的验证密钥k一起允许使用任一密钥进行认证的更新。一旦稳定性得到证明,您就可以放弃对密钥k的信任。这种方案需要对发布版本的多个签名进行支持,并在验证候选更新时需要多个验证密钥。它还具有签名密钥轮换的优势——这是加密密钥管理的最佳实践——并且因此在事件发生后可能会起作用。

密钥轮换可以帮助您从非常严重的妥协中恢复,攻击者设法暂时控制了发布管理并签署并部署了Release[MASVN]设置为最大值的发布。在这种类型的攻击中,通过将ComponentState[MASVN]设置为其最大值,攻击者迫使您将Release[SVN]设置为其最大值,以便未来的发布能够可行,从而使整个 MASVN 方案无效。作为回应,您可以在由新密钥签署的新发布中撤销受损的公钥,并添加专用逻辑来识别异常高的ComponentState[MASVN]并将其重置。由于这种逻辑本身是微妙且潜在危险的,您应该谨慎使用,并在它们达到目的后立即积极撤销任何包含它的发布。

本章不涵盖严重和有针对性的妥协事件的全部复杂性。有关更多信息,请参阅第十八章。

回滚固件和其他硬件中心的约束

具有相应固件的硬件设备——如机器及其 BIOS,或网络接口卡(NIC)及其固件——是自更新组件的常见表现形式。这些设备在健壮的 MASVN 或密钥轮换方案方面提出了额外的挑战,我们在这里简要涉及。这些细节在恢复中发挥着重要作用,因为它们有助于实现可扩展或自动化的从潜在恶意行为中恢复。

有时,只可编程一次(OTP)设备如保险丝被 ROM 或固件用于通过存储ComponentState[MASVN]来实现一种仅向前的 MASVN 方案。这些方案存在重大的可靠性风险,因为回滚是不可行的。额外的软件层有助于解决物理硬件的约束。例如,OTP 支持的ComponentState[MASVN]覆盖了一个小型、单一用途的引导加载程序,其中包含其自己的 MASVN 逻辑,并且具有对单独的可变 MASVN 存储区的独占访问。然后,该引导加载程序将更健壮的 MASVN 语义暴露给更高级别的软件堆栈。

对于验证签名的硬件设备,有时使用 OTP 存储器来存储与这些密钥相关的公钥(或其哈希)和吊销信息。支持的密钥轮换或吊销次数通常受到严重限制。在这些情况下,一个常见的模式再次是使用 OTP 编码的公钥和吊销信息来验证一个小型引导加载程序。该引导加载程序然后包含其自己的验证和密钥管理逻辑层,类似于 MASVN 的示例。

当处理大量积极利用这些机制的硬件设备时,管理备件可能是一个挑战。备件在库存中停留多年,然后被部署时将必然具有非常旧的固件。这些旧的固件必须进行更新。如果旧的密钥完全不再使用,并且更新的发布仅由在备件最初制造时不存在的新密钥签署,那么新的更新将无法验证。

注意

一种解决方案是通过一系列升级来引导设备,确保它们在密钥轮换事件期间停留在所有信任旧密钥和新密钥的发布上。另一种解决方案是支持每个发布的多个签名。即使更新的映像(和已更新以运行这些更新映像的设备)不信任旧的验证密钥,这些更新的映像仍然可以携带由该旧密钥签名的签名。只有旧的固件版本才能验证该签名——这是一种期望的操作,允许它们在被剥夺更新后恢复。

考虑设备在其生命周期内可能使用多少密钥,并确保设备具有足够的空间来存储密钥和签名。例如,一些 FPGA 产品支持多个密钥用于验证或加密它们的比特流。¹³

深入探讨:使用显式吊销机制

吊销系统的主要作用是阻止某种访问或功能。面对主动妥协,吊销系统可以成为救命稻草,让您快速吊销受攻击者控制的凭证,并恢复对系统的控制。然而,一旦建立了吊销系统,意外或恶意行为可能会导致可靠性和安全性后果。如果可能的话,在设计阶段考虑这些问题。理想情况下,吊销系统应该在任何时候都能够履行其职责,而不会引入太多自身的安全和可靠性风险。

为了说明有关吊销的一般概念,我们将考虑以下不幸但常见的情景:您发现攻击者以某种方式控制了有效凭证(例如允许登录到一组服务器的客户端 SSH 密钥对),并且您希望吊销这些凭证。

注意

吊销是一个涉及到恢复、安全性和可靠性许多方面的复杂话题。本节仅讨论了与恢复相关的吊销的一些方面。其他要探讨的主题包括何时使用允许列表与拒绝列表,如何以崩溃恢复的方式卫生地旋转证书,以及如何在部署过程中安全地进行新更改的试点。本书的其他章节提供了许多这些主题的指导,但请记住,没有一本单独的书是足够的参考。

吊销证书的集中式服务

您可能选择使用集中式服务来吊销证书。这种机制通过要求您的系统与存储证书有效性信息的集中式证书有效性数据库进行通信,从而优先考虑安全性。您必须仔细监控和维护这个数据库,以保持系统的安全性,因为它成为了哪些证书是有效的的权威记录存储。这种方法类似于构建一个独立于用于实现更改的服务的独立速率限制服务,如本章前面讨论的那样。然而,要求与证书有效性数据库通信确实有一个缺点:如果数据库宕机,所有其他依赖系统也会宕机。如果证书有效性数据库不可用,很容易产生失败开放的强烈诱惑,以便其他系统也不会变得不可用。请谨慎处理!

失败开放

失败开放避免了锁定和简化了恢复,但也带来了一个危险的权衡:这种策略规避了对滥用或攻击的重要访问保护。即使部分失败开放的情况也可能会引起问题。例如,想象一下,证书有效性数据库依赖的系统宕机了。假设数据库依赖于时间或时代服务,但接受所有正确签名的凭证。如果证书有效性数据库无法访问时间/时代服务,那么进行相对简单的拒绝服务攻击的攻击者,例如通过压倒时间/时代服务的网络链接,可能会重新使用甚至非常旧的吊销凭证。这种攻击之所以有效,是因为在拒绝服务攻击持续期间,吊销的证书再次有效。在您尝试恢复时,攻击者可能会侵入您的网络或找到新的传播方式。

与其失败开放,您可能希望将已知的良好数据从吊销服务分发到个别服务器,以吊销列表的形式,节点可以在本地缓存。然后,节点根据他们对世界状态的最佳理解继续进行,直到他们获得更好的数据。这种选择比超时和失败开放政策安全得多。

直接处理紧急情况

为了快速吊销密钥和证书,您可能希望设计基础设施直接处理紧急情况,通过部署更改到服务器的authorized_users或 Key Revocation List (KRL)文件。¹⁵这种解决方案在多个方面都很麻烦。

注意

在处理少量节点时,直接管理authorized_keysknown_hosts文件尤其诱人,但这种做法扩展性差,会在整个服务器群中模糊真相。很难确保一组给定的密钥已从所有服务器的文件中删除,特别是如果这些文件是唯一的真相来源。

与直接管理authorized_keysknown_hosts文件不同,您可以通过集中管理密钥和证书,并通过吊销列表将状态分发到服务器,以确保更新过程是一致的。实际上,部署显式吊销列表是在您以最大速度移动时最小化不确定性的机会:您可以使用通常的机制来更新和监控文件,包括您在各个节点上使用的速率限制机制。

消除对准确时间概念的依赖

使用显式吊销具有另一个优点:对于证书验证,这种方法消除了对准确时间概念的依赖。无论是意外还是恶意造成的,不正确的时间都会对证书验证造成严重破坏。例如,旧证书可能会突然再次变为有效,从而让攻击者进入,而正确的证书可能会突然无法通过验证,导致服务中断。这些都是您不希望在紧张的妥协或服务中断期间经历的复杂情况。

最好让系统的证书验证依赖于您直接控制的方面,例如推送包含根授权公钥或包含吊销列表的文件。推送文件的系统、文件本身以及真相的中央来源可能比时间的分发更安全、更有维护性和更受监控。然后,恢复只需简单地推送文件,监控只需检查这些是否是预期的文件——这些是您的系统已经使用的标准流程。

规模化吊销凭据

在使用显式吊销时,重要的是要考虑可扩展性的影响。可扩展的吊销需要谨慎,因为部分攻破您系统的攻击者可以利用这种部分妥协作为拒绝进一步服务的强大工具,甚至可能吊销整个基础设施中的每个有效凭据。继续之前提到的 SSH 示例,攻击者可能会尝试吊销所有 SSH 主机证书,但您的机器需要支持操作,如更新 KRL 文件以应用新的吊销信息。您如何保护这些操作免受滥用?

在更新 KRL 文件时,盲目地用新文件替换旧文件是麻烦的根源¹⁶——单次推送可能会吊销整个基础设施中的每个有效凭据。一个保护措施是让目标服务器在应用新 KRL 之前评估新 KRL,并拒绝吊销自己凭据的任何更新。忽略吊销所有主机凭据的 KRL 将被所有主机忽略。因为攻击者的最佳策略是吊销一半的机器,所以您至少可以保证在每次 KRL 推送后,至少一半的机器仍将正常运行。恢复和恢复一半的基础设施要容易得多,比恢复全部要容易得多。¹⁷

避免风险异常

由于规模,大型分布式系统可能会遇到分发吊销列表的问题。这些问题可能会限制您部署新的吊销列表的速度,并在删除受损凭据时减慢您的响应时间。

为了解决这个缺点,您可能会想要构建一个专门的“紧急”吊销列表。然而,这种解决方案可能不太理想。由于您很少使用紧急列表,当您最需要它时,这种机制可能不太可能起作用。更好的解决方案是对吊销列表进行分片,以便您可以逐步更新它。这样,在紧急情况下吊销凭据只需要更新部分数据。始终使用分片意味着您的系统始终使用多部分吊销列表,并且在正常和紧急情况下使用相同的机制。

同样,要注意添加“特殊”帐户(例如提供对高级员工的直接访问权限的帐户),这些帐户可以绕过吊销机制。这些帐户对攻击者非常有吸引力 - 对这种帐户的成功攻击可能使您所有的吊销机制失效。

了解您的预期状态,直到字节。

从任何类别的错误中恢复 - 无论是随机的、意外的、恶意的还是软件错误 - 都需要将系统恢复到已知的良好状态。如果您确实知道系统的预期状态并且有一种方法来读取部署状态,那么这样做就会更容易。这一点可能显而易见,但不知道预期状态是问题的常见根源。在每一层(每个服务、每个主机、每个设备等)彻底编码预期状态并减少可变状态,使得在返回到良好工作状态时更容易识别。最终,彻底编码预期状态是卓越自动化、安全、入侵检测和恢复到已知状态的基础。

主机管理

假设您管理一个单独的主机,比如物理机、虚拟机,甚至一个简单的 Docker 容器。您已经建立了基础设施来执行自动恢复,这带来了很多价值,因为它可以高效地处理可能发生的许多问题,比如在SRE 书的第七章中详细讨论的问题。为了实现自动化,您需要对该单独机器的状态进行编码,包括运行在上面的工作负载。您需要编码足够的信息,以便自动化安全地将机器恢复到良好状态。在谷歌,我们在每个抽象层次上都应用这种范例,上下硬件和软件堆栈。

谷歌的系统将我们主机的软件包分发到机器群中,如“尽快设计(受策略保护)”中所述,以非常规的方式持续监视系统的整个状态。每台机器不断监视其本地文件系统,维护一个包括文件系统中每个文件的名称和加密校验和的映射。我们使用一个中央服务收集这些映射,并将它们与每台机器的分配软件包的预期状态进行比较。当我们发现预期状态与当前状态之间存在偏差时,我们将该信息附加到偏差列表中。

由于将各种恢复手段统一为一个过程,捕获机器状态的策略为恢复提供了强大的优势。如果宇宙射线随机损坏了磁盘上的一个位,我们会发现校验和不匹配并修复偏差。如果机器的预期状态发生了变化,因为一个组件的软件部署无意中改变了另一个组件的文件,改变了该文件的内容,我们会修复偏差。如果有人试图在正常管理工具和审查流程之外修改机器的本地配置(无论是意外还是恶意),我们也会修复该偏差。您可能选择通过重新映像整个系统来修复偏差,这种方法不太复杂,更容易实现,但在规模上更具破坏性。

除了捕获磁盘上文件的状态之外,许多应用程序还具有相应的内存状态。自动化恢复必须修复这两种状态。例如,当 SSH 守护程序启动时,它会从磁盘读取配置,并且除非受到指示,否则不会重新加载配置。为了确保内存状态根据需要进行更新,每个软件包都需要具有幂等的post_install命令,每当其文件出现偏差时都会运行。OpenSSH软件包的post_install会重新启动 SSH 守护程序。类似的pre_rm命令会在删除文件之前清理任何内存状态。这些简单的机制可以维护机器的所有内存状态,并报告和修复偏差。

对这种状态进行编码,让自动化检查每个偏差是否存在任何恶意差异。关于机器状态的丰富信息在安全事件后的取证分析中也非常有价值,帮助您更好地理解攻击者的行动和意图。例如,也许攻击者找到了一种方法在一些机器上存放恶意 shellcode,但无法逆向监控和修复系统,后者恢复了一个或多个机器上的意外变化。攻击者很难掩盖自己的行踪,因为中央服务会注意到并记录主机报告的偏差。

总之,在这个抽象级别上,所有状态变化都是相等的。您可以以类似的方式自动化、保护和验证所有状态变化,将失败的金丝雀分析回滚和更新的 bash 二进制文件的紧急部署视为常规变化。使用相同的基础设施使您能够就如何快速应用每个变化做出一致的政策决策。在这个级别上的速率限制可防止不同类型的变化之间意外碰撞,并建立最大可接受的变化速率。

设备固件

对于固件更新,您还可以在更低的层次上捕获状态。现代计算机中的各个硬件部件都有自己的软件和配置参数。出于安全和可靠性的考虑,您至少应该跟踪每个设备固件的版本。理想情况下,您应该捕获该固件可用的所有设置,并确保它们设置为预期值。

在管理谷歌机器的固件及其配置时,我们利用了与我们用于管理主机软件更新和偏差分析的相同系统和流程(参见“主机管理”)。自动化安全地分发所有固件的预期状态作为一个软件包,报告任何偏差,并根据我们的速率限制政策和其他处理中断的政策来修复偏差。

通常,预期状态并不直接暴露给监视文件系统的本地守护程序,它对设备固件没有特殊知识。我们通过允许每个软件包引用激活检查来解耦与硬件交互的复杂性,即软件包中定期运行的脚本或二进制文件,以确定软件包是否正确安装。脚本或二进制文件会执行与硬件通信和比较固件版本和配置参数的操作,然后报告任何意外的偏差。这种功能对于恢复特别有用,因为它赋予主题专家(即子系统所有者)采取适当措施来解决他们的专业领域中的问题。例如,自动化跟踪目标状态、当前状态和偏差。如果一台机器的目标是运行 BIOS 版本 3,但当前正在运行 BIOS 版本 1,自动化对 BIOS 版本 2 没有意见。软件包管理脚本确定是否可以将 BIOS 升级到版本 3,或者是否唯一的约束要求通过多个安装版本来引导子系统。

一些具体的例子说明了管理预期状态对安全性和可靠性都至关重要。谷歌使用特殊供应商提供的硬件与外部时间源(例如 GNSS/GPS 系统和原子钟)进行接口,以在生产中实现准确的时间保持,这是 Spanner 的前提条件。我们的硬件在设备上存储了两个不同的固件版本,分别存储在两个不同的芯片上。为了正确操作这些时间源,我们需要仔细配置这些固件版本。作为额外的复杂性,一些固件版本存在已知的错误,影响它们如何处理闰秒和其他边缘情况。如果我们不仔细维护这些设备上的固件和设置,我们就无法在生产中提供准确的时间。状态管理还需要覆盖备用、次要或其他非活动代码或配置,这些在恢复期间可能突然变为活动状态。在这种情况下,如果机器启动但时钟硬件无法提供足够准确的时间来运行我们的服务,那么我们的系统就没有充分恢复。

举个例子,现代 BIOS 具有对引导(和引导后)系统安全至关重要的许多参数。例如,您可能希望引导顺序优先选择 SATA 而不是 USB 引导设备,以防止数据中心中的恶意行为者轻松地从 USB 驱动器引导系统。更高级的部署跟踪和维护允许签署 BIOS 更新的密钥数据库,既用于管理轮换,又用于防范篡改。如果在恢复过程中主引导设备发生硬件故障,您不希望发现 BIOS 因为您忘记监视和指定次要引导设备的设置而卡在等待键盘输入的状态。

全球服务

服务中的最高抽象层和基础设施中最持久的部分,例如存储、命名和身份,可能是系统恢复最困难的领域。捕获状态的范式也适用于堆栈的这些高层。在构建或部署像 Spanner 或 Hadoop 这样的新全局单例系统时,确保支持多个实例,即使您永远不打算使用多个实例,甚至在第一次部署时也是如此。除了备份和恢复之外,您可能需要重建整个系统的新实例,以恢复该系统上的数据。

与手动设置服务不同,你可以通过编写命令式的自动化或使用声明性的高级配置语言(例如,容器编排配置工具如 Terraform)来设置服务。在这些情况下,你应该捕获服务创建的状态。这类似于测试驱动开发捕获代码的预期行为的方式,然后指导你的实现并帮助澄清公共 API。这两种做法都会导致更易维护的系统。

容器的流行意味着许多全球服务的构建模块的状态通常会被默认捕获。虽然自动捕获“大部分”服务状态是很好的,但不要被误导以为安全感是虚假的。从头开始恢复基础设施需要执行一系列复杂的依赖关系。这可能会导致发现意外的容量问题或循环依赖。如果你在物理基础设施上运行,问问自己:你是否有足够的备用机器、磁盘空间和网络容量来启动基础设施的第二份副本?如果你在像 GCP 或 AWS 这样的大型云平台上运行,你可能可以购买所需的物理资源,但你是否有足够的配额来在短时间内使用这些资源?你的系统是否自然地产生了任何相互依赖,以阻止从头开始进行干净的启动?在受控情况下进行灾难测试可能是有用的,以确保你对意外情况有所准备。

持久数据

没有人关心备份;他们只关心恢复。

到目前为止,我们已经专注于安全地恢复运行服务所需的基础设施。这对于一些无状态服务是足够的,但许多服务还存储持久数据,这增加了一系列特殊的挑战。关于备份和恢复持久数据的挑战有很多优秀的信息。在这里,我们讨论与安全性和可靠性相关的一些关键方面。

为了防御前面提到的错误类型,尤其是恶意错误,你的备份需要与主要存储具有相同级别的完整性保护。恶意内部人员可能会更改备份的内容,然后强制从损坏的备份中进行恢复。即使你有强大的加密签名来覆盖你的备份,如果你的恢复工具在恢复过程中不验证这些签名,或者如果你的内部访问控制没有正确限制可以手动进行签名的人员,那么这些签名就是无用的。

在可能的情况下,对持久数据的安全保护进行分隔是很重要的。如果你在数据的服务副本中检测到损坏,你应该将损坏隔离到实际上最小的数据子集中。如果你能够识别备份数据的这个子集并验证其完整性,那么恢复 0.01%的持久数据将会快得多,而不需要读取和验证其他 99.99%的数据。这种能力在持久数据的大小增长时变得尤为重要,尽管如果你遵循大型分布式系统设计的最佳实践,分隔通常会自然发生。计算分隔的块大小通常需要在存储和计算开销之间进行权衡,但你还应该考虑块大小对 MTTR 的影响。

你还应该考虑系统需要部分恢复的频率。还要考虑在恢复和数据迁移中涉及的系统之间共享多少常见基础设施:数据迁移通常与低优先级的部分恢复非常相似。如果每次数据迁移——无论是到另一台机器、机架、集群还是数据中心——都能够对恢复系统的关键部分进行练习并建立信心,那么当你最需要时,你就会知道所涉及的基础设施更有可能正常工作并且被理解。

数据恢复也可能引入其自己的安全和隐私问题。删除数据对于许多系统来说是必要的,而且通常也是法律要求的功能。确保你的数据恢复系统不会无意中允许你恢复被假定已销毁的数据。要意识到删除加密密钥和删除加密数据之间的区别。通过销毁相关的加密密钥使数据无法访问可能是有效的,但这种方法要求以符合细粒度数据删除要求的方式对用于各种类型数据的密钥进行分隔。

设计测试和持续验证

如第八章所讨论的,持续验证可以帮助维护一个强大的系统。为了做好恢复的准备,你的测试策略需要包括对恢复过程的测试。由于其性质,恢复过程在不熟悉的条件下执行不寻常的任务,如果没有测试,它们将遇到意想不到的挑战。例如,如果你正在自动创建一个干净的系统实例,一个良好的测试设计可能会揭示一个特定服务只有一个全局实例的假设,并帮助识别难以为该服务恢复创建第二个实例的情况。考虑测试可想象的恢复场景,以便在测试效率和生产实际性之间取得正确的平衡。

你还可以考虑测试恢复特别困难的小众情况。例如,在谷歌,我们在各种环境中实现了一个加密密钥管理协议:Arm 和 x86 CPU、UEFI 和裸机固件、Microsoft Visual C++(MSVC)、Clang、GCC 编译器等。我们知道对这个逻辑的所有故障模式进行练习将是具有挑战性的——即使在全面投入端到端测试的情况下,要真实地模拟硬件故障或中断通信也是困难的。因此,我们选择在一个可移植、编译器中立、位宽中立的方式中实现核心逻辑。我们对逻辑进行了大量的单元测试,并关注了用于抽象外部组件的接口设计。例如,为了伪造个别组件并练习它们的故障行为,我们创建了用于从闪存读取和写入字节、用于加密密钥存储以及用于性能监控原语的接口。这种测试环境条件的方法经受住了时间的考验,因为它明确地捕捉了我们想要恢复的故障类别。

最后,寻找通过持续验证来对恢复方法建立信心的方法。恢复涉及人类采取的行动,而人类是不可靠和不可预测的。仅仅依靠单元测试,甚至连续集成/交付/部署也无法捕捉到由人类技能或习惯导致的错误。例如,除了验证恢复工作流的有效性和互操作性之外,你还必须验证恢复说明是否可读且易于理解。

紧急访问

本章描述的恢复方法依赖于响应者与系统进行交互的能力,并且我们倡导使用与正常运营相同的主要服务来进行恢复过程。然而,当正常访问方法完全破坏时,您可能需要设计一个专用解决方案来部署。

组织通常具有紧急访问的独特需求和选择。关键是制定计划并建立维护和保护该访问的机制。此外,您需要了解您无法控制的系统层——这些层面的任何故障都是无法采取行动的,尽管它们会影响您。在这些情况下,您可能需要在其他人修复公司所依赖的服务时静观其变。为了最大程度地减少第三方故障对您的服务的影响,寻找您可以在基础设施的任何层面部署的潜在具有成本效益的冗余。当然,可能没有任何具有成本效益的替代方案,或者您可能已经达到了服务提供商所保证的最高 SLA。在这种情况下,请记住,您的可用性取决于您的依赖关系的总和。

谷歌的远程访问策略集中在部署自包含的关键服务到地理分布的机架上。为了支持恢复工作,我们旨在提供远程访问控制、高效的本地通信、替代网络和基础设施中的关键防御点。在全球性故障期间,由于每个机架至少对某些响应者保持可用,响应者可以开始修复他们可以访问的机架上的服务,然后径向扩展恢复进度。换句话说,当全球协作实际上是不可能的时候,任意较小的地区可以尝试自行解决问题。尽管响应者可能缺乏发现他们最需要的地方的上下文,并且存在地区分歧的风险,但这种方法可能会显著加速恢复。

访问控制

组织的访问控制服务不应成为所有远程访问的单点故障是至关重要的。理想情况下,您将能够实现避免相同依赖关系的替代组件,但是这些替代组件的可靠性可能需要不同的安全解决方案。虽然它们的访问策略必须同样强大,但出于技术或实际原因,它们可能不太方便和/或具有降级的功能集。

由于远程访问凭据可能无法使用,因此不能依赖于典型的凭据服务。因此,除非您可以用低依赖性实现替换这些组件,否则不能从访问基础设施的动态组件(如单一登录(SSO)或联合身份提供者)派生访问凭据。此外,选择这些凭据的生命周期构成了一个困难的风险管理权衡:对用户或设备强制执行短期访问凭据的良好做法,如果故障持续时间超过了它们,那么这将成为一个定时炸弹,因此您被迫延长凭据的生命周期以超过任何预期故障的长度,尽管存在额外的安全风险。此外,如果您是按固定时间表主动发放远程访问凭据,而不是在故障开始时按需激活它们,那么故障可能会在它们即将到期时开始。

如果网络访问采用用户或设备授权,对于任何依赖于动态组件的风险,与凭据服务面临的风险类似。随着越来越多的网络使用动态协议,您可能需要提供更加静态的替代方案。您可用的网络提供商列表可能会限制您的选择。如果可以使用具有静态网络访问控制的专用网络连接,请确保它们的定期更新不会破坏路由或授权。实现足够的监控以检测网络访问中断的位置,或者帮助区分网络访问问题和网络上层的问题可能尤为重要。

通信

紧急通信渠道是紧急响应的下一个关键因素。当值班人员的常规聊天服务无法使用或无法访问时,他们应该怎么办?如果聊天服务受到攻击者的威胁或被监听,该怎么办?

选择一种尽可能少依赖的通信技术(例如 Google Chat、Skype 或 Slack),并且对于响应团队的规模来说足够有用。如果该技术是外包的,那么即使系统外部的层面出现故障,响应者是否能够访问该系统?电话桥接虽然效率低下,但作为一种老式选择仍然存在,尽管它们越来越多地使用依赖于互联网的 IP 电话技术进行部署。如果公司希望部署自己的解决方案,互联网中继聊天(IRC)基础设施是可靠且自包含的,但缺乏一些安全方面的考虑。此外,您仍然需要确保在网络中断期间您的 IRC 服务器仍然可以访问。当您的通信渠道托管在自己的基础设施之外时,您可能还需要考虑提供商是否能够保证足够的身份验证和保密性来满足公司的需求。

响应者习惯

紧急访问技术的独特性通常导致与日常操作不同的做法。如果您不优先考虑这些技术的端到端可用性,响应者可能不知道如何在紧急情况下使用它们,您将失去这些技术的好处。整合低依赖性的替代方案可能会很困难,但这只是问题的一部分——一旦在很少使用的流程和工具中混入人类在压力下的混乱,结果复杂性可能会阻碍所有访问。换句话说,人类而不是技术可能会使紧急工具失效。

您能够最大程度地减少正常和紧急流程之间的区别,响应者就能够更多地依赖习惯。这将释放更多的认知能力,让他们能够更多地专注于不同之处。因此,组织对中断的弹性可能会提高。例如,在谷歌,我们集中在 Chrome、其扩展和与之相关的任何控件和工具作为远程访问的单一平台。将紧急模式引入 Chrome 扩展程序使我们能够在前期尽可能少地增加认知负荷,同时保留将其整合到更多扩展程序中的选项。

为了确保您的响应者定期进行紧急访问实践,引入将紧急访问整合到值班人员的日常习惯中的政策,并持续验证相关系统的可用性。例如,定义并强制执行所需练习之间的最短时间。团队负责人可以在团队成员需要完成必需的凭证刷新或培训任务时发送电子邮件通知,或者可以选择放弃练习,如果他们确定该个人定期参与等效活动。这增加了信心,当发生事故时,团队的其他成员确实拥有相关的凭证,并且最近完成了必要的培训。否则,让您的员工强制练习打破玻璃操作和任何相关流程。

最后,请确保相关的文件,如政策、标准和操作指南,是可用的。人们往往会忘记很少使用的细节,这样的文件也可以在压力和怀疑下减轻压力。架构概述和图表对于事件响应者也是有帮助的,并且可以让不熟悉该主题的人快速了解,而不太依赖于专家。

意想不到的好处

本章描述的设计原则,建立在弹性设计原则的基础上,提高了系统的恢复能力。可靠性和安全性之外的意想不到的好处可能会帮助您说服您的组织采用这些实践。考虑一个专门用于固件更新认证、回滚、锁定和证明机制的服务器。有了这些基本功能,您可以自信地从检测到的妥协中恢复机器。现在考虑在“裸金属”云托管服务中使用这台机器,供应商希望使用自动化清理和转售机器。已经考虑了恢复的机器已经有了一个安全和自动化的解决方案。

这些好处在供应链安全方面进一步增加。当机器由许多不同的组件组装而成时,您需要更少地关注那些完整性可以通过自动方式恢复的组件的供应链安全性。您的首次操作只需要运行恢复程序。作为额外的奖励,重新利用恢复程序意味着您定期锻炼您的关键恢复能力,因此当发生事故时,您的员工已经准备好采取行动。

为恢复设计系统被认为是一个高级话题,其商业价值只有在系统脱离预期状态时才能得到证明。但是,鉴于我们建议操作系统使用错误预算来最大化成本效率,我们预计这样的系统会经常处于错误状态。我们希望您的团队将慢慢开始在开发过程中尽早投资于速率限制或回滚机制。有关如何影响您的组织的更多见解,请参见第二十一章。

结论

本章探讨了为恢复设计系统的各个方面。我们解释了系统应该在部署更改的速率方面具有灵活性的原因:这种灵活性使您能够在可能的情况下缓慢推出更改,并避免协调失败,但也使您能够在必须接受更多风险以满足安全目标时快速而自信地推出更改。回滚更改的能力对于构建可靠的系统至关重要,但有时您可能需要防止回滚到不安全或足够旧的版本。了解、监视和尽可能重现系统的状态——通过软件版本、内存、挂钟时间等——是可靠地恢复系统到以前的工作状态的关键,并确保其当前状态符合您的安全要求。作为最后的手段,紧急访问允许响应者保持连接,评估系统并缓解情况。深思熟虑地管理政策与程序、中央真相来源与本地功能、预期状态与系统实际状态之间的关系为可恢复的系统铺平了道路,同时促进了韧性和日常运营的稳健性。

¹ CAP 定理描述了扩展分布式系统涉及的一些权衡以及其后果。

² 意外的位翻转可能是由于硬件故障,来自其他系统的噪音,甚至是宇宙射线引起的。第十五章更详细地讨论了硬件故障。

³ 一个名为人类可靠性分析(HRA)的研究领域记录了在给定任务中人为错误的可能性。有关更多信息,请参见美国核监管委员会的概率风险评估

⁴ “计算机科学中的所有问题都可以通过另一级间接性来解决。” ——David Wheeler

⁵ “… 除了太多级别的间接性。” ——未知

⁶ 有关在受到威胁时如何应对以及确定您的恢复系统是否受到威胁的元问题的详细讨论,请参见第十八章。第七章还有额外的设计模式和示例,描述了如何选择适当的速率。

⁷ 请参见CVE-2014-6271CVE-2014-6277CVE-2014-6278CVE-2014-7169

⁸ 请参见CVE-2014-0160

⁹ 这个原则的推论是:如果您在紧急情况下有一个有效的方法(通常是因为它的依赖性低),那就把它作为您的标准方法。

¹⁰ 例如,完整程序或固件映像的加密哈希(如 SHA256)。

¹¹ “已知不良”版本可能是由于成功但不可逆转的更改,例如主要模式更改,或者由于错误和漏洞。

¹² 另一个广泛部署的精心管理的安全版本号的例子存在于英特尔的微码 SVN 中,例如用于缓解安全问题 CVE-2018-3615

¹³ 一个例子是Xilinx Zynq Ultrascale+设备中的硬件信任根支持。

¹⁴ 立即撤销凭证可能并不总是最佳选择。有关应对妥协的讨论,请参见第十七章。

  1. KRL 文件是由证书颁发机构(CA)吊销的密钥的紧凑二进制表示。有关详细信息,请参阅ssh-keygen(1) manpage

  2. 虽然本章重点是恢复,但也至关重要的是考虑这样操作的弹性。在像 Linux 这样的 POSIX 系统上替换关键配置文件时,需要谨慎确保在崩溃或其他故障发生时具有稳健的行为。考虑使用带有RENAME_EXCHANGE标志的renameat2系统调用。

  3. 连续的恶意 KRL 推送可能会扩大负面影响,但速度和广度的限制仍然会实质性地扩大响应窗口。

  4. post_installpre_rm 的概念是从Debianpreinstpostinstprermpostrm中借鉴而来的。谷歌的软件包管理系统采取了更加强硬的方法:它不允许软件包的配置和安装分开,也不允许安装过程中出现一半的成功。任何软件包的更改都保证会成功,否则机器将完全回滚到先前的状态。如果回滚失败,机器将通过我们的修复流程进行重新安装和潜在的硬件更换。这种方法使我们能够消除软件包状态的许多复杂性。

  5. 有关应对妥协的进一步讨论,请参阅第十七章。

  6. Spanner是谷歌的全球分布式数据库,支持外部一致的分布式事务。它需要数据中心之间非常紧密的时间同步。

  7. 请参阅 Krishnan, Kripa. 2012. “Weathering the Unexpected.” ACM Queue 10(9). https://oreil.ly/vJ66c

  8. 许多版本的这一优秀建议都被归因于许多备受尊敬的工程师。我们在印刷品中找到的最古老版本,碰巧是在作者书架上的一本书中,是来自 W. Curtis Preston 的Unix Backup & Recovery(O’Reilly)。他将这句话归因于 Ron Rodriguez,即“没有人在乎你能否备份——只在乎你能否恢复”。

  9. 有关入门知识,请参阅 Kristina Bennett 的 SREcon18 演讲“Tradeoffs in Resiliency: Managing the Burden of Data Recoverability”

  10. 例如,查看谷歌的数据保留政策

  11. 请参阅 Treynor, Ben 等人。2017. “The Calculus of Service Availability.” ACM Queue 15(2). https://oreil.ly/It4-h

  12. 例如,软件定义网络(SDN)。

  13. Breakglass 工具是可以绕过策略以允许工程师快速解决故障的机制。请参阅“Breakglass”

  14. 有关错误预算的更多信息,请参阅SRE 书中的第三章

第十章:减轻拒绝服务攻击

原文:10. Mitigating Denial-of-Service Attacks

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Damian Menscher

与 Vitaliy Shipitsyn 和 Betsy Beyer 一起

安全从业者通常从攻击防御的角度考虑他们保护的系统。但在典型的拒绝服务攻击中,经济学提供了更有用的术语:对手试图导致对特定服务的需求超过该服务容量的供应。¹ 最终结果是服务容量不足以为其合法用户提供服务。组织随后必须决定是承担更大的费用来吸收攻击,还是遭受停机时间(以及相应的财务损失),直到攻击停止。

虽然一些行业比其他行业更经常成为 DoS 攻击的目标,但任何服务都可能以这种方式受到攻击。DoS 勒索是一种金融攻击,对手威胁要破坏服务,除非付款,相对不加选择地打击。²

攻击和防御策略

攻击者和防御者资源有限,必须有效地利用资源来实现其目标。在制定防御策略时,了解对手的策略是有帮助的,这样您就可以在他们之前找到防御的弱点。有了这种理解,您可以构建已知攻击的防御,并设计具有灵活性的系统,以快速减轻新型攻击。

攻击者的策略

攻击者必须专注于有效地利用有限的资源来超过其目标的容量。聪明的对手可能能够破坏更强大对手的服务。

典型服务有几个依赖关系。考虑一个典型用户请求的流程:

  1. DNS 查询提供应该接收用户流量的服务器的 IP 地址。

  2. 网络将请求传递到服务前端。

  3. 服务前端解释用户请求。

  4. 服务后端为自定义响应提供数据库功能。

成功破坏这些步骤中的任何一个的攻击将破坏服务。大多数新手攻击者将尝试发送一大堆应用程序请求或网络流量。更复杂的攻击者可能会生成更昂贵的请求来回答 - 例如,滥用许多网站上存在的搜索功能。

因为单台机器很少足以破坏大型服务(通常由多台机器支持),决心的对手将开发工具,利用许多机器的力量进行所谓的分布式拒绝服务(DDoS)攻击。要执行 DDoS 攻击,攻击者可以利用易受攻击的机器并将它们组合成僵尸网络,或发动放大攻击

防御者的策略

资源充足的防御者可以通过过度配置整个堆栈来吸收攻击,但代价很大。充满耗电量的机器的数据中心很昂贵,而为吸收最大攻击而提供始终开启的容量是不可行的。虽然自动扩展可能是具有充足容量的云平台上构建的服务的选项,但防御者通常需要利用其他成本效益的方法来保护其服务。

在确定最佳的 DoS 防御策略时,您需要考虑工程时间 - 您应该优先考虑具有最大影响的策略。虽然专注于解决昨天的故障很诱人,但最近的偏见可能导致迅速变化的优先事项。相反,我们建议使用威胁模型方法,集中精力解决依赖链中的最薄弱环节。您可以根据攻击者需要控制的机器数量来比较威胁,以造成用户可见的破坏。

注意

我们使用术语DDoS来指代仅因其分布式性质而有效的 DoS 攻击,以及使用大型僵尸网络或放大攻击。我们使用术语DoS来指代可能源自单个主机的攻击。在设计防御时,这种区别是相关的,因为您通常可以在应用层部署 DoS 防御,而 DDoS 防御通常利用基础设施内的过滤器。

为防御而设计

理想的攻击会将所有力量集中在单一受限资源上,例如网络带宽、应用服务器 CPU 或内存,或者像数据库这样的后端服务。您的目标应该是以最有效的方式保护这些资源。

随着攻击流量深入系统,它变得更加集中和更加昂贵,因此,分层防御,即每一层保护其后面的层,是一种必不可少的设计特征。在这里,我们将研究导致两个主要层中的可防御系统的设计选择:共享基础设施和个体服务。

可防御的架构

大多数服务共享一些常见的基础设施,例如对等容量、网络负载均衡器和应用负载均衡器。

共享基础设施是提供共享防御的自然场所。边缘路由器可以限制高带宽攻击,保护骨干网络。网络负载均衡器可以限制数据包洪泛攻击,以保护应用负载均衡器。应用负载均衡器可以在流量到达服务前端之前限制特定于应用程序的攻击。

分层防御往往是具有成本效益的,因为您只需要为内部层的 DoS 攻击风格进行容量规划,这些攻击风格可以突破外部层的防御。尽早消除攻击流量既节省了带宽,也节省了处理能力。例如,通过在网络边缘部署 ACL,您可以在流量有机会消耗内部网络带宽之前丢弃可疑流量。在网络边缘附近部署缓存代理也可以提供显着的成本节约,同时还可以减少合法用户的延迟。

注意

有状态的防火墙规则通常不适合作为接收入站连接的生产系统的第一道防线。⁵对手可以进行状态耗尽攻击,其中大量未使用的连接填满了启用了连接跟踪的防火墙的内存。相反,使用路由器 ACL 来限制流量到必要的端口,而不会引入有状态的系统到数据路径中。

在共享基础设施中实现防御也提供了有价值的规模经济。虽然为任何个体服务提供重要的防御能力可能不具有成本效益,但共享防御可以让您一次性覆盖广泛的服务。例如,图 10-1 显示了一次针对一个站点的攻击产生的流量量远高于该站点的正常流量,但与Project Shield保护的所有站点接收到的流量相比,仍然是可以管理的。商业 DoS 缓解服务使用类似的捆绑方法来提供成本效益的解决方案。

通过 Project Shield 保护的站点遭受的 DDoS 攻击,从(上)个体站点的角度和(下)Project Shield 负载均衡器的角度看

图 10-1. 通过 Project Shield 保护的站点遭受的 DDoS 攻击,从(上)个体站点的角度和(下)Project Shield 负载均衡器的角度看

特别大规模的 DDoS 攻击可能会压倒数据中心的容量,就像放大镜可以利用太阳的能量点燃火一样。任何防御策略都必须确保分布式攻击所达到的能力不能集中到任何单一组件上。您可以使用网络和应用负载均衡器不断监视传入流量,并将流量传送到最近具有可用容量的数据中心,防止这种类型的过载。

您可以使用anycast来保护共享基础设施,而不依赖于一种被动系统,这是一种技术,其中一个 IP 地址从多个位置宣布。使用这种技术,每个位置都会吸引附近用户的流量。因此,分布式攻击将分散到世界各地的位置,并且因此无法将其能力集中到任何单一数据中心。

可防御服务

网站或应用程序设计可能会对服务的防御姿态产生重大影响。尽管确保服务在过载条件下能够优雅地降级提供了最佳的防御,但可以进行几项简单的更改来提高对攻击的弹性,并在正常运行中实现显着的成本节约:

利用缓存代理

使用Cache-Control和相关标头可以允许代理重复请求内容,而无需每个请求都命中应用程序后端。这适用于大多数静态图像,甚至适用于主页。

避免不必要的应用程序请求

每个请求都会消耗服务器资源,因此最好尽量减少所需的请求次数。如果一个网页包含多个小图标,最好将它们全部放在一个(较大的)图像中,这种技术称为图像合并。作为一个附带好处,减少真实用户对服务的请求次数将减少在识别恶意机器人时的误报。

最小化出口带宽

虽然传统攻击试图饱和入口带宽,但攻击也可能通过请求大资源来饱和您的带宽。将图像调整为所需大小将节省出口带宽,并减少用户的页面加载时间。限制速率或降低不可避免的大型响应也是另一种选择。

减轻攻击

虽然可防御的架构提供了抵御许多 DoS 攻击的能力,但您可能还需要积极的防御来减轻大规模或复杂的攻击。

监控和警报

停机解决时间由两个因素主导:检测到故障的平均时间(MTTD)和修复故障的平均时间(MTTR)。DoS 攻击可能会导致服务器 CPU 利用率飙升,或者应用程序在排队请求时耗尽内存。为了快速诊断根本原因,您需要监视请求速率以及 CPU 和内存使用情况。

对异常高的请求速率发出警报可以清楚地指示事件响应团队遭受了攻击。但是,请确保您的警报是可操作的。如果攻击没有对用户造成实质性伤害,通常最好是吸收它。我们建议只在需求超过服务容量并且自动 DoS 防御已经启动时发出警报。

只有在可能需要人工干预时才发出警报的原则同样适用于网络层攻击。许多 synflood 攻击可以被吸收,但如果触发了 syncookies,则可能需要发出警报。类似地,高带宽攻击只有在链接饱和时才值得关注。

优雅降级

如果吸收攻击是不可行的,您应尽量减少对用户的影响。

在大规模攻击期间,您可以使用网络 ACL 来限制可疑流量,提供有效的开关以立即限制攻击流量。重要的是不要一直阻止可疑流量,这样您可以保持对系统的可见性,并最小化影响与攻击特征匹配的合法流量的风险。因为聪明的对手可能模拟合法流量,所以限制可能不足够。此外,您可以使用服务质量(QoS)控件来优先处理关键流量。对于像批量复制这样不太重要的流量使用较低的 QoS 可以在需要时释放带宽到更高的 QoS 队列。

在超载的情况下,应用程序也可以退回到降级模式。例如,Google 处理超载的方式有:

  • 博客服务以只读模式提供,禁用评论。

  • Web 搜索继续提供带有减少功能集的服务。

  • DNS 服务器会尽可能回答尽可能多的请求,但设计上不会在任何负载下崩溃。

有关处理超载的更多想法,请参见第八章。

DoS 缓解系统

自动防御,例如限制前几个 IP 地址或提供 JavaScript 或 CAPTCHA 挑战,可以快速而一致地缓解攻击。这使得事件响应团队有时间了解问题,并确定是否需要自定义缓解措施。

自动 DoS 缓解系统可以分为两个组件:

检测

系统必须对传入流量有尽可能详细的可见性。这可能需要在所有端点进行统计抽样,并将其汇总到一个中央控制系统。控制系统识别可能表明攻击的异常情况,同时与了解服务容量的负载均衡器一起确定是否需要响应。

响应

系统必须具有实现防御机制的能力,例如,提供一组要阻止的 IP 地址。

在任何大规模系统中,误报(和误报)是不可避免的。当通过 IP 地址阻止时,尤其如此,因为多个设备共享单个网络地址是很常见的(例如,在使用网络地址转换时)。为了最小化对相同 IP 地址后面的其他用户造成的附带损害,您可以利用 CAPTCHA 来允许真实用户绕过应用程序级别的阻止。

您还必须考虑 DoS 缓解系统的故障模式 - 问题可能由攻击,配置更改,不相关的基础设施故障或其他原因触发。

DoS 缓解系统本身必须对攻击具有弹性。因此,它应避免依赖可能受到 DoS 攻击影响的生产基础设施。这些建议不仅适用于服务本身,还适用于事件响应团队的工具和通信程序。例如,由于 Gmail 或 Google Docs 可能会受到 DoS 攻击的影响,Google 拥有备用通信方法和 playbook 存储。

攻击通常会导致立即中断。虽然优雅降级可以减少过载服务的影响,但最好的情况是 DoS 缓解系统可以在几秒钟而不是几分钟内做出响应。这种特性与缓慢部署变更的最佳实践产生自然的紧张关系,以防止中断。作为一种权衡,我们可以在我们的生产基础设施的子集上对所有变更(包括自动响应)进行金丝雀测试,然后再将其部署到所有地方。金丝雀测试可能非常简短 - 在某些情况下可能只有 1 秒钟!

如果中央控制器失败,我们不希望出现关闭失败(因为这将阻止所有流量,导致故障)或者开放失败(因为这将让正在进行的攻击通过)。相反,我们失败静态,这意味着策略不会改变。这允许控制系统在攻击期间失败(这实际上在 Google 发生过!)而不会导致故障。因为我们失败静态,DoS 引擎不必像前端基础设施那样高度可用,从而降低成本。

战略性响应

在应对故障时,很容易纯粹是被动的,并尝试过滤当前的攻击流量。虽然快速,但这种方法可能并不是最佳的。攻击者可能在第一次尝试失败后放弃,但如果他们不放弃呢?对手有无限的机会来探测防御并构建绕过。战略性的响应避免通知对手对您的系统的分析。例如,我们曾经收到一个攻击,它通过其User-Agent: I AM BOTNET.轻松识别。如果我们简单地丢弃所有带有该字符串的流量,我们将教会我们的对手使用更合理的User-Agent,比如Chrome。相反,我们列举了发送该流量的 IP,并拦截了他们一段时间内的所有请求,使用验证码。这种方法使对手更难使用A/B 测试来了解我们如何隔离攻击流量。即使他们修改为发送不同的User-Agent,它也会主动阻止他们的僵尸网络。

了解对手的能力和目标可以指导您的防御。小型放大攻击表明您的对手可能仅限于可以发送伪造数据包的单个服务器,而重复获取同一页面的 HTTP DDoS 攻击表明他们可能可以访问僵尸网络。但有时“攻击”是无意的-您的对手可能只是试图以不可持续的速度抓取您的网站。在这种情况下,您最好的解决方案可能是确保网站不容易被抓取。

最后,请记住您并不孤单-其他人也面临类似的威胁。考虑与其他组织合作,以改善您的防御和响应能力:DoS 缓解提供商可以清除某些类型的流量,网络提供商可以执行上游过滤,网络运营商社区可以识别和过滤攻击源。

处理自我造成的攻击

在主要故障的肾上腺素飙升期间,自然反应是专注于打败对手的目标。但如果没有对手可以打败呢?突然增加流量的一些其他常见原因。

用户行为

大多数时候,用户做出独立的决定,他们的行为平均成为平滑的需求曲线。然而,外部事件可以同步他们的行为。例如,如果夜间地震唤醒了人口中的每个人,他们可能会突然转向他们的设备搜索安全信息,发布到社交媒体,或者与朋友联系。这些同时的行动可能导致服务接收到突然增加的使用,就像图 10-2 中显示的流量峰值一样。

2019 年 10 月 14 日发生 4.5 级地震时,测量每秒 HTTP 请求达到谷歌基础设施的网络流量,为旧金山湾区的用户提供服务

图 10-2:当 2019 年 10 月 14 日发生 4.5 级地震时,测量每秒 HTTP 请求达到谷歌基础设施的网络流量,为旧金山湾区的用户提供服务

我们通过设计更改来解决这种“攻击”:我们推出了一个功能,当您输入时会建议单词完成。

客户端重试行为

一些“攻击”是无意的,只是由于软件的不当行为。如果客户端期望从服务器获取资源,而服务器返回错误,会发生什么?开发人员可能认为重试是合适的,如果服务器仍在提供错误,就会导致循环。如果许多客户端陷入这种循环,由此产生的需求会使从停机中恢复变得困难。⁹

客户端软件应该被精心设计以避免紧密的重试循环。如果服务器失败,客户端可能会重试,但应该实现指数退避——例如,每次尝试失败时等待时间加倍。这种方法限制了对服务器的请求次数,但单独使用是不够的——停机会同步所有客户端,导致高流量的重复突发。为了避免同步重试,每个客户端应该等待一个随机的持续时间,称为抖动。在谷歌,我们在大多数客户端软件中实现了带有抖动的指数退避。

如果你不能控制客户端会怎么办?这是运营权威 DNS 服务器的人们的一个常见关注点。如果他们遭受停机,合法递归 DNS 服务器的重试率可能会导致流量显著增加——通常是正常使用量的 30 倍左右。这种需求会使从停机中恢复变得困难,并经常挫败找到其根本原因的尝试:运营商可能认为 DDoS 攻击是原因,而不是症状。在这种情况下,最好的选择是简单地回答尽可能多的请求,同时通过上游请求限流保持服务器的健康。每个成功的响应都将允许客户端摆脱其重试循环,问题很快就会得到解决。

结论

每个在线服务都应该为 DoS 攻击做好准备,即使他们认为自己不太可能成为目标。每个组织都有它可以吸收的流量上限,防御者的任务是以最有效的方式减轻超出部署能力的攻击。

重要的是要记住你的 DoS 防御的经济约束。简单地吸收攻击很少是最廉价的方法。相反,利用成本效益的缓解技术,从设计阶段开始。在遭受攻击时,考虑所有选项,包括阻止有问题的托管提供商(可能包括少量真实用户)或遭受短期停机并向用户解释情况。还要记住,“攻击”可能是无意的。

在服务堆栈的每一层实现防御措施需要与几个团队合作。对于一些团队来说,DoS 防御可能不是首要任务。为了获得他们的支持,要关注 DoS 缓解系统可以提供的成本节约和组织简化。容量规划可以专注于真实用户需求,而不需要在堆栈的每一层吸收最大规模的攻击。使用 Web 应用程序防火墙(WAF)过滤已知的恶意请求,允许安全团队专注于新型威胁。如果发现应用程序级别的漏洞,同样的系统可以阻止利用尝试,让开发团队有时间准备补丁。

通过仔细的准备,你可以按照自己的条件确定服务的功能和故障模式,而不是对手的条件。

¹ 为了讨论的方便,我们将专注于常见情况,即攻击者没有物理接触和挖掘机,也不知道崩溃漏洞。

² 一些勒索者会发动小规模的“示范攻击”来促使目标付款。几乎所有情况下,这些攻击者没有能力发动更大规模的攻击,如果他们的要求被忽视,他们也不会再做进一步的威胁。

(3)参见 Rossow, Christian. 2014. “Amplification Hell: Revisiting Network Protocols for DDoS Abuse.” Proceedings of the 21st Annual Network and Distributed System Security Symposium. doi:10.14722/ndss.2014.23233.

(4)基于 TCP 的协议也可以被利用进行这种类型的攻击。有关讨论,请参见 Kührer, Mark 等人。2014. “Hell of a Handshake: Abusing TCP for Reflective Amplification DDoS Attacks.” Proceedings of the 8th USENIX Workshop on Offensive Technologies. https://oreil.ly/0JCPP.

(5)有状态防火墙,执行连接跟踪,最适合用于保护发起出站流量的服务器。

(6)如果服务处于全局超载状态,它们也可能会丢弃流量。

(7)我们的一个服务设计为所有 UI 元素使用了圆角。在最初的形式中,浏览器为每个角都获取了图像。通过更改站点以下载一个圆形,然后在客户端分割图像,我们每天节省了 1000 万个请求。

(8)在 synflood 攻击中,TCP 连接请求以高速发送,但不完成握手。如果接收服务器没有实现防御机制,它将耗尽内存来跟踪所有入站连接。常见的防御是使用 syncookies,提供一种无状态机制来验证新连接。

(9)请参阅SRE 书中的第 22 章