构建安全可靠的系统-全-

构建安全可靠的系统(全)

原文:Building Secure and Reliable Systems

译者:飞龙

协议:CC BY-NC-SA 4.0

罗亚尔·汉森的前言

原文:Foreword by Royal Hansen

译者:飞龙

协议:CC BY-NC-SA 4.0

多年来,我一直希望有人能写一本像这样的书。自从它们出版以来,我经常钦佩并推荐谷歌的可靠性工程(SRE)书籍,所以当我来到谷歌时,我很高兴地发现一本关注安全和可靠性的书已经在进行中,我也很乐意以一种小小的方式参与其中。自从我开始在技术行业工作以来,跨越各种规模的组织,我看到人们在如何组织安全方面一直在挣扎:它应该是集中的还是联邦的?独立的还是嵌入式的?操作性的还是咨询性的?技术性的还是管理性的?等等……

当 SRE 模型和类似 SRE 的 DevOps 版本变得流行时,我注意到 SRE 所处理的问题领域表现出与安全问题类似的动态。一些组织已经将这两个学科结合起来,形成了一种称为“DevSecOps”的方法。

SRE 和安全都对经典软件工程团队有很强的依赖性。然而,它们与经典软件工程团队在根本上有所不同:

  • 可靠性工程师(SRE)和安全工程师往往是破坏和修复,也是构建者。

  • 他们的工作涵盖了运营,而不仅仅是开发。

  • SRE 和安全工程师是专家,而不是经典的软件工程师。

  • 它们经常被视为障碍,而不是促进因素。

  • 它们经常是分立的,而不是在产品团队中整合。

SRE 创建了一个特定技能集的角色和责任,我们可以将其视为安全工程师的角色。SRE 还创建了一个连接团队的实现模型,这似乎是安全社区需要采取的下一步。多年来,我和我的同事们一直主张安全应该成为软件的一流和嵌入式质量。我相信采用受 SRE 启发的方法是朝着这个方向迈出的一个合乎逻辑的步骤。

自从来到谷歌以来,我更多地了解了 SRE 模型是如何在这里建立的,SRE 如何实现 DevOps 理念,以及 SRE 和 DevOps 是如何发展的。与此同时,我一直在将我在金融服务行业的 IT 安全经验转化为谷歌的技术和项目安全能力。这两个领域并不无关,但每个领域都有其值得理解的历史。与此同时,企业正处于一个关键时刻,云计算、各种形式的机器学习以及复杂的网络安全格局共同决定着一个日益数字化的世界将走向何方,以及它将以多快的速度到达,以及涉及哪些风险。

随着我对安全和 SRE 交叉领域的理解加深,我越来越确信更彻底地将安全实践整合到软件和数据服务的整个生命周期中是非常重要的。现代混合云的性质——其中大部分基于提供互连数据和微服务的开源软件框架——使得紧密集成的安全和弹性能力变得更加重要。

在过去 20 年里,大型企业的安全运营和组织方法差异巨大。最突出的实例包括完全集中的首席信息安全官和涵盖防火墙、目录服务、代理等核心基础设施运营团队,这些团队已经发展到数百或数千名员工。在另一端,联邦业务信息安全团队拥有支持或管理一系列功能或业务运营所需的业务线或技术专业知识。在中间某处,委员会、指标和监管要求可能管理安全政策,嵌入式安全冠军可能扮演关系管理角色或跟踪指定组织单位的问题。最近,我看到团队在 SRE 模型上进行改进,将嵌入式角色演变成类似站点安全工程师的角色,或者成为专门安全团队的敏捷 Scrum 角色。

出于充分的理由,企业安全团队主要关注保密性。然而,组织通常认识到数据完整性和可用性同样重要,并通过不同的团队和不同的控制措施来解决这些问题。SRE 功能是可靠性的最佳实践方法。然而,它也在实时检测和响应技术问题方面发挥作用,包括对特权访问或敏感数据的安全相关攻击。最终,尽管工程团队在组织上根据专业技能集分开,但它们有一个共同的目标:确保系统或应用的质量和安全。

在一个每年对技术依赖性越来越强的世界中,一本关于安全和可靠性方法的书籍,从谷歌和整个行业的经验中汲取,可以对软件开发、系统管理和数据保护的演变做出重要贡献。随着威胁形势的演变,动态和综合的防御方法现在已经成为基本必需品。在我之前的角色中,我寻求对这些问题更正式的探讨;我希望安全组织内外的各种团队在方法和工具演变时会发现这次讨论有用。这个项目加强了我对它所涵盖的主题值得在行业中讨论和推广的信念,特别是在越来越多的组织采用 DevOps、DevSecOps、SRE 和混合云架构以及相关的运营模式的情况下。至少,这本书是在日益数字化的世界中系统和数据安全演变和增强的又一步。

皇家汉森,安全工程副总裁

迈克尔·威尔德帕纳的序言

原文:Foreword by Michael Wildpaner

译者:飞龙

协议:CC BY-NC-SA 4.0

在本质上,网站可靠性工程和安全工程都关注保持系统的可用性。诸如破损的发布、容量短缺和错误配置等问题可能使系统无法使用(至少是暂时的)。破坏用户信任的安全或隐私事件也会削弱系统的实用性。因此,系统安全是 SRE 们首要关注的问题。

在设计层面,安全已经成为分布式系统的高度动态属性。我们已经从早期基于 Unix 的电话交换机上的无密码帐户(没有人有调制解调器拨入它们,或者人们这样认为),静态用户名/密码组合和静态防火墙规则走了很长一段路。如今,我们使用的是有时限的访问令牌和每秒数百万次的高维风险评估。数据在传输和静态时的细粒度加密,再加上频繁的密钥轮换,使密钥管理成为处理敏感信息的任何网络、处理或存储系统的额外依赖。构建和操作这些基础设施安全软件系统需要原始系统设计者、安全工程师和 SRE 之间的密切合作。

分布式系统的安全对我来说有着额外的、更加个人化的意义。从我大学时代直到加入谷歌之前,我在攻击性安全方面有着一段副业生涯,专注于网络渗透测试。我对分布式软件系统的脆弱性以及系统设计者和运营者与攻击者之间的不对称性有了很多了解:前者需要防范所有可能的攻击,而攻击者只需要找到一个可利用的弱点。

理想情况下,SRE 参与重要的设计讨论和实际的系统变更。作为 Gmail 的早期 SRE 技术负责人之一,我开始看到 SRE 作为防御的最佳线路之一(在系统变更的情况下,确实是最后一道防线),以防止糟糕的设计或实现影响我们系统的安全。

谷歌关于 SRE 的两本书——《网站可靠性工程》和《网站可靠性工作手册》——阐述了 SRE 的原则和最佳实践,但并未详细讨论可靠性和安全的交集。这本书填补了这一空白,并且还有空间深入探讨安全相关的主题。

多年来,在谷歌,我们一直在挑选工程师并与他们进行“谈话”——关于如何负责地处理我们系统安全的对话。但是,如何设计和操作安全的分布式系统的更正式的处理已经迫在眉睫。通过这种方式,我们可以更好地扩展先前非正式的合作。

安全是发现新攻击类型并使我们的系统免受当今网络环境中各种威胁的主要前沿,而 SRE 在预防和纠正此类问题方面发挥着重要作用。在软件开发生命周期中,推动可靠性和安全作为不可或缺的部分是没有替代方案的。

迈克尔·威尔德帕纳,高级总监,网站可靠性工程

前言

原文:Preface

译者:飞龙

协议:CC BY-NC-SA 4.0

如果一个系统在根本上不安全,它能被认为是可靠的吗?或者如果它不可靠,它能被认为是安全的吗?

成功设计、实现和维护系统需要对整个系统生命周期的承诺。只有当安全性和可靠性是系统架构的核心要素时,这种承诺才是可能的。然而,它们经常被忽视,只有在发生事故后才被考虑,导致昂贵且有时困难的改进。

安全设计在许多产品连接到互联网并且云技术变得更加普遍的世界中变得越来越重要。我们越来越依赖这些系统,它们就需要更加可靠;我们对它们的安全性的信任越大,它们就需要更加安全。

我们为什么写这本书

我们希望写一本书,重点是将安全性和可靠性直接融入软件和系统的生命周期,旨在突出保护系统并保持其可靠的技术和实践,并说明这些实践如何相互作用。本书的目的是提供来自专门从事安全性和可靠性的实践者的系统设计、实现和维护方面的见解。

我们要明确指出,本书推荐的一些策略需要基础设施支持,而这可能在您目前的工作地点并不存在。在可能的情况下,我们建议采用可以适应任何规模组织的方法。然而,我们认为重要的是开始讨论如何我们都可以发展和改进现有的安全性和可靠性实践,因为我们不断增长和技术娴熟的专业人士社区的所有成员都可以相互学习很多。我们希望其他组织也会急于与社区分享他们的成功和战斗经验。随着对安全性和可靠性的理念不断发展,行业可以从多样化的实现示例中受益。安全性和可靠性工程仍然是快速发展的领域。我们不断发现导致我们修改(或在某些情况下替换)先前坚定的信念的条件和案例。

这本书适合谁

因为安全性和可靠性是每个人的责任,我们的目标受众是广泛的:设计、实现和维护系统的人员。我们挑战传统专业角色之间的分界线,包括开发人员、架构师、站点可靠性工程师(SRE)、系统管理员和安全工程师。虽然我们将深入探讨一些可能更适合有经验的工程师的主题,但我们邀请您——读者——在阅读各章节时尝试不同的角色,想象自己扮演您(目前)没有的角色,并思考如何改进您的系统。

我们认为每个人都应该从开发过程的最开始就考虑可靠性和安全性的基本原则,并在系统生命周期的早期阶段就将这些原则整合进去。这是本书整体的一个关键概念。在行业中有许多活跃的讨论,关于安全工程师变得更像软件开发人员,SRE 和软件开发人员变得更像安全工程师。我们邀请您加入这个讨论。

当我们在书中说“您”时,我们指的是读者,而不是特定的工作或经验水平。本书挑战了工程角色的传统期望,并旨在赋予您对整个产品生命周期的安全性和可靠性负责的能力。您不必担心在特定情况下使用本书中描述的所有实践。相反,我们鼓励您在职业生涯的不同阶段或组织的发展过程中返回本书,考虑一开始似乎不重要的想法可能会变得有意义。

关于文化的说明

在本书中建立和采用我们推荐的广泛最佳实践需要一种支持这种变革的文化。我们认为,您需要同时关注组织的文化和您所做的技术选择,以便专注于安全性和可靠性,以便您所做的任何调整都是持久和有弹性的。在我们看来,不重视安全性和可靠性重要性的组织需要改变,而改变组织的文化本身通常需要前期投资。

我们在整本书中融入了技术最佳实践,并用数据支持它们,但是不可能包含有数据支持的文化最佳实践。虽然本书提出了我们认为其他人可以采用或概括的方法,但每个组织都有独特的文化。我们讨论了谷歌如何在其文化中努力工作,但这可能并不直接适用于您的组织。相反,我们鼓励您从本书中包含的高层建议中提取出自己的实际应用。

如何阅读本书

虽然本书包含了大量的例子,但它不是一本食谱。它呈现了谷歌和行业故事,并分享了多年来我们所学到的东西。每个人的基础设施都是不同的,因此您可能需要大幅调整我们提出的一些解决方案,有些解决方案可能根本不适用于您的组织。我们试图提出高层原则和实际解决方案,以便您可以以适合您独特环境的方式实现。

我们建议您从 1 和 2 章开始阅读,然后阅读您最感兴趣的章节。大多数章节都以方框内的前言或执行摘要开头,概述以下内容:

  • 问题陈述

  • 在软件开发生命周期中,您应该应用这些原则和实践

  • 要考虑的可靠性和安全性之间的交集和/或权衡

在每一章中,主题通常按从最基础到最复杂的顺序排列。我们还用鳄鱼图标标出深入研究和专业主题。

本书推荐了许多工具或技术,被认为是行业中的良好实践。并非每个想法都适用于您的特定用例,因此您应该评估项目的要求,并设计适应您特定风险环境的解决方案。

虽然本书旨在是自包含的,但您会发现Site Reliability EngineeringThe Site Reliability Workbook的参考,其中谷歌的专家描述了可靠性对服务设计的基本性质。阅读这些书可能会让您更深入地理解某些概念,但这不是必要条件。

我们希望您喜欢这本书,并且这些页面中的一些信息可以帮助您提高系统的可靠性和安全性。

本书使用的约定

本书使用以下排版约定:

斜体

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

等宽字体

用于程序列表,以及段落内引用程序元素,如变量或函数名称,数据库,数据类型,环境变量,语句和关键字。

**固定宽度粗体**

显示用户应直接输入的命令或其他文本。也用于程序列表中的强调。

*固定宽度斜体*

显示应由用户提供值或由上下文确定的值的文本。

注意

此元素表示一般说明。

深入了解

此图标表示深入了解。

O'Reilly 在线学习

注意

40 多年来,O'Reilly Media提供技术和商业培训,知识和见解,帮助公司取得成功。

我们独特的专家和创新者网络通过书籍,文章,会议和我们的在线学习平台分享他们的知识和专业知识。O'Reilly 的在线学习平台为您提供按需访问实时培训课程,深入学习路径,交互式编码环境以及来自 O'Reilly 和其他 200 多家出版商的大量文本和视频。有关更多信息,请访问https://oreilly.com

如何联系我们

请将有关本书的评论和问题发送至出版商:

  • O'Reilly Media,Inc.

  • 1005 Gravenstein Highway North

  • 塞巴斯托波尔,CA 95472

  • 800-998-9938(在美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为这本书创建了一个网页,列出勘误,示例和任何其他信息。您可以在https://oreil.ly/buildSecureReliableSystems上访问此页面。

发送电子邮件bookquestions@oreilly.com以评论或询问有关本书的技术问题。

有关我们的图书,课程,会议和新闻的更多信息,请访问我们的网站https://www.oreilly.com

在 Facebook 上找到我们:https://facebook.com/oreilly

在 Twitter 上关注我们:https://twitter.com/oreillymedia

在 YouTube 上观看我们:https://www.youtube.com/oreillymedia

致谢

这本书是大约 150 人的热情和慷慨贡献的成果,包括来自工程,法律和营销的作者,技术作家,章节经理和审阅人员。贡献者遍布美洲,欧洲和亚太地区的 18 个时区,以及 10 多个办公室。我们想花点时间感谢每个人,他们已经列在每章的基础上。

作为 Google 安全和 SRE 的领导者,Gordon Chaffee,Royal Hansen,Ben Lutch,Sunil Potti,Dave Rensin,Benjamin Treynor Sloss 和 Michael Wildpaner‎是 Google 内部的执行赞助商。他们对将安全性和可靠性直接整合到软件和系统生命周期的项目的信念对于使这本书成为现实至关重要。

如果没有 Ana Oprea 的努力和奉献精神,这本书将永远不会问世。她认识到这样一本书可能具有的价值,在 Google 发起了这个想法,向 SRE 和安全领导者传道,并组织了必要的大量工作。

我们要感谢那些通过提供深思熟虑的意见,讨论和审查做出贡献的人。按章节顺序,他们是:

  • 第一章,安全性和可靠性的交汇点:Felipe Cabrera,Perry The Cynic 和 Amanda Walker

  • 第二章,了解对手:John Asante,Shane Huntley 和 Mike Koivunen

  • 第三章,案例研究:安全代理:Amaya Booker,Michał Czapiński,Scott Dier 和 Rainer Wolafka

  • 第四章,设计权衡:Felipe Cabrera,Douglas Colish,Peter Duff,Cory Hardman,Ana Oprea 和 Sergey Simakov

  • 第五章,最小权限设计:Paul Guglielmino 和 Matthew Sachs‎

  • 第六章,易懂设计:Douglas Colish,Paul Guglielmino,Cory Hardman,Sergey Simakov 和 Peter Valchev

  • 第七章,应对不断变化的景观设计:Adam Bacchus,Brandon Baker,Amanda Burridge,Greg Castle,Piotr Lewandowski,Mark Lodato,Dan Lorenc,Damian Menscher,Ankur Rathi,Daniel Rebolledo Samper,Michee Smith,Sampath Srinivas,Kevin Stadmeyer 和 Amanda Walker

  • 第八章,弹性设计:Pierre Bourdon,Perry The Cynic,Jim Higgins,August Huber,Piotr Lewandowski,Ana Oprea,Adam Stubblefield,Seth Vargo 和 Toby Weingartner

  • 第九章,恢复设计:Ana Oprea 和 JC van Winkel

  • 第十章,减轻拒绝服务攻击:Zoltan Egyed,Piotr Lewandowski 和 Ana Oprea

  • 第十一章,案例研究:设计、实现和维护公开可信 CA:Heather Adkins,Betsy Beyer,Ana Oprea 和 Ryan Sleevi

  • 第十二章,编写代码:Douglas Colish,Felix Gröbert,Christoph Kern,Max Luebbe,Sergey Simakov 和 Peter Valchev

  • 第十三章,测试代码:Douglas Colish,Daniel Fabian,Adrien Kunysz,Sergey Simakov 和 JC van Winkel‎

  • 第十四章,部署代码:Brandon Baker,Max Luebbe 和 Federico Scrinzi

  • 第十五章,调查系统:‎Oliver Barrett‎,Pierre Bourdon 和 Sandra Raicevic

  • 第十六章,灾难规划:Heather Adkins,John Asante,Tim Craig 和 Max Luebbe

  • 第十七章,危机管理:Heather Adkins,Johan Berggren,John Lunney,James Nettesheim,Aaron Peterson 和 Sara Smollet

  • 第十八章,恢复和后果:Johan Berggren,Matt Linton,Michael Sinno 和 Sara Smollett

  • 第十九章,Chrome 安全团队案例研究:Abhishek Arya,Will Harris,Chris Palmer,Carlos Pizano,Adrienne Porter Felt 和 Justin Schuh

  • 第二十章,理解角色和责任:Angus Cameron,Daniel Fabian,Vera Haas,Royal Hansen,Jim Higgins,August Huber,Artur Janc,Michael Janosko,Mike Koivunen,Max Luebbe,Ana Oprea,Andrew Pollock,Laura Posey,Sara Smollett,Peter Valchev 和 Eduardo Vela Nava

  • 第二十一章,建立安全和可靠文化:David Challoner,Artur Janc,Christoph Kern,Mike Koivunen,Kostya Serebryany 和 Dave Weinstein

我们还要特别感谢 Andrey Silin 在整本书中的指导。

以下审阅者为我们提供了宝贵的见解和反馈,指导我们前进:Heather Adkins,Kristin Berdan,Shaudy Danaye-Armstrong,Michelle Duffy,Jim Higgins,Rob Mann,Robert Morlino,Lee-Anne Mulholland,Dave O’Connor,Charles Proctor,Olivia Puerta,John Reese,Pankaj Rohatgi,Brittany Stagnaro,Adam Stubblefield,Todd Underwood 和 Mia Vu。特别感谢 JC van Winkel 进行了书籍级别的一致性审查。

我们还要感谢以下贡献者,他们提供了重要的专业知识或资源,或者对这项工作产生了一些其他卓越的影响:Ava Katushka,Kent Kawahara,Kevin Mould,Jennifer Petoff,Tom Supple,Salim Virji‎和 Merry Yen。

Eric Grosse 的外部定向审查帮助我们在新颖性和实用建议之间取得了良好的平衡。我们非常感谢他的指导,以及来自整本书的行业审阅者 Blake Bisset,David N. Blank-Edelman,Jennifer Davis 和 Kelly Shortridge 的深思熟虑的反馈。以下人员的深入审查使每一章更加针对外部受众:Kurt Andersen,Andrea Barberio,Akhil Behl,Alex Blewitt,Chris Blow,Josh Branham,Angelo Failla,Tony Godfrey,Marco Guerri,Andrew Hoffman,Steve Huff,Jennifer Janesko,Andrew Kalat,Thomas A. Limoncelli,Allan Liska,John Looney,Niall Richard Murphy,Lukasz Siudut,Jennifer Stevens,Mark van Holsteijn 和 Wietse Venema。

我们要特别感谢 Shylaja Nukala 和 Paul Blankinship,他们慷慨地投入了 SRE 和安全技术写作团队的时间和技能。

最后,我们要感谢以下贡献者,他们在这本书中没有直接出现的内容上工作:Heather Adkins,Amaya Booker,Pierre Bourdon,Alex Bramley,Angus Cameron,David Challoner,Douglas Colish,Scott Dier,Fanuel Greab,Felix Gröbert,Royal Hansen,Jim Higgins,August Huber,Kris Hunt,Artur Janc,Michael Janosko,Hunter King,Mike Koivunen,Susanne Landers,Roxana Loza,Max Luebbe,Thomas Maufer,Shylaja Nukala‎,Ana Oprea,Massimiliano Poletto,Andrew Pollock,Laura Posey,Sandra Raicevic,Fatima Rivera,Steven Roddis,Julie Saracino,David Seidman,Fermin Serna,Sergey Simakov,Sara Smollett,Johan Strumpfer,Peter Valchev,Cyrus Vesuna,Janet Vong,Jakub Warmuz,Andy Warner 和 JC van Winkel。

还要感谢 O'Reilly Media 团队——Virginia Wilson,Kristen Brown,John Devins,Colleen Lobner 和 Nikki McDonald——他们在使这本书成为现实方面提供了帮助和支持。感谢 Rachel Head 带来了一次奇妙的编辑体验!

最后,书籍核心团队也想亲自感谢以下人员:

来自 Heather Adkins

人们经常问我谷歌如何保持安全,我能给出的最简短的答案是,谷歌员工的多样化特质是谷歌自卫能力的关键。这本书反映了这种多样性,我相信在我的一生中,我不会发现比谷歌员工更伟大的互联网捍卫者。我个人特别感谢我的美妙丈夫 Will(+42!!),我的妈妈(Libby),爸爸(Mike)和哥哥(Patrick),以及 Apollo 和 Orion,因为他们插入了所有这些错别字。感谢我的团队和谷歌同事在写这本书期间容忍我的缺席,并在面对巨大对手时表现出的坚韧;感谢 Eric Grosse,Bill Coughran,Urs Hölzle,Royal Hansen,Vitaly Gudanets 和 Sergey Brin 在过去 17 年多的指导,反馈和偶尔的挑眉;感谢我的亲爱的朋友和同事(Merry,Max,Sam,Lee,Siobhan,Penny,Mark,Jess,Ben,Renee,Jak,Rich,James,Alex,Liam,Jane,Tomislav 和 Natalie),特别是 r00t++,感谢你们的鼓励。感谢 John Bernhardt 博士教会我很多;抱歉我没有完成学位!

来自 Betsy Beyer

感谢祖母,Elliott,阿姨 E 和 Joan,你们每一天都激励着我。你们是我的英雄!还有 Duzzie,Hammer,Kiki,Mini 和 Salim,你们的积极性和理智检查让我保持理智!

来自 Paul Blankinship

首先,我要感谢 Erin 和 Miller,我依赖他们的支持,还有 Matt 和 Noah,他们总是让我笑。我要对我的谷歌朋友和同事表示感激,特别是我的技术作家同行,他们在概念和语言上挣扎,需要同时成为专家和对天真用户的倡导者。我非常感激这本书的其他作者们——我钦佩并尊重你们每一个人,能够与你们的名字联系在一起是我的荣幸。

来自 Susanne Landers

对于这本书的所有贡献者,我无法表达我有多荣幸能够成为这个旅程的一部分!没有一些特别的人,我今天就不会在这里:Tom 找到了合适的机会;Cyrill 教会了我今天所知道的一切;Hannes,Michael 和 Piotr 邀请我加入了有史以来最棒的团队(Semper Tuti!)。对于那些带我喝咖啡的人(你们知道自己是谁!),没有你们生活将会无比无聊。对 Verbena,她可能比任何其他人更能塑造我,最重要的是,对我生命中最爱的人的无条件支持,以及我们最美好和美妙的孩子们。我不知道我怎么配得上你们,但我会尽我最大的努力。

来自 Piotr Lewandowski

对于每个人都让世界变得比他们发现时更美好。感谢我的家人无条件的爱。感谢我的伴侣与我分享她的生活,无论是好是坏。感谢我的朋友给我生活带来的快乐。感谢我的同事成为我工作中最好的部分。感谢我的导师持续的信任;没有他们的支持,我就不可能成为这本书的一部分。

来自 Ana Oprea

对于这本书即将印刷时将出生的小家伙。感谢我的丈夫 Fabian,他支持我并使我能够在建立家庭的同时从事这项工作和许多其他事情。我感激我的父母 Ica 和 Ion 理解我离他们很远。这个项目证明了没有开放、建设性的反馈循环就不会有进步。我之所以能够领导这本书,是因为我在过去几年中所获得的经验,这要感谢我的经理 Jan 以及整个开发者基础设施团队,他们信任我将工作重点放在安全、可靠性和开发的交叉点上。最后但同样重要的是,我要对 BSides:慕尼黑和 MUC:SEC 的支持社区表示感激,这些地方一直是我不断学习的重要场所。

来自 Adam Stubblefield

感谢我的妻子,我的家人以及多年来所有的同事和导师。

¹ 例如,参见 Dino Dai Zovi 在 Black Hat USA 2019 的“Every Security Team Is a Software Team Now” talk,Open Security Summit 的DevSecOps track,以及 Dave Shackleford 的“A DevSecOps Playbook” SANS Analyst paper

第一部分:介绍性材料

原文:Part I. Introductory Material

译者:飞龙

协议:CC BY-NC-SA 4.0

如果你问你的客户列出他们产品的最喜欢的特性,他们的清单不太可能以安全性和可靠性开头。这两个特性通常隐藏在他们的期望中:如果它们运行良好,您的客户就不会注意到它们。

我们相信安全性和可靠性应该是任何组织的首要任务。最终,很少有人愿意使用不安全和不可靠的产品,因此这些方面提供了不同的商业价值。

本书的第一部分突出了安全和可靠系统的基本实践之间的重叠,以及您可能需要在两者之间进行权衡。然后,我们就潜在的对手提供了高层次的指导:他们可能如何行动,以及他们的行动可能如何影响您系统的生命周期。

第一章:安全性和可靠性的交集

原文:1. The Intersection of Security and Reliability

译者:飞龙

协议:CC BY-NC-SA 4.0

由亚当·斯塔布菲尔德、马西米利亚诺·波莱托和皮奥特·莱万多夫斯基撰写

与大卫·胡斯卡和贝琪·拜尔一起

关于密码和电钻

2012 年 9 月 27 日,一封无辜的谷歌公司范围内的公告在内部服务中引发了一系列连锁故障。最终,从这些故障中恢复需要使用电钻。

谷歌有一个内部密码管理器,允许员工存储和共享第三方服务的密码,这些服务不支持更好的身份验证机制。其中一个秘密是连接谷歌旧金山湾区校园的大型巴士上的访客 WiFi 系统的密码。

在那个九月的一天,公司的交通团队向成千上万的员工发送了一封电子邮件公告,称 WiFi 密码已更改。由此产生的流量激增远远超过了密码管理系统的处理能力,该系统多年前为一小群系统管理员开发而成。

密码管理器的主要副本由于负载过大而变得无响应,因此负载均衡器将流量转移到次要副本,但次要副本立即以相同的方式失败。此时,系统呼叫了值班工程师。工程师没有经验来应对服务的故障:密码管理器是在尽力支持的基础上运行的,并且在其存在的五年中从未发生过故障。工程师试图重新启动服务,但不知道重新启动需要硬件安全模块(HSM)智能卡。

这些智能卡存放在全球不同的谷歌办公室的多个保险柜中,但不在值班工程师所在的纽约市。当服务无法重新启动时,工程师联系了澳大利亚的一位同事来取回智能卡。令他们大为沮丧的是,澳大利亚的工程师无法打开保险柜,因为组合密码存储在现在已经离线的密码管理器中。幸运的是,加利福尼亚的另一位同事记住了现场保险柜的组合密码,并成功取回了智能卡。然而,即使加利福尼亚的工程师插入了卡片,服务仍然无法重新启动,显示了“密码无法加载任何保护此密钥的卡片”的加密错误。

此时,澳大利亚的工程师决定采用蛮力方法解决保险柜的问题,并用电钻进行了尝试。一个小时后,保险柜被打开了,但即使新取回的卡片也触发了相同的错误消息。

团队花了额外一个小时才意识到智能卡读卡器上的绿灯实际上并没有正确插入卡片。当工程师们翻转卡片时,服务重新启动,故障结束了。

可靠性和安全性都是真正值得信赖的系统的关键组成部分,但构建既可靠又安全的系统是困难的。虽然可靠性和安全性的要求有许多共同的属性,但它们也需要不同的设计考虑。很容易忽视可靠性和安全性之间微妙的相互作用,这可能导致意想不到的结果。密码管理器的故障是由可靠性问题引发的——负载均衡和负载分担策略不佳——而其恢复后又受到了多项旨在增加系统安全性的措施的影响。

可靠性与安全性:设计考虑

在设计可靠性和安全性时,您必须考虑不同的风险。主要的可靠性风险是非恶意的,例如,糟糕的软件更新或物理设备故障。然而,安全风险来自积极试图利用系统漏洞的对手。在设计可靠性时,您假设某些事情在某个时候会出错。在设计安全性时,您必须假设对手可能在任何时候试图让事情出错。

因此,不同的系统设计以非常不同的方式响应故障。在没有对手的情况下,系统通常会失败安全(或开放):例如,电子锁设计为在停电时保持开放,以便通过门安全出口。失败安全/开放行为可能导致明显的安全漏洞。为了防御可能利用停电的对手,您可以设计门在停电时失败安全并保持关闭。

保密性、完整性、可用性

安全性和可靠性都关注系统的保密性、完整性和可用性,但它们通过不同的视角看待这些属性。两种观点之间的关键区别在于是否存在恶意对手。可靠的系统不得意外泄露保密性,例如,一个有缺陷的聊天系统可能会误送、弄乱或丢失消息。此外,安全系统必须防止积极的对手访问、篡改或销毁机密数据。让我们看一些例子,说明可靠性问题如何导致安全问题。

注意

保密性、完整性和可用性一直被认为是安全系统的基本属性,并被称为CIA 三位一体。尽管许多其他模型将安全属性的集合扩展到这三个之外,但 CIA 三位一体随着时间的推移仍然很受欢迎。尽管有这个缩写,但这个概念与中央情报局没有任何关系。

保密性

在航空业中,麦克风卡在传输位置是一个显著的保密问题。在几个有记录的案例中,卡住的麦克风在驾驶舱中广播了飞行员之间的私人对话,这代表了保密的违反。在这种情况下,没有恶意的对手参与:硬件可靠性缺陷导致设备在飞行员不打算时进行传输。

完整性

同样,数据完整性损害不一定涉及主动对手。2015 年,谷歌的网站可靠性工程师(SREs)注意到一些数据块的端到端加密完整性检查失败了。因为一些处理数据的机器后来表现出不可纠正的内存错误的证据,SREs 决定编写软件,详尽地计算每个数据版本的完整性检查,只需一个位翻转(0 变为 1,反之亦然)。这样,他们可以看到是否有一个结果与原始完整性检查的值匹配。所有错误确实都是单位翻转,SREs 恢复了所有数据。有趣的是,这是一个安全技术在可靠性事件中发挥作用的例子。(谷歌的存储系统也使用非加密的端到端完整性检查,但其他问题阻止了 SREs 检测到位翻转。)

可用性

最后,当然,可用性既是可靠性问题,也是安全问题。对手可能利用系统的弱点使系统停止运行或损害其对授权用户的操作。或者他们可能控制世界各地的大量设备来执行经典的分布式拒绝服务(DDoS)攻击,指示许多设备向受害者发送流量。

拒绝服务(DoS)攻击是一个有趣的案例,因为它们跨越了可靠性和安全性的领域。从受害者的角度来看,恶意攻击可能与设计缺陷或合法的流量激增无法区分。例如,2018 年的软件更新导致一些谷歌 Home 和 Chromecast 设备在调整时钟时生成大量同步的网络流量,对谷歌的中央时间服务造成意外负载。同样,一条重大新闻报道或其他事件可能导致数百万人发出几乎相同的查询,看起来非常像传统的应用层 DDoS 攻击。如图 1-1 所示,在 2019 年 10 月的一个深夜,当 4.5 级地震袭击旧金山湾区时,谷歌为该地区提供服务的基础设施遭受了大量查询。

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

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

可靠性和安全性:共同点

可靠性和安全性——与许多其他系统特性不同——是系统设计的新兴属性。它们都很难在事后添加,因此最好从最早的设计阶段就考虑它们。它们还需要在整个系统生命周期中进行持续关注和测试,因为系统变化很容易无意中影响它们。在复杂系统中,可靠性和安全性属性通常由许多组件的相互作用决定,一个组件的看似无害的更新可能最终影响整个系统的可靠性或安全性,直到引发事故才会显现。让我们更详细地研究这些和其他共同点。

不可见性

可靠性和安全性在一切顺利时大多是看不见的。但可靠性和安全团队的目标之一是赢得并保持客户和合作伙伴的信任。良好的沟通——不仅在麻烦时,而且在一切顺利时——是建立这种信任的坚实基础。信息的诚实和具体性非常重要,不应包含陈词滥调和行话。

不幸的是,在没有紧急情况下,可靠性和安全性的固有不可见性意味着它们经常被视为可以减少或推迟的成本,而不会立即产生后果。然而,可靠性和安全性失败的成本可能是严重的。据媒体报道,数据泄露可能导致 Verizon 在 2017 年收购 Yahoo!互联网业务时减少了 3.5 亿美元的价格。同年,一次停电导致达美航空的关键计算机系统关闭,并导致近 700 次航班取消和数千次延误,使达美航空当天的航班吞吐量减少了约 60%。

评估

因为实现完美的可靠性或安全性并不切实际,所以可以使用基于风险的方法来估计负面事件的成本,以及预防这些事件的前期成本和机会成本。然而,应该以不同的方式衡量可靠性和安全性的负面事件的概率。您可以推断系统组合的可靠性,并根据所需的错误预算计划工程工作,至少部分原因是您可以假设各个组件之间的故障是独立的。这样的组合的安全性更难评估。分析系统的设计和实现可以提供一定程度的保证。对抗性测试——通常从定义的对手的角度进行的模拟攻击——也可以用来评估系统对特定类型攻击的抵抗能力,攻击检测机制的有效性以及攻击的潜在后果。

简单

将系统设计尽可能简单是提高系统可靠性和安全性评估能力的最佳途径之一。简单的设计减少了攻击面,减少了意外系统交互的可能性,并使人类更容易理解和推理系统。在紧急情况下,可理解性尤为重要,因为它可以帮助应对者快速缓解症状并减少修复时间(MTTR)。第六章详细讨论了这个话题,并讨论了诸如最小化攻击面和将安全不变量的责任隔离到可以独立推理的小型简单子系统等策略。

演变

无论最初的设计多么简单和优雅,系统很少会随着时间保持不变。新的功能要求、规模变化以及基础架构的演变都往往引入复杂性。在安全方面,需要跟上不断演变的攻击和新的对手也可能增加系统的复杂性。此外,满足市场需求的压力可能导致系统开发人员和维护人员采取捷径并积累技术债务。第七章讨论了其中一些挑战。

复杂性往往是无意中积累的,但这可能导致临界点情况,即一个小的看似无害的变化对系统的可靠性或安全性产生重大后果。2006 年引入的一个错误,几乎两年后在 Debian GNU/Linux 版本的 OpenSSL 库中被发现,提供了一个臭名昭著的例子,即由一个小的变化引起的重大故障。一个开源开发人员注意到 Valgrind,一个用于调试内存问题的标准工具,报告了关于初始化前使用的内存的警告。为了消除警告,开发人员删除了两行代码。不幸的是,这导致 OpenSSL 的伪随机数生成器只使用了一个进程 ID 作为种子,而在当时的 Debian 默认为 1 到 32,768 之间的一个数字。然后可以轻松地破解加密密钥。

谷歌并不免于由看似无害的变化引发的故障。例如,2018 年 10 月,由于一个通用日志记录库的小改动,YouTube 全球宕机超过一个小时。一个旨在改善事件记录粒度的变化对其作者和指定的代码审查者来说看起来是无害的,并且通过了所有测试。然而,开发人员并没有完全意识到在 YouTube 规模下这种改变的影响:在生产负载下,这个改变迅速导致 YouTube 服务器耗尽内存并崩溃。随着故障将用户流量转移到其他仍然健康的服务器,级联故障使整个服务停止运行。

韧性

当然,内存利用问题不应该导致全球服务中断。系统应该被设计成在不利或意外情况下具有弹性。从可靠性的角度来看,这些情况通常是由意外高负载或组件故障引起的。负载是系统请求的数量和平均成本的函数,因此你可以通过减少一部分传入负载(处理更少)或减少每个请求的处理成本(更便宜地处理)来实现弹性。为了解决组件故障,系统设计应该包括冗余和不同的故障域,这样你就可以通过重新路由请求来限制故障的影响。第八章进一步讨论了这些话题,第十章则深入探讨了特定的 DoS 缓解措施。

然而,无论系统的个别组件有多么具有弹性,一旦它变得足够复杂,你就无法轻易证明整个系统对妥协是免疫的。你可以部分地通过深度防御和不同的故障域来解决这个问题。深度防御是应用多种,有时是冗余的防御机制。不同的故障域限制了故障的“爆炸半径”,因此也增加了可靠性。一个良好的系统设计限制了对手利用受损主机或被盗凭证进行横向移动或提升特权并影响系统其他部分的能力。

你可以通过对权限进行分隔或限制凭证的范围来实现不同的故障域。例如,谷歌的内部基础设施支持明确限定地理区域的凭证。这些类型的功能可以限制攻击者在一个地区损坏服务器后横向移动到其他地区的能力。

为敏感数据使用独立的加密层是深度防御的另一个常见机制。例如,尽管磁盘提供设备级加密,但在应用层也加密数据通常是一个好主意。这样,即使驱动控制器中的加密算法实现有缺陷,也不足以 compromise 受保护数据的机密性,如果攻击者获得对存储设备的物理访问。

虽然迄今为止引用的例子都依赖于外部攻击者,但你也必须考虑来自恶意内部人员的潜在威胁。尽管内部人员可能比第一次窃取员工凭证的外部攻击者更了解潜在的滥用途径,但在实践中这两种情况通常并没有太大的区别。最小权限原则可以缓解内部威胁。它规定用户在特定时间内应该具有执行工作所需的最小权限集。例如,像 Unix 的sudo这样的机制支持细粒度策略,指定哪些用户可以以哪种角色运行哪些命令。

在谷歌,我们还使用多方授权来确保敏感操作得到特定员工组的审查和批准。这种多方机制既可以防范恶意内部人员,也可以减少无辜人为错误的风险,这是可靠性故障的常见原因。最小权限和多方授权并不是新概念——它们在许多非计算场景中都有应用,从核导弹发射井到银行保险库。第五章深入讨论了这些概念。

从设计到生产

即使是将坚实的设计转化为完全部署的生产系统时,也应该牢记安全性和可靠性考虑。从编写代码开始,通过代码审查可以发现潜在的安全性和可靠性问题,甚至可以通过使用常见的框架和库来预防整类问题。第十二章讨论了一些这些技术。

在部署系统之前,您可以使用测试来确保它在正常情况下和通常影响可靠性和安全性的边缘情况下都能正确运行。无论您使用负载测试来了解系统在大量查询下的行为,模糊测试来探索可能意外的输入的行为,还是专门的测试来确保加密库不会泄露信息,测试在获得保证实际构建的系统与设计意图匹配方面发挥着关键作用。第十三章深入讨论了这些方法。

最后,一些部署代码的方法(见第十四章)可以限制安全性和可靠性风险。例如,金丝雀发布和缓慢的部署可以防止您同时为所有用户破坏系统。同样,一个只接受经过适当审查的代码的部署系统可以帮助减轻内部人员将恶意二进制文件推送到生产环境的风险。

调查系统和日志记录

到目前为止,我们已经专注于设计原则和实现方法,以防止可靠性和安全性故障。不幸的是,实现完美的可靠性或安全性通常是不切实际或成本过高的。您必须假设预防机制会失败,并制定一个计划来检测和从故障中恢复。

正如我们在第十五章中讨论的那样,良好的日志记录是检测和故障准备的基础。一般来说,您的日志越完整和详细,越好,但这个指导原则也有一些注意事项。在足够大的规模下,日志量会带来显著的成本,并且有效分析日志可能变得困难。本章前面的 YouTube 示例说明了日志记录也可能引入可靠性问题。安全日志带来了额外的挑战:日志通常不应包含敏感信息,例如身份验证凭据或个人可识别信息(PII),以免日志本身成为对手的吸引目标。

危机响应

在紧急情况下,团队必须迅速而顺利地合作,因为问题可能会立即产生后果。在最坏的情况下,一次事件可能会在几分钟内摧毁一家企业。例如,2014 年,一名攻击者通过接管服务的管理工具并删除所有数据(包括所有备份)使代码托管服务 Code Spaces 在几个小时内破产。熟练的协作和良好的事件管理对及时应对这些情况至关重要。

组织危机响应是具有挑战性的,因此最好在紧急情况发生之前制定计划。当你发现事件时,时间可能已经过去了一段时间。无论如何,响应者都在压力和时间压力下运作,并且(至少最初)具有有限的情境意识。如果一个组织很大,事件需要 24/7 的响应能力或跨时区的协作,那么在团队之间保持状态并在工作时间交接边界处交接事件管理的需求进一步复杂化了操作。安全事件通常也涉及在需要知道的基础上限制信息共享的冲动与法律或监管要求驱使的需要之间的紧张关系。此外,最初的安全事件可能只是冰山一角。调查可能会超出公司范围或涉及执法机构。

在危机期间,拥有清晰的指挥链和一套可靠的检查表、操作手册和协议至关重要。正如第十六章和第十七章中所讨论的,谷歌已经将危机响应编码化为一个名为谷歌事件管理(IMAG)的项目,该项目建立了一种标准、一致的处理各种事件的方式,从系统故障到自然灾害,并组织有效的响应。IMAG 是基于美国政府的事件指挥系统(ICS)建立的,这是一种在多个政府机构的应急响应者之间进行指挥、控制和协调的标准化方法。

当没有面临持续事件的压力时,响应者通常会在很长的间隔时间内进行很少的活动。在这些时候,团队需要保持个人的技能和动力,并改进流程和基础设施,以应对下一次紧急情况。谷歌的灾难恢复测试计划(DiRT)定期模拟各种内部系统故障,并迫使团队应对这些类型的场景。频繁的攻击性安全演习测试我们的防御,并帮助发现新的漏洞。谷歌甚至针对小事件也使用 IMAG,这进一步促使我们定期练习紧急工具和流程。

恢复

从安全失败中恢复通常需要修补系统以修复漏洞。直觉上,你希望这个过程尽快进行,使用经常练习并因此相当可靠的机制。然而,快速推送更改的能力是一把双刃剑:虽然这种能力可以帮助快速关闭漏洞,但也可能引入导致大量损害的错误或性能问题。如果漏洞被广泛知晓或严重,那么推送补丁的压力就会更大。是否慢慢推送修复补丁——因此更有把握确保没有意外副作用,但风险是漏洞会被利用——或者快速推送补丁,最终取决于风险评估和商业决策。例如,为了修复严重漏洞,可能可以接受一些性能损失或增加资源使用。

这样的选择凸显了可靠的恢复机制的必要性,这些机制使我们能够快速推出必要的更改和更新,而不会影响可靠性,并且在造成大范围故障之前也能发现潜在问题。例如,一个强大的机群恢复系统需要可靠地表示每台机器的当前状态和期望状态,并且还需要提供后备措施,以确保状态永远不会回滚到过时或不安全的版本。第九章涵盖了这一点以及许多其他方法,第十八章讨论了一旦发生事件后如何实际恢复系统。

结论

安全性和可靠性有很多共同点——它们都是所有信息系统固有的属性,一开始很容易在速度的名义下牺牲,但事后修复却代价高昂。本书旨在帮助您在系统发展和成长过程中及早解决与安全性和可靠性相关的不可避免的挑战。除了工程努力外,每个组织都必须了解有助于建立安全性和可靠性文化的角色和责任(参见第二十章),以持续实践。通过分享我们的经验和教训,我们希望能够使您在系统生命周期的早期采纳本书中描述的一些原则,从而避免在未来付出更大的代价。

我们撰写本书时考虑了广泛的受众,希望您会发现它与您的项目的阶段或范围无关。阅读时,请牢记您项目的风险概况——运营股票交易所或为异见者提供通信平台与运营动物庇护所网站具有截然不同的风险概况。下一章将详细讨论对手的类别及其可能的动机。

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

第二章:了解对手

原文:2. Understanding Adversaries

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Heather Adkins 和 David Huska

作者:Jen Barnason‎

1986 年 8 月,劳伦斯利弗莫尔实验室的系统管理员克利福德·斯托尔(Clifford Stoll)偶然发现了一个看似无害的会计错误,导致了对从美国窃取政府机密的人进行了为期 10 个月的搜寻。1 这被普遍认为是其类别的第一个公开例子,斯托尔主导了一项调查,揭示了对手用来实现目标的具体战术、技术和程序(TTPs)。通过仔细研究,调查团队能够构建出攻击者如何针对和从受保护系统中吸取数据的画面。许多系统设计者已经吸收了从斯托尔的开创性文章中得出的教训,该文章描述了团队的努力,“追踪狡猾的黑客”。

在 2012 年 3 月,谷歌对其比利时数据中心的一次异常停电做出了回应,最终导致了当地数据损坏。调查发现,一只猫损坏了附近的外部电源,触发了建筑物电力系统的一系列级联故障。通过研究类似方式中复杂系统的故障,谷歌已经能够在全球范围内采用具有弹性的设计实践,悬挂、埋藏和浸入电缆。

了解系统的对手对于建立各种灾难的弹性和生存能力至关重要。在可靠性的背景下,对手通常以良性意图行事,并采取抽象的形式。它们可能存在为例常的硬件故障或者用户兴趣的压倒性案例(所谓的“成功灾难”)。它们也可能是导致系统以意想不到的方式行事的配置更改,或者意外切断海底光纤电缆的渔船。相比之下,在安全的背景下,对手是人类;他们的行动是经过计算的,以影响目标系统的不良方式。尽管这些对立的意图和方法,研究可靠性和安全对手对于理解如何设计和实现具有弹性的系统至关重要。没有这些知识,预测狡猾黑客或好奇猫的行动将是非常具有挑战性的。

在本章中,我们深入探讨了安全对手,以帮助不同领域的专家发展对抗性思维。也许会诱人地通过流行的刻板印象来思考安全对手:在黑暗地下室中的攻击者,有着聪明的绰号和潜在的可疑行为。虽然这样的丰富多彩的角色确实存在,但任何有时间、知识或金钱的人都可以破坏系统的安全。只需支付少许费用,任何人都可以购买软件,使他们能够接管他们可以物理接触的计算机或手机。政府经常购买或制造软件来破坏他们的目标系统。研究人员经常探究系统的安全机制,以了解它们的工作原理。因此,我们鼓励您保持客观的观点,了解谁在攻击系统。

没有两次攻击或攻击者是相同的。我们建议查看第二十一章讨论处理对手文化方面的内容。预测未来的安全灾难大多是一个猜测游戏,即使对于知识渊博的安全专家也是如此。在接下来的几节中,我们提出了三种框架来理解攻击者,这些框架多年来对我们有所帮助,探讨了攻击系统的人们的潜在动机,一些常见的攻击者档案,以及如何思考攻击者的方法。我们还在这三个框架内提供了说明性(并且希望有趣的)例子。

攻击者动机

安全对手首先是人类(至少目前是这样)。因此,我们可以通过攻击者的眼睛来考虑攻击的目的。这样做可能更好地装备我们了解我们应该如何做出反应,无论是主动(在系统设计期间)还是被动(在事件期间)。考虑以下攻击动机:

娱乐

为了纯粹的乐趣而破坏系统的安全性,因为他们知道这是可以做到的。

名望

为了展示技术技能而获得恶名。

活动主义

为了表达观点或传播信息,通常是政治观点,广泛传播。

经济利益

赚钱

胁迫

让受害者有意做一些他们不想做的事情。

操纵

创建预期的结果或改变行为,例如发布虚假数据(错误信息)。

间谍活动

获取可能有价值的信息(间谍活动,包括工业间谍活动)。这些攻击通常由情报机构执行。

破坏

破坏系统,销毁其数据,或者只是将其下线。

攻击者可能同时是一个出于经济动机的漏洞研究人员、政府间谍特工和犯罪行为者!例如,2018 年 6 月,美国司法部起诉了朝鲜公民朴振赫,指控他代表政府参与了各种活动,包括制造臭名昭著的 2017 年 WannaCry 勒索软件(用于谋取经济利益),2014 年侵犯索尼影视公司(旨在迫使索尼不发布一部有争议的电影,并最终损害公司的基础设施),以及电力公用事业的侵犯(据推测是出于间谍或破坏目的)。研究人员还观察到政府攻击者使用相同的恶意软件在国家级攻击中用于在视频游戏中窃取电子货币以获取个人利益。

在设计系统时,重要的是要记住这些多样的动机。考虑一个正在代表其客户进行资金转账的组织。如果我们了解攻击者可能对该系统感兴趣的原因,我们就可以更安全地设计系统。在这种情况下,一个可能的动机的很好的例子可以在一群朝鲜政府攻击者(包括朴)的活动中看到,他们据称企图通过侵入银行系统并利用 SWIFT 交易系统从客户账户中转移资金来窃取数百万美元。

攻击者档案

通过考虑这些人本身,我们可以更好地理解攻击者的动机:他们是谁,他们是为自己还是为他人进行攻击,以及他们的一般兴趣。在本节中,我们概述了一些攻击者的档案,指出他们与系统设计者的关系,并包括一些保护系统免受这些类型攻击者侵害的建议。为了简洁起见,我们对一些概括进行了一些自由解释,但请记住:没有两次攻击或攻击者是相同的。这些信息旨在作为说明性的,而不是决定性的。

业余爱好者

最早的计算机黑客是“业余爱好者”——渴望了解系统工作原理的好奇技术人员。在拆解计算机或调试程序的过程中,这些“黑客”发现了原始系统设计者没有注意到的缺陷。一般来说,业余爱好者的动机是对知识的渴望;他们以娱乐为目的进行黑客攻击,并且可以成为希望在系统中建立弹性的开发者的盟友。通常情况下,业余爱好者遵守有关不损害系统的个人道德规范,并且不会涉足犯罪行为。通过利用对这些黑客如何思考问题的洞察,您可以使您的系统更加安全。

漏洞研究人员

漏洞研究人员将他们的安全专业知识作为职业。他们可以作为全职员工、兼职自由职业者,甚至作为偶然发现漏洞的普通用户。许多研究人员参与漏洞奖励计划,也被称为漏洞赏金(见第二十章)。

漏洞研究人员通常受到改善系统的动机,并且可以成为寻求保护其系统的组织的重要盟友。他们往往遵循一套可预测的披露规范,这些规范设定了系统所有者和研究人员之间关于漏洞的发现、报告、修复和讨论的期望。遵循这些规范的研究人员避免不当访问数据、造成伤害或违法行为。通常来说,违反这些规范会使得获得奖励的可能性无效,并且可能被视为犯罪行为。

与此相关的,红队和渗透测试人员在获得系统所有者的许可下攻击目标,并且可能会被明确聘请进行这些练习。与研究人员一样,他们寻找击败系统安全的方法,专注于改善安全,并遵守一套道德准则。有关红队的更多讨论,请参见第二十章。

政府和执法机构

政府组织(例如执法机构和情报机构)可能会聘请安全专家来收集情报、打击国内犯罪、进行经济间谍活动,或者辅助军事行动。到目前为止,大多数国家政府都投资于培养这些目的的安全专业知识。在某些情况下,政府可能会转向刚刚毕业的有才华的学生、在监狱度过时间的改过自新的攻击者,或者安全行业的知名人士。虽然我们无法在这里广泛涵盖这些类型的攻击者,但我们提供了一些他们最常见活动的例子。

情报收集

情报收集可能是最公开讨论的政府活动,这些活动雇佣了懂得如何侵入系统的人。在过去的几十年中,包括信号情报(SIGINT)和人员情报(HUMINT)在内的传统间谍技术随着互联网的出现而现代化。在 2011 年的一个著名例子中,安全公司 RSA 遭到了攻击,许多专家将这些攻击者与中国的情报机构联系在一起。攻击者侵入 RSA 以窃取其流行的双因素身份验证令牌的加密种子。一旦他们获得了这些种子,攻击者就不需要物理令牌来生成一次性身份验证凭证,以登录美国军方技术承包商洛克希德·马丁公司的系统。曾经,侵入像洛克希德这样的公司会由现场的人员操作,例如贿赂员工或者在公司雇佣间谍。然而,系统入侵的出现使得攻击者能够使用更复杂的电子技术以新的方式获取秘密。

军事目的

政府可能侵入系统用于军事目的,专家们通常称之为网络战信息战。想象一下,一个政府想要入侵另一个国家。他们是否能够攻击目标的防空系统,并欺骗他们不认出来袭击的空军?他们是否能够关闭他们的电力、水务或银行系统?或者,想象一下,一个政府想要阻止另一个国家建造或获取武器。他们是否能够远程和悄悄地干扰他们的进展?这种情况据说发生在 2000 年代末的伊朗,当时攻击者非法地将一款模块化软件引入用于浓缩铀的离心机的控制系统。研究人员称之为Stuxnet,这次行动据说旨在摧毁离心机并停止伊朗的核计划。

监管国内活动

政府也可能侵入系统以监管国内活动。最近的一个例子是,网络安全承包商 NSO 集团向各国政府出售软件,允许私人监视人们之间的通信,而这些人并不知情(通过远程监视手机通话)。据报道,这款软件旨在监视恐怖分子和罪犯,这些目标相对不具争议。不幸的是,NSO 集团的一些政府客户也使用该软件监听记者和活动人士,有时导致骚扰、逮捕,甚至可能导致死亡。政府使用这些能力对待自己的人民的伦理问题是一个备受争议的话题,特别是在法律框架不健全、监督不到位的国家。

保护您的系统免受国家行为者的侵害

系统设计者应仔细考虑他们是否可能成为国家行为者的目标。为此,您需要了解您的组织所进行的可能对这些行为者具有吸引力的活动。考虑一个构建和销售微处理器技术给政府军事部门的技术公司。其他政府可能也对这些芯片感兴趣,并可能通过电子手段窃取其设计。

您的服务可能也拥有政府想要但难以获取的数据。一般来说,情报机构和执法部门重视个人通信、位置数据和类似类型的敏感个人信息。2010 年 1 月,谷歌宣布它目睹了来自中国的一次复杂的有针对性的攻击(研究人员称之为“奥罗拉行动”),针对的是长期访问 Gmail 账户的企业基础设施。存储客户的个人信息,特别是私人通信,可能会增加情报或执法机构对您的系统感兴趣的风险。

有时候您可能是一个目标,却没有意识到。奥罗拉行动不仅限于大型科技公司——它影响了至少 20 个受害者,涉及金融、技术、媒体和化工等多个领域。这些组织既有大型也有小型,许多人并不认为自己处于国家攻击的风险之中。

例如,一个旨在为运动员提供数据跟踪分析的应用程序。这些数据是否会成为情报机构的吸引目标?2018 年,分析人员在健身追踪公司 Strava 创建的公共热图上考虑了这个确切的问题,当他们注意到美国军队使用该服务跟踪他们的锻炼时,揭示了叙利亚秘密军事基地的位置

系统设计者还应该意识到,政府通常可以投入大量资源来获取他们感兴趣的数据。对抗对你的数据感兴趣的政府可能需要远远超出你的组织可以投入的资源来实现安全解决方案。我们建议组织在建立安全防御方面采取长远眼光,通过早期投资保护他们最敏感的资产,并通过持续的严格计划,随着时间的推移应用新的保护层。理想的结果是迫使对手耗费大量资源来针对你,增加他们被抓到的风险,以便他们的活动可以被揭露给其他可能的受害者和政府当局。

活动分子

骇客活动是利用技术呼吁社会变革的行为。这个术语被宽泛地应用于各种在线政治活动,从颠覆政府监视到对系统的恶意破坏。⁴为了考虑如何设计系统,我们在这里考虑后一种情况。

骇客活动分子已经被知晓破坏网站,即用政治信息替换正常内容。在2015 年的一个例子中,叙利亚电子军(支持巴沙尔·阿萨德政权的一群恶意行为者)接管了为www.army.mil提供网页流量的内容分发网络(CDN)。攻击者随后能够插入一条支持阿萨德的信息,随后被访问网站的访客看到。这种攻击对网站所有者来说可能非常尴尬,并可能破坏用户对网站的信任。

其他骇客活动分子的攻击可能会更具破坏性。例如,2012 年 11 月,分散的国际骇客活动分子组织匿名者⁵ 通过拒绝服务攻击使许多以色列网站下线。结果,访问受影响的网站的人经历了缓慢的服务或错误。这种性质的分布式拒绝服务攻击向受害者发送来自全球数千台受损机器的洪水式流量。这些所谓的僵尸网络经纪人通常在线提供这种能力,使攻击变得普遍且易于实现。在严重程度的另一端,攻击者甚至可能威胁要完全摧毁或破坏系统,这促使一些研究人员将他们标记为网络恐怖分子。

与其他类型的攻击者不同,骇客活动分子通常会公开宣称他们的活动,并经常公开宣称。这可以通过多种方式表现出来,包括在社交媒体上发布或破坏系统。参与此类攻击的活动分子甚至可能并不具备很高的技术水平。这可能使得预测或防御骇客活动变得困难。

保护你的系统免受骇客活动分子的攻击

我们建议思考你的业务或项目是否涉及可能引起活动分子注意的有争议的话题。例如,你的网站是否允许用户托管他们自己的内容,比如博客或视频?你的项目是否涉及政治问题,比如动物权利?活动分子是否使用你的产品,比如消息服务?如果对任何一个问题的答案是“是”,你可能需要考虑非常强大的、分层的安全控制,以确保你的系统修补漏洞并且能够抵御 DoS 攻击,并且你的备份能够快速恢复系统和数据。

犯罪行为者

攻击技术用于执行与其非数字化表亲非常相似的犯罪行为 - 例如,身份欺诈,盗窃钱财和勒索。犯罪分子具有各种技术能力。有些可能很复杂,并编写自己的工具。其他人可能购买或借用其他人构建的工具,依赖于它们易于使用的点击式攻击界面。事实上,社会工程 - 欺骗受害者协助您进行攻击的行为 - 尽管难度最低,但非常有效。对于大多数犯罪分子来说,进入的唯一障碍是一点时间,一台计算机和一点现金。

提供数字领域中发生的各种犯罪活动的完整目录是不可能的,但我们在这里提供了一些说明性的例子。例如,想象一下,你想预测并购活动,以便相应地安排某些股票交易。2014 年至 2015 年,中国的三名犯罪分子就有这个想法,并通过从毫不知情的律师事务所窃取敏感信息赚了几百万美元

在过去的 10 年中,攻击者还意识到受害者在其敏感数据受到威胁时会交出钱财。勒索软件是一种软件,它将系统或其信息扣为人质(通常是通过加密),直到受害者向攻击者支付赎金。通常,攻击者通过利用漏洞,将勒索软件与合法软件捆绑在一起,或者欺骗用户自行安装来感染受害者的机器。

犯罪活动并不总是以公开的偷钱行为表现出来。跟踪软件 - 通常以低至 20 美元的价格出售的间谍软件旨在在不知情的情况下收集有关另一个人的信息。恶意软件被引入受害者的计算机或手机上,要么是通过欺骗受害者安装它,要么是通过攻击者直接安装到设备上。一旦安装完成,该软件就可以记录视频和音频。由于跟踪软件通常由受害者身边的人使用,比如配偶,这种信任的滥用可能会产生毁灭性的效果。

并非所有犯罪分子都为自己工作。公司,律师事务所,政治活动,卡特尔,帮派和其他组织都会为了自己的目的而雇佣恶意行为者。例如,一名哥伦比亚攻击者声称他被聘请协助墨西哥 2012 年总统竞选候选人以及拉丁美洲其他选举,通过窃取反对派信息和散布虚假信息。在利比里亚的一个惊人案例中,一家移动电话服务提供商 Cellcom 的雇员据称聘请了一名攻击者来破坏其竞争对手 Lonestar 的网络。这些攻击破坏了 Lonestar 为其客户提供服务的能力,导致该公司损失了大量收入。

保护您的系统免受犯罪分子的攻击

在设计系统以抵御犯罪行为者时,请记住这些行为者往往倾向于选择最简单的方式以最小的成本和努力来实现他们的目标。如果你的系统足够强大,他们可能会将注意力转移到另一个受害者身上。因此,请考虑他们可能会瞄准哪些系统,以及如何使他们的攻击变得昂贵。完全自动化的公共图灵测试(CAPTCHA)系统的发展是如何随着时间增加攻击成本的一个很好的例子。CAPTCHA 用于确定网站是否与人类或自动机器人进行交互,例如在登录时。机器人通常是恶意活动的迹象,因此能够确定用户是否为人类可能是一个重要的信号。早期的 CAPTCHA 系统要求人类验证略微扭曲的字母或数字,这些对机器人来说很难识别。随着机器人变得更加复杂,CAPTCHA 实现者开始使用扭曲图片和物体识别。这些策略旨在随着时间的推移显著增加攻击 CAPTCHA 的成本。⁶

自动化和人工智能

2015 年,美国国防高级研究计划局(DARPA)宣布了网络大挑战比赛,旨在设计一个网络推理系统,能够自学习并在没有人类干预的情况下发现软件漏洞,开发利用这些漏洞的方法,然后修补这些利用。七个团队参加了现场的“最终事件”,并观看他们完全独立的推理系统在一个大型舞厅内相互攻击。第一名的团队成功开发了这样一个自学习系统!

网络大挑战的成功表明,未来至少有一些攻击可能会在没有人类直接控制的情况下执行。科学家和伦理学家在思考是否完全有意识的机器可能足够能够学会如何相互攻击。自主攻击平台的概念也促使了对越来越自动化的防御的需求,我们预测这将是未来系统设计者重要的研究领域。

保护您的系统免受自动化攻击

为了抵御自动化攻击的冲击,开发人员需要默认考虑到弹性系统设计,并能够自动迭代其系统的安全姿态。我们在本书中涵盖了许多这些主题,比如第五章中的自动化配置分发和访问理由;第十四章中的代码的自动构建、测试和部署;以及第八章中的处理 DoS 攻击。

内部人员

每个组织都有内部人员:目前或曾经的员工,他们被信任可以内部访问系统或专有知识。内部人员风险就是这些个人带来的威胁。当一个人能够执行一系列恶意、疏忽或意外情况的行动时,他就成为了内部人员威胁,这可能会对组织造成伤害。内部风险是一个庞大的主题,可以填满几本书的篇幅。为了帮助系统设计者,我们在这里简要介绍这个主题,考虑了三个一般类别,如表 2-1 中所述。

表 2-1. 内部人员的一般类别和示例

第一方内部人员 第三方内部人员 相关内部人员
员工 第三方应用程序开发人员 朋友
实习生 开源贡献者 家人
高管 受信任的内容贡献者 室友
董事会成员 商业合作伙伴
承包商
供应商
审计员

第一方内部人员

第一方内部人员是为了特定目的而被引入的人员,通常是直接参与实现业务目标。这个类别包括直接为公司工作的员工、高管和做出关键公司决策的董事会成员。您可能还能想到其他属于这个类别的人。具有对敏感数据和系统的第一方访问权限的内部人员占据了大多数关于内部风险的新闻报道。以通用电气公司的工程师为例,他于 2019 年 4 月被起诉窃取专有文件,将其嵌入照片中使用隐写软件(以掩盖其盗窃行为),并将其发送到他的个人电子邮件账户。检察官声称,他的目标是使他和他在中国的商业伙伴能够生产通用电气公司的涡轮机的低成本版本,并将其出售给中国政府。这样的故事在生产下一代技术的高科技公司中普遍存在。

对个人数据的访问也可能诱惑内部人员具有窥视倾向的人,那些想要因为拥有特权访问而显得重要的人,甚至那些想要出售此类信息的人。在2008 年的一个臭名昭著的案例中,几名医院工作人员因不当查看患者文件(包括知名名人)而被 UCLA 医疗中心解雇。随着越来越多的消费者注册社交网络、消息传递和银行服务,保护他们的数据免受不当员工访问比以往任何时候都更为重要。

一些最激进的内部风险故事涉及不满的内部人员。2019 年 1 月,一名因绩效不佳而被解雇的男子被判删除了他前雇主的 23 个虚拟服务器。这一事件使公司失去了重要合同和大量收入。几乎任何存在一段时间的公司都有类似的故事。由于就业关系的动态性质,这种风险是不可避免的。

前面的例子涵盖了恶意意图影响系统和信息安全的情况。然而,正如本书前面的一些例子所说明的,第一方内部人员也可能影响系统的可靠性。例如,前一章讨论了一系列不幸的内部人员行为,这些行为影响了密码存储系统的设计、运行和维护,从而阻止了 SRE 在紧急情况下访问凭据。正如我们将看到的,预见内部人员可能引入的错误对于保证系统完整性至关重要。

第三方内部人员

随着开源软件和开放平台的兴起,越来越有可能内部威胁可能是您组织中很少有人(或没有人)见过的人。考虑以下情景:您的公司开发了一个有助于处理图像的新库。您决定开源该库并接受公众的代码更改列表。除了公司员工,您现在还必须考虑开源贡献者作为内部人员。毕竟,如果您从未见过的世界另一端的开源贡献者提交了恶意更改列表,他们可以伤害使用您的库的人。

同样,开源开发人员很少有能力在可能部署其代码的所有环境中测试其代码。对代码库的添加可能引入不可预测的可靠性问题,例如意外的性能下降或硬件兼容性问题。在这种情况下,您需要实现控制,确保所有提交的代码都经过彻底审查和测试。有关该领域最佳实践的更多详细信息,请参阅第十三章和第十四章。

您还应该仔细考虑如何通过应用程序编程接口(API)扩展产品的功能。假设您的组织开发了一个人力资源平台,具有第三方开发者 API,以便公司可以轻松地扩展软件的功能。如果第三方开发者对数据拥有特权或特殊访问权限,他们现在可能是内部威胁。仔细考虑通过 API 提供的访问权限,以及第三方获得访问权限后可以做什么。您能否限制这些扩展内部人员对系统可靠性和安全性的影响?

相关内部人员

我们经常会对我们生活在一起的人有隐含的信任,但是系统设计者在设计安全系统时经常忽视这些关系。考虑这样一种情况,员工在周末把他们的笔记本电脑带回家。当它在厨房桌子上解锁时,谁可以访问该设备,他们可能会产生什么影响,无论是恶意还是无意的?远程办公、在家工作和深夜的值班对技术工作者来说越来越普遍。在考虑内部风险威胁模型时,一定要使用“工作场所”的广泛定义,其中也包括家庭。键盘后面的人可能并不总是“典型”的内部人员。

内部风险威胁建模

存在许多用于建模内部风险的框架,从简单到高度专题特定、复杂和详细。如果您的组织需要一个简单的模型来开始,我们已成功使用表 2-2 中的框架。这个模型也适用于快速的头脑风暴会议或有趣的卡牌游戏。

表 2-2. 内部风险建模框架

角色/行为者 动机 行为 目标
工程 意外 数据访问 用户数据
运营 疏忽 外泄(盗窃) 源代码
销售 受损 删除 文档
法律 金融 修改 日志
市场营销 意识形态 注入 基础设施
高管 报复性 向媒体泄露 服务
虚荣 财务

首先,建立组织中存在的角色/行为者列表。尝试考虑可能造成伤害(包括意外)和潜在目标(数据、系统等)的所有行为。您可以从每个类别中组合项目以创建许多场景。以下是一些示例,供您开始:

  • 一名拥有源代码访问权限的工程师对他们的绩效评估不满,并通过在生产中注入恶意后门来报复,窃取用户数据

  • 一名 SRE 拥有网站 SSL 加密密钥的访问权限,被一个陌生人接触,并被强烈鼓励(例如通过对其家人的威胁)交出敏感材料。

  • 一名财务分析师在加班准备公司财务报表时意外修改了最终年度营收数字,使其增加了 1000%。

  • SRE 的孩子在家使用他们父母的笔记本电脑,并安装了一个捆绑了恶意软件的游戏,导致电脑被锁定,阻止了 SRE 对严重故障的响应。

内部风险设计

这本书提供了许多安全设计策略,适用于防范内部风险和恶意的“外部”攻击者。在设计系统时,您必须考虑到有权访问系统或其数据的人可能是本章概述的任何攻击者类型。因此,检测和减轻两种类型风险的策略是相似的。

我们发现在思考内部风险时,有一些概念特别有效:

最小权限

授予执行工作职责所需的最少权限,无论是范围还是访问的持续时间。请参见第五章。

零信任

设计自动化或代理机制来管理系统,以便内部人员不具有广泛访问权限,从而导致他们造成伤害。参见第三章。

多方授权

使用技术控制要求多人授权敏感操作。参见第五章。

业务理由

要求员工正式记录他们访问敏感数据或系统的原因。参见第五章。

审计和检测

审查所有访问日志和理由,确保它们是适当的。参见第十五章。

可恢复性

在发生破坏性行为后恢复系统的能力,比如不满的员工删除关键文件或系统。参见第九章。

攻击者方法

我们描述的威胁行为者是如何进行攻击的?了解这个问题的答案对于理解某人可能如何破坏您的系统以及您如何保护它们至关重要。了解攻击者的操作方式可能感觉像复杂的魔术。试图预测任何特定攻击者在任何给定日子可能做什么是不可行的,因为攻击方法的多样性。我们无法在这里呈现每种可能的方法,但幸运的是,开发人员和系统设计师可以利用越来越大的示例和框架库来解决这个问题。在本节中,我们讨论了几种研究攻击者方法的框架:威胁情报、网络杀伤链和 TTPs。

威胁情报

许多安全公司都会详细描述他们在野外看到的攻击。这种威胁情报可以帮助系统防御者了解真实攻击者每天的工作方式以及如何击退他们。威胁情报有多种形式,每种形式都有不同的用途:

  • 书面报告描述了攻击发生的方式,对于了解攻击者的进展和意图特别有用。这些报告通常是作为实际响应活动的结果生成的,根据研究人员的专业知识可能质量不同。

  • 威胁指标(IOCs)通常是攻击的有限属性,例如攻击者托管网络钓鱼网站的 IP 地址或恶意二进制文件的 SHA256 校验和。IOCs 通常使用通用格式结构化⁸,并通过自动化源获取,以便用于自动配置检测系统。

  • 恶意软件报告提供了对攻击工具能力的洞察,并可以成为 IOCs 的来源。这些报告是由专家使用标准工具(如 IDA Pro 或 Ghidra)对二进制文件进行反向工程生成的。恶意软件研究人员还使用这些研究来根据它们的共同软件属性交叉相关无关的攻击。

从声誉良好的安全公司获取威胁情报,最好是有客户参考的公司,可以帮助您更好地了解攻击者的观察活动,包括影响同行组织的攻击。了解类似您的组织所面临的攻击类型可以提供您可能在未来面临的早期警告。许多威胁情报公司还免费公开发布每年的摘要和趋势报告。⁹

网络杀伤链™

为了应对攻击,一种方法是列出攻击者可能需要采取的所有步骤来实现他们的目标。一些安全研究人员使用形式化的框架,如网络攻击链¹⁰来分析攻击。这些框架可以帮助您绘制攻击的正式进展以及需要考虑的防御控制。表 2-3 显示了假想攻击的阶段相对于一些防御层。

表 2-3.假想攻击的网络攻击链

攻击阶段 攻击示例 示例防御
侦察:监视目标受害者,了解他们的弱点。 攻击者使用搜索引擎查找目标组织员工的电子邮件地址。 教育员工有关在线安全。
入口:获取进入网络、系统或账户的权限,以执行攻击。 攻击者向员工发送钓鱼邮件,导致账户凭证被盗。攻击者然后使用这些凭证登录到组织的虚拟专用网络(VPN)服务。 对 VPN 服务使用双因素身份验证(如安全密钥)。只允许组织管理的系统连接 VPN。
横向移动:在系统或账户之间移动,以获取额外的访问权限。 攻击者使用被盗的凭证远程登录到其他系统。 允许员工只能登录到自己的系统。要求多用户系统登录使用双因素身份验证。
持久性:确保持续访问被损害的资产。 攻击者在新被损害的系统上安装后门,以提供远程访问。 使用应用程序白名单,只允许授权软件运行。
目标:采取攻击目标的行动。 攻击者从网络中窃取文件,并使用远程访问后门将其外泄。 对敏感数据启用最低特权访问,并监控员工账户。

战术、技术和程序

系统地对攻击者的 TTP 进行分类是一种越来越常见的分类攻击方法的方式。最近,MITRE 已经开发了ATT&CK 框架来更彻底地实现这一想法。简而言之,该框架将网络攻击链的每个阶段扩展为详细的步骤,并提供了攻击者如何执行攻击的正式描述。例如,在凭证访问阶段,ATT&CK 描述了用户的.bash_history可能包含攻击者可以通过简单阅读文件获得的意外输入的密码。ATT&CK 框架列出了攻击者可能操作的数百(甚至数千)种方式,以便防御者可以针对每种攻击方法建立防御措施。

风险评估考虑因素

了解潜在对手、他们是谁以及他们可能使用的方法可能是复杂而微妙的。在评估各种攻击者所构成的风险时,我们发现以下考虑因素很重要:

你可能没有意识到自己是一个目标。

你的公司、组织或项目可能是潜在目标并不是立即显而易见的。许多组织,尽管规模较小或不涉及处理敏感信息,也可以被利用来进行攻击。2012 年 9 月,Adobe 公司(以能够让内容创作者使用的软件而闻名)披露攻击者已经侵入其网络,并明确表示要使用公司的官方软件签名证书来数字签名其恶意软件。这使攻击者能够部署看起来合法的恶意软件,以躲避杀毒软件和其他安全防护软件的检测。考虑一下你的组织是否拥有攻击者感兴趣的资产,无论是直接获利还是作为对其他人的更大规模攻击的一部分。

攻击的复杂性并不是成功的真正预测因素。

即使攻击者拥有大量资源和技能,也不要假设他们总是会选择最困难、昂贵或晦涩的手段来实现他们的目标。一般来说,攻击者选择最简单和成本效益最高的方法来破坏系统,以满足他们的目标。例如,一些最显著和有影响力的情报收集行动依赖于基本的钓鱼——诱使用户交出他们的密码。因此,在设计你的系统时,一定要先涵盖安全的基本要点(如使用双因素认证),然后再担心晦涩和奇特的攻击(如固件后门)。

不要低估你的对手。

不要假设对手无法获取资源来进行昂贵或困难的攻击。仔细考虑你的对手愿意花费多少。美国国家安全局在路上拦截装运给客户的思科硬件并在其中植入后门的非凡故事说明了资金充裕和有才能的攻击者为实现目标所付出的努力。然而,请记住,这类案例很大程度上是例外而不是常规。

归因很难。

2016 年 3 月,研究人员发现了一种新型勒索软件——一种恶意程序,使数据或系统无法使用,直到受害者支付赎金,他们将其命名为 Petya。Petya 似乎是出于经济动机。一年后,研究人员发现了一种新的恶意软件,它与原始的 Petya 程序共享许多元素。这个被称为NotPetya的新恶意软件迅速在全球传播,但主要出现在乌克兰的系统上,而且正好是在乌克兰的一个节日前夕。为了传播 NotPetya,攻击者侵入了一家专门为乌克兰市场生产产品的公司,并滥用他们的软件分发机制来感染受害者。一些研究人员认为,这次攻击是由俄罗斯政府支持的行为者进行的,目的是针对乌克兰。

这个例子表明,有动机的攻击者可以以创造性的方式隐藏他们的动机和身份——在这种情况下,伪装成可能更温和的东西。由于攻击者的身份和意图可能并不总是被充分理解,我们建议你专注于攻击者的工作方式(他们的 TTPs),而不是担心他们具体是谁。

攻击者并不总是害怕被抓。

即使你设法追踪到攻击者的位置和身份,刑事系统(尤其是国际上)可能会使他们难以对他们的行为承担法律责任。这对于直接为可能不愿意引渡他们进行刑事起诉的政府工作的国家行为者尤其如此。

结论

所有安全攻击都可以追溯到一个有动机的人。我们已经介绍了一些常见的攻击者档案,以帮助你确定谁可能想要攻击你的服务以及为什么,从而让你能够相应地优先考虑你的防御。

评估谁可能会以你为目标。你有什么资产?谁购买你的产品或服务?你的用户或他们的行为是否会激励攻击者?你的防御资源与潜在对手的进攻资源相比如何?即使面对资金充裕的攻击者,本书的其余信息也可以帮助你成为一个更昂贵的目标,可能消除攻击的经济动机。不要忽视那些更小、不太显眼的对手——匿名性、位置、充裕的时间以及起诉的困难都可以成为攻击者的优势,使他们能够给你造成不成比例的大量损害。考虑你的内部风险,因为所有组织都面临来自内部人员的恶意和非恶意潜在威胁。内部人员被授予的提升访问权限使他们能够造成重大损害。

及时了解安全公司发布的威胁情报。虽然多步攻击方法可能有效,但它也提供了多个接触点,您可以在那里检测和阻止攻击。要注意复杂的攻击策略,但不要忘记简单、不成熟的攻击,比如网络钓鱼,可能会非常有效。不要低估你的对手或你作为目标的价值。

¹ 斯托尔在《ACM 通讯》的一篇文章中记录了这次攻击,“追踪狡猾的黑客”,以及书籍《布谷鸟的蛋:在计算机间谍的迷宫中追踪间谍》(画廊书籍)。这两本书对于任何设计安全可靠系统的人来说都是很好的资源,因为它们的发现今天仍然相关。

² 作为这个领域复杂性的一个例子,在这类冲突中,并非所有攻击者都是有组织的军队的一部分。例如,据报道,荷兰攻击者在波斯湾战争(1991 年)期间入侵了美国军方,并向伊拉克政府提供了窃取的信息。

³ NSO 集团的活动已经被多伦多大学蒙克全球事务与公共政策学院的研究和政策实验室公民实验室研究和记录。例如,请参见https://oreil.ly/IqDN_

⁴ 关于谁创造了这个术语以及它的含义存在一些争论,但在 1996 年之后,它在Hacktivismo采用后被广泛使用,这是与死牛(cDc)邪教有关的一个团体。

⁵ 匿名是许多人在黑客行动(和其他行动)中使用的别名。根据情况,它可能(或可能不)指的是单个人或一群相关的人。

⁶ 提高 CAPTCHA 技术效果的竞赛仍在继续,最新的进展利用用户与 CAPTCHA 交互时的行为分析。reCAPTCHA是您可以在您的网站上使用的免费服务。有关研究文献的相对最新概述,请参见 Chow Yang-Wei,Willy Susilo 和 Pairat Thorncharoensri。2019 年的文章“CAPTCHA 设计和安全问题”。在《网络安全的进展:原理、技术和应用》中,由 Kuan-Ching Li,Xiaofeng Chen 和 Willy Susilo 编辑,69–92。新加坡:斯普林格出版社。

⁷ 有关考虑安全和隐私功能如何受家庭伴侣虐待影响的例子,请参见马修斯(Matthews, Tara)等人 2017 年的文章“幸存者的故事:应对亲密伴侣虐待时的隐私和安全实践。”《2017 年人机交互计算系统 CHI 会议论文集》:2189–2201。https://ai.google/research/pubs/pub46080

⁸ 例如,许多工具正在将结构威胁信息表达(STIX)语言纳入标准化的 IOC 文档,这些文档可以在使用受信任的自动化指标信息交换(TAXII)项目等服务之间交易。

⁹ 值得注意的例子包括每年的Verizon 数据泄露调查报告和 CrowdStrike 的年度全球威胁报告

¹⁰ 由洛克希德·马丁公司构思(并注册商标)的网络杀伤链是传统军事攻击结构的改编。它定义了网络攻击的七个阶段,但我们发现这可以被改编;一些研究人员将其简化为四到五个关键阶段,就像我们在这里所做的那样。

¹¹ 格林沃尔德,格伦。2014 年。《无处可藏:爱德华·斯诺登、NSA 和美国监视国家》。纽约:大都会图书,149 页。

第二部分:设计系统

原文:Part II. Designing Systems

译者:飞龙

协议:CC BY-NC-SA 4.0

第二部分着重于以尽可能早的阶段,在设计系统时实现安全性和可靠性要求的最具成本效益的方式。

尽管产品设计理想上应该从一开始就考虑安全性和可靠性,但你将开发的许多与安全性和可靠性相关的功能可能会被添加到现有产品中。第三章提供了一个例子,说明了我们是如何使谷歌已经运行的系统更安全,更不容易发生故障的。您可以为您的系统添加许多类似的增强功能,并且当配合一些后续的设计原则时,它们将更加有效。

第四章考虑了将安全性和可靠性问题推迟处理以换取持续速度的自然倾向。我们认为功能和非功能要求不一定需要相互对立。

如果你想知道从哪里开始将安全性和可靠性原则融入到你的系统中,第五章——讨论如何根据风险评估访问的优点——是一个很好的起点。第六章然后探讨了如何通过不变量和心智模型来分析和理解你的系统。特别是,该章建议使用建立在标准化框架上的分层系统架构,用于身份、授权和访问控制。

为了应对不断变化的风险格局,您需要能够频繁快速地改变您的基础设施,同时保持高度可靠的服务。第七章介绍了让您适应短期、中期和长期变化,以及在运行服务时可能出现的意外复杂性的做法。

到目前为止提到的指导方针,如果系统无法经受重大故障或中断,将会有限的好处。第八章讨论了在事故期间保持系统运行的策略,也许是以降级模式。第九章从修复系统的角度来看待系统。最后,第十章介绍了一种可靠性和安全性相交的场景,并举例说明了在服务堆栈的每一层中一些成本效益的 DoS 攻击缓解技术。

第三章:案例研究:安全代理

原文:3. Case Study: Safe Proxies

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Jakub Warmuz 和 Ana Oprea

与 Thomas Maufer,Susanne Landers,Roxana Loza,Paul Blankinship 和 Betsy Beyer 一起

生产环境中的安全代理

总的来说,代理提供了一种解决新的可靠性和安全性要求的方法,而无需对已部署的系统进行重大更改。您可以简单地使用代理来路由本来直接到达系统的连接。代理还可以包括控件,以满足您的新安全性和可靠性要求。在本案例研究中,我们将研究谷歌使用的一组安全代理,以限制特权管理员在生产环境中意外或恶意引起问题的能力。

安全代理是一个框架,允许授权人员访问或修改物理服务器、虚拟机或特定应用程序的状态。在谷歌,我们使用安全代理来审查、批准和运行风险命令,而无需与系统建立 SSH 连接。使用这些代理,我们可以授予细粒度的访问权限以调试问题,或者可以限制机器重新启动的速率。安全代理代表网络之间的单个入口点,并且是使我们能够执行以下操作的关键工具:

  • 审计舰队中的每个操作

  • 控制对资源的访问

  • 保护生产环境免受人为错误的影响

Zero Touch Prod是谷歌的一个项目,要求所有生产中的更改都由自动化(而不是人类)进行,经过软件预验证,或者通过经过审计的紧急机制触发。¹ 安全代理是我们用来实现这些原则的一组工具。我们估计,谷歌评估的所有故障中约 13%可以通过 Zero Touch Prod 预防或减轻。

在安全代理模型中,显示在图 3-1 中,客户端与目标系统直接交流,而是与代理交流。在谷歌,我们通过限制目标系统仅接受代理的调用来强制执行此行为。此配置指定了通过访问控制列表(ACL)可以由哪些客户端角色执行哪些应用层远程过程调用(RPC)。检查访问权限后,代理将请求发送到目标系统以通过 RPC 执行。通常,每个目标系统都有一个应用层程序,该程序接收请求并直接在系统上执行。代理记录与其交互的所有请求和命令。

我们发现使用代理来管理系统有多个好处,无论客户是人类、自动化还是两者兼而有之。代理提供以下功能:

  • 强制执行多方授权(MPA)的中心点,²我们为与敏感数据交互的请求做出访问决策

  • 管理使用审计,我们可以跟踪特定请求的执行时间和执行者

  • 速率限制,例如系统重新启动逐渐生效,并且我们可能限制错误的影响范围

  • 与闭源第三方目标系统兼容,我们通过代理的附加功能控制组件的行为(我们无法修改)

  • 持续改进集成,我们在中央代理点添加安全性和可靠性增强功能

安全代理模型

图 3-1:安全代理模型

代理也有一些缺点和潜在的陷阱:

  • 成本增加,包括维护和运营开销。

  • 单点故障,如果系统本身或其依赖项之一不可用。我们通过运行多个实例来减轻这种情况,以增加冗余性。我们确保我们系统的所有依赖项都具有可接受的服务级别协议(SLA),并且每个依赖项的运营团队都有记录的紧急联系人。

  • 访问控制的策略配置本身可能是错误的来源。我们通过提供模板或自动生成默认安全设置来引导用户做出正确的选择。在创建这样的模板或自动化时,我们遵循第 II 部分中提出的设计策略。

  • 对手可能控制的中央机器。上述策略配置要求系统转发客户端的身份并代表客户端执行任何操作。代理本身并不赋予高权限,因为没有请求是在代理角色下执行的。

  • 对变化的抵抗,因为用户可能希望直接连接到生产系统。为了减少代理施加的摩擦,我们与工程师密切合作,确保他们在紧急情况下可以通过紧急机制访问系统。我们将在第二十一章中更详细地讨论这些话题。

由于安全代理的主要用例是添加与访问控制相关的安全性和可靠性功能,因此代理公开的接口应使用与目标系统相同的外部 API。因此,代理不会影响整体用户体验。假设安全代理是透明的,它可以在执行一些预处理和后处理进行验证和日志记录后简单地转发流量。下一节将讨论我们在谷歌使用的安全代理的一个具体实例。

谷歌工具代理

谷歌员工使用命令行界面(CLI)工具执行大部分管理操作。其中一些工具可能很危险,例如,某些工具可以关闭服务器。如果这样的工具指定了不正确的范围选择器,命令行调用可能会意外地停止几个服务前端,导致中断。跟踪每个 CLI 工具,确保它执行集中式日志记录,并确保敏感操作有进一步的保护将是困难且昂贵的。为了解决这个问题,谷歌创建了一个工具代理:一个公开通用 RPC 方法的二进制文件,通过 fork 和 exec 内部执行指定的命令行。所有调用都受到策略的控制,用于审计的日志记录,并具有要求 MPA 的能力。

使用工具代理实现了零接触生产的主要目标之一:通过不允许人员直接访问生产使生产更安全。工程师无法直接在服务器上运行任意命令;他们需要通过工具代理进行联系。

我们通过使用一组细粒度的策略来配置谁被允许执行哪些操作,这些策略执行 RPC 方法的授权。示例 3-1 中的策略允许group:admin的成员在group:admin-leads中的某人批准命令后运行borg CLI 的最新版本以及任何参数。工具代理实例通常作为 Borg 作业部署。

示例 3-1. 谷歌工具代理 Borg 策略
config = {
  proxy_role = 'admin-proxy'
  tools = {
    borg = {
      mpm = 'client@live'
      binary_in_mpm = 'borg'
      any_command = true
      allow = ['group:admin']
      require_mpa_approval_from = ['group:admin-leads']
      unit_tests = [{
        expected = 'ALLOW'
        command = 'file.borgcfg up'
      }]
    }
  }
}

示例 3-1 中的策略允许工程师通过类似以下命令来从他们的工作站停止生产中的 Borg 作业:

$ tool-proxy-cli --proxy_address admin-proxy borg kill ...

该命令向指定地址的代理发送 RPC,这将启动以下事件链,如图 3-2 所示:

  1. 代理记录所有 RPC 和执行的检查,提供了审计先前运行的管理操作的简单方法。

  2. 代理检查策略,以确保调用者在group:admin中。

  3. 由于这是一个敏感命令,MPA 被触发,代理等待group:admin-leads中的人员授权。

  4. 如果获得批准,代理执行命令,等待结果,并将返回代码、stdout 和 stderr 附加到 RPC 响应。

工具代理使用工作流程

图 3-2:工具代理使用工作流程

工具代理需要对开发工作流程进行小的更改:工程师需要在他们的命令前加上 tool-proxy-cli --proxy_address。为了确保特权用户不会规避代理,我们修改了服务器,只允许对 admin-proxy 进行管理操作,并在非紧急情况下拒绝任何直接连接。

结论

使用安全代理是向系统添加日志记录和多方授权的一种方式。代理可以帮助使您的系统更安全、更可靠。这种方法可以是现有系统的一种经济有效的选择,但如果与第二部分中描述的其他设计原则配对使用,将会更加强大。正如我们在第四章中讨论的那样,如果您正在启动一个新项目,最好使用与日志记录和访问控制模块集成的框架来构建系统架构。

¹ 紧急情况机制是一种可以绕过策略以允许工程师快速解决故障的机制。参见“紧急情况”

² 多方授权要求在允许操作发生之前,需要额外的用户批准。参见“多方授权(MPA)”

第四章:设计权衡

原文:4. Design Tradeoffs

译者:飞龙

协议:CC BY-NC-SA 4.0

由 Christoph Kern

与 Brian Gustafson,Paul Blankinship 和 Felix Gröbert 合作

所以你要构建一个(软件)产品!在这个复杂的旅程中,你会有很多事情要考虑,从制定高层计划到部署代码。

通常,你会对产品或服务要做什么有一个大致的想法。例如,这可能是一个游戏的高层概念,或者是基于云的生产力应用的高层业务需求。你还会制定关于服务提供方式的高层计划。

当你深入设计过程,对产品形状的想法变得更加具体时,对应用程序的设计和实现会出现额外的需求和约束。产品的功能性将有特定的需求,还有一般的约束,比如开发和运营成本。你还会遇到安全性和可靠性的需求和约束:你的服务可能会有一定的可用性和可靠性需求,你可能会有保护应用程序处理的敏感用户数据的安全性需求。

其中一些需求和约束可能会相互冲突,你需要做出权衡,并找到它们之间的平衡。

设计目标和需求

产品的功能需求往往具有与安全性和可靠性需求显着不同的特征。让我们更仔细地看看在设计产品时你将面临的需求类型。

功能需求

功能需求,也称为功能性需求,¹标识了服务或应用程序的主要功能,并描述了用户如何完成特定任务或满足特定需求。它们通常以用例用户故事用户旅程的形式表达——用户与服务或应用程序之间的交互序列。关键需求是功能需求的子集,对产品或服务至关重要。如果设计不满足关键需求或关键用户故事,那么你就没有一个可行的产品。

功能需求通常是你设计决策的主要驱动因素。毕竟,你正在尝试构建一个满足特定一组用户需求的系统或服务。你经常需要在各种需求之间做出权衡决策。考虑到这一点,区分关键需求和其他功能需求是有用的。

通常,许多需求适用于整个应用程序或服务。这些需求通常不会出现在用户故事或个别功能需求中。相反,它们在集中的需求文档中一次性陈述,甚至是隐含地假定。这里有一个例子:

应用程序的 Web UI 的所有视图/页面必须:

  • 遵循常见的视觉设计指南

  • 遵守无障碍指南

  • 在页脚上加上链接,链接到隐私政策和服务条款(Terms of Service)

非功能性需求

几类需求关注的是系统的一般属性或行为,而不是特定的行为。这些非功能性需求与我们的关注——安全性和可靠性相关。例如:

  • 在哪些独特的情况下,某人(外部用户、客户支持代理或运维工程师)可以访问某些数据?

  • 对于诸如正常运行时间或 95th 百分位和 99th 百分位响应延迟等指标的服务水平目标(SLOs)是什么?系统在超过某个阈值的负载下如何响应?

在平衡要求时,同时考虑系统本身以外领域的要求可能会有所帮助,因为这些领域的选择可能会对核心系统要求产生重大影响。这些更广泛的领域包括以下内容:

开发效率和速度

考虑到所选择的实现语言、应用程序框架、测试流程和构建流程,开发人员能够多有效地迭代新功能吗?开发人员能够多有效地理解、修改或调试现有代码吗?

部署速度

从功能开发到该功能对用户/客户可用需要多长时间?

功能与紧急属性

功能要求通常在要求、满足这些要求的代码和验证实现的测试之间有着相当直接的联系。例如:

规范

用户故事或需求可能规定了应用程序的已登录用户如何查看和修改与其用户配置文件相关的个人数据(例如姓名和联系信息)。

实现

基于此规范的 Web 或移动应用程序通常会有与该要求非常相关的代码,例如以下内容:

  • 结构化类型来表示配置文件数据

  • UI 代码来呈现和允许修改配置文件数据

  • 服务器端 RPC 或 HTTP 操作处理程序从数据存储中查询已登录用户的配置文件数据,并接受要写入数据存储的更新信息

验证

通常会有一个集成测试,基本上逐步执行指定的用户故事。测试可能使用 UI 测试驱动程序填写并提交“编辑配置文件”表单,然后验证提交的数据是否出现在预期的数据库记录中。该用户故事的各个步骤可能还有单元测试。

相比之下,像可靠性和安全性要求这样的非功能性要求往往更难以确定。如果您的 Web 服务器有一个--enable_high_reliability_mode标志,而要使您的应用程序可靠,您只需要翻转该标志并支付您的托管或云提供商高级服务费就可以了。但是没有这样的标志,也没有任何应用程序源代码中“实现”可靠性的特定模块或组件。

例如:谷歌设计文档

谷歌使用设计文档模板来指导新功能设计,并在启动工程项目之前收集利益相关者的反馈意见。

与可靠性和安全性考虑相关的模板部分提醒团队考虑其项目的影响,并在适当的情况下启动生产准备或安全审查流程。设计审查有时会在工程师正式开始考虑启动阶段之前的多个季度进行。

平衡要求

由于满足安全性和可靠性要求的系统属性很大程度上是紧急属性,它们往往既与功能要求的实现相互作用,又与彼此相互作用。因此,很难单独推理涉及安全性和可靠性的权衡。

本节提供了一个示例,说明了您可能需要考虑的权衡。这个示例的一些部分深入到技术细节,这些细节本身并不一定重要。设计支付处理系统及其运作所涉及的所有合规性、监管、法律和商业考虑对于这个示例也不重要。相反,目的是说明要求之间复杂相互依赖的思维过程。换句话说,重点不是保护信用卡号的细枝末节,而是设计具有复杂安全性和可靠性要求的系统所需的思维过程。

示例:支付处理

想象一下,你正在构建一个在线服务,向消费者销售小部件。该服务的规范包括一个用户故事,规定用户可以通过使用移动或网络应用程序从在线目录中选择小部件。然后用户可以购买所选的小部件,这需要他们提供付款方式的详细信息。

安全性和可靠性考虑

接受付款信息会为系统的设计和组织流程引入重大的安全性和可靠性考虑。姓名、地址和信用卡号码是需要特殊保护的敏感个人数据,并且根据适用的司法管辖区,可能会使您的系统受到监管标准的约束。接受付款信息还可能使服务受到符合行业或监管安全标准(如 PCI DSS)的合规范围。

对这些敏感用户信息的妥协,尤其是个人可识别信息(PII),可能会对项目甚至整个组织/公司产生严重后果。您可能会失去用户和客户的信任,并因此失去他们的业务。近年来,立法机构已颁布了法律和法规,对受数据泄露影响的公司施加了可能耗时和昂贵的义务。一些公司甚至因严重的安全事件而完全倒闭,如第一章所述。

在某些情况下,产品设计层面的更高级别权衡可能会使应用程序摆脱处理支付的需求,例如,也许产品可以以基于广告或社区资助的模式重塑。对于我们的例子,我们将坚持接受付款是一个关键要求的前提。

使用第三方服务提供商处理敏感数据

通常,减轻对敏感数据的安全担忧的最佳方法是根本不保存这些数据(有关此主题的更多信息,请参见第五章)。您可以安排敏感数据永远不会通过您的系统,或者至少设计系统不会持久存储数据。您可以选择各种商业支付服务 API 与应用程序集成,并将支付信息、支付交易和相关问题(如欺诈对策)的处理外包给供应商。

好处

根据情况,使用支付服务可能会降低风险,并减少您需要在这一领域建立内部专业知识的程度,而是依赖供应商的专业知识:

  • 您的系统不再保存敏感数据,减少了系统或流程中的漏洞可能导致数据泄露的风险。当然,第三方供应商的妥协仍可能危及您的用户数据。

  • 根据具体情况和适用要求,您在支付行业安全标准下的合同和合规义务可能会简化。

  • 您无需构建和维护基础设施来保护系统数据存储中的数据。这可能消除了大量的开发和持续运营工作。

  • 许多第三方支付提供商提供对抗欺诈交易和支付风险评估服务。您可以使用这些功能来减少支付欺诈风险,而无需自行构建和维护基础设施。

另一方面,依赖第三方服务提供商会带来自己的成本和风险。

成本和非技术风险

显然,供应商将收取费用。交易量可能会影响你的选择 - 超过一定量后,可能更划算在内部处理交易。

你还需要考虑依赖第三方的工程成本:你的团队将不得不学习如何使用供应商的 API,并且你可能需要按照供应商的时间表跟踪 API 的更改/发布。

可靠性风险

通过外包支付处理,你为你的应用程序增加了一个额外的依赖关系 - 在这种情况下,是第三方服务。额外的依赖通常会引入额外的故障模式。在第三方依赖的情况下,这些故障模式可能部分地超出你的控制。例如,如果支付提供商的服务停机或通过网络无法访问,你的用户故事“用户可以购买他们选择的小部件”可能会失败。这种风险的重要性取决于你与该提供商的SLAs的遵守程度。

你可以通过在系统中引入冗余来解决这个风险(参见第八章) - 在这种情况下,通过添加备用支付提供商,你的服务可以切换到备用支付提供商。这种冗余引入了成本和复杂性 - 两个支付提供商很可能有不同的 API,因此你必须设计你的系统能够与两者通信,以及所有额外的工程和运营成本,以及增加对错误或安全妥协的暴露。

你也可以通过你自己的后备机制来减轻可靠性风险。例如,如果支付服务不可达,你可以在通信渠道与支付提供商中插入一个队列机制来缓冲交易数据。这样做将允许“购买流程”用户故事在支付服务中断期间继续进行。

然而,添加消息队列机制会引入额外的复杂性,并可能引入自己的故障模式。如果消息队列没有设计成可靠的(例如,它只在易失性内存中存储数据),你可能会丢失交易 - 这是一个新的风险。更一般地说,只在罕见和特殊情况下使用的子系统可能隐藏着隐藏的错误和可靠性问题。

你可以选择使用更可靠的消息队列实现。这可能涉及分布在多个物理位置的内存存储系统,再次引入复杂性,或者存储在持久性磁盘上。即使只在特殊情况下将数据存储在磁盘上,也会重新引入关于存储敏感数据(妥协风险、合规考虑等)的担忧,这正是你一开始想要避免的。特别是,一些支付数据甚至不允许命中磁盘,这使得依赖持久存储的重试队列难以应用在这种情况下。

在这种情况下,你可能需要考虑攻击(特别是内部人员的攻击),他们有意打破与支付提供商的联系,以激活交易数据的本地排队,然后可能被 compromise。

总之,你最终会遇到一个安全风险,这是由于你试图减轻可靠性风险而产生的,而这又是因为你试图减轻安全风险!

安全风险

依赖第三方服务的设计选择也立即引起了安全考虑。

首先,你正在把敏感的客户数据交给第三方供应商。你需要选择一个安全立场至少与你自己相等的供应商,并且必须在选择和持续评估供应商时进行仔细评估。这不是一项容易的任务,还有复杂的合同、监管和责任考虑因素,这些都超出了本书的范围,应该咨询你的法律顾问。

其次,与供应商服务集成可能需要您将供应商提供的库链接到您的应用程序中。这会带来一个风险,即该库中的漏洞,或者其传递依赖关系中的一个漏洞,可能导致系统中的漏洞。您可以考虑通过对该库进行沙盒化⁵并准备快速部署更新版本来减轻这种风险(参见第七章)。您可以通过使用不需要您将专有库链接到您的服务中的供应商(参见第六章)来基本避免这种担忧。如果供应商使用像 REST+JSON、XML、SOAP 或 gRPC 这样的开放协议来公开其 API,那么可以避免使用专有库。

您可能需要在您的 Web 应用程序客户端中包含一个 JavaScript 库,以便与供应商集成。这样做可以避免通过您的系统传递付款数据,即使是暂时的——相反,付款数据可以直接从用户的浏览器发送到提供商的 Web 服务。然而,这种集成引发了与包含服务器端库类似的担忧:供应商的库代码在您应用程序的 Web 来源中以完全权限运行。⁶该代码的漏洞或者提供该库的服务器的妥协可能导致您的应用程序受到威胁。您可以考虑通过在单独的 Web 来源或沙箱 iframe 中对与付款相关的功能进行沙盒化来减轻这种风险。然而,这种策略意味着您需要一个安全的跨来源通信机制,这再次引入了复杂性和额外的故障模式。另外,付款供应商可能提供基于 HTTP 重定向的集成,但这可能导致用户体验不够流畅。

与非功能性需求相关的设计选择可能在领域特定技术专业知识领域产生相当深远的影响:我们开始讨论与处理付款数据相关的风险缓解相关的权衡,最终考虑到了深入到 Web 平台安全领域的考虑。在这个过程中,我们还遇到了合同和监管方面的问题。

管理紧张关系和调整目标

通过一些前期规划,您通常可以满足重要的非功能性需求,如安全性和可靠性,而无需放弃功能,并且成本合理。当回顾整个系统和开发运营工作流程的背景来考虑安全性和可靠性时,往往会发现这些目标与一般软件质量属性非常一致。

示例:微服务和谷歌 Web 应用程序框架

考虑谷歌内部微服务和 Web 应用程序框架的演变。创建该框架的团队的主要目标是简化大型组织的应用程序和服务的开发和运营。在设计这个框架时,团队融入了一个关键的想法,即应用静态和动态的符合性检查,以确保应用代码符合各种编码准则和最佳实践。例如,符合性检查验证在并发执行上下文之间传递的所有值都是不可变类型——这种做法极大地降低了并发错误的可能性。另一组符合性检查强制执行组件之间的隔离约束,这样就不太可能导致应用程序中一个组件的更改导致另一个组件中的错误。

因为基于这个框架构建的应用程序具有相当严格和明确定义的结构,所以该框架可以为许多常见的开发和部署任务提供开箱即用的自动化功能——从新组件的脚手架搭建,到持续集成(CI)环境的自动设置,再到大部分自动化的生产部署。这些优势使得这个框架在谷歌开发人员中非常受欢迎。

所有这些与安全性和可靠性有什么关系?该框架开发团队在设计和实现阶段与 SRE 和安全团队合作,确保安全性和可靠性最佳实践被编织到框架的结构中,而不是在最后才添加。该框架负责处理许多常见的安全性和可靠性问题。同样,它还自动设置了操作指标的监控,并整合了可靠性功能,如健康检查和 SLA 合规性。

例如,该框架的 Web 应用程序支持处理了大多数常见类型的 Web 应用程序漏洞。通过 API 设计和代码符合性检查的结合,它有效地防止开发人员在应用程序代码中意外引入许多常见类型的漏洞。就这些类型的漏洞而言,该框架不仅仅是“默认安全性”,而是全面负责安全,并积极确保基于它的任何应用程序不受这些风险的影响。我们将在第六章和第十二章中更详细地讨论这是如何实现的。

对新出现的属性要求进行对齐

该框架示例说明,与常见看法相反,与其他产品目标(尤其是代码和项目健康、可维护性和长期持续的项目速度)相关的安全性和可靠性目标通常是很好对齐的。相比之下,试图作为后期附加的方式来追加安全性和可靠性目标通常会导致增加风险和成本。

安全性和可靠性的优先级也可以与其他领域的优先级对齐:

  • 正如在第六章中讨论的那样,使人们能够有效和准确地推理系统的不变量和行为对于安全性和可靠性至关重要。可理解性也是代码和项目健康属性的关键,也是开发速度的关键支持:一个可理解的系统更容易调试和修改(而不会在一开始引入错误)。

  • 设计用于恢复(见第九章)使我们能够量化和控制由变更和部署引入的风险。通常,这里讨论的设计原则支持更高的变更速度(即部署速度),这是我们以其他方式无法实现的。

  • 安全性和可靠性要求我们设计一个不断变化的环境(见第七章)。这样做使我们的系统设计更具适应性,不仅能迅速应对新出现的漏洞和攻击场景,还能更快地适应不断变化的业务需求。

初始速度与持续速度

特别是在较小的团队中,有一种自然倾向,即将安全性和可靠性问题推迟到将来的某个时间点(“等我们有了一些客户后,我们会加入安全性并担心扩展问题”)。团队通常会以“速度”为借口,忽视安全性和可靠性作为早期和主要的设计驱动因素,他们担心花时间思考和解决这些问题会减慢开发速度,并在首次发布周期中引入不可接受的延迟。

重要的是要区分初始速度和持续速度。选择不考虑安全、可靠性和可维护性等关键要求可能确实会增加项目在项目生命周期早期的速度。然而,经验表明,这样做通常也会在项目后期显著减速。在项目周期后期进行大规模修改以满足作为新兴属性出现的要求的成本可能非常高。此外,为了解决安全和可靠性风险而进行侵入性的后期更改本身可能会引入更多的安全和可靠性风险。因此,早期将安全和可靠性融入团队文化非常重要。

互联网的早期历史和 IP、TCP、DNS 和 BGP 等基础协议的设计和演变,为这个话题提供了有趣的视角。可靠性——特别是在节点故障的情况下网络的生存能力以及在容易出现故障的链路上通信的可靠性——是早期互联网的早期前身,如 ARPANET 的明确和高优先级的设计目标。

然而,在早期的互联网论文和文档中并没有多少提到安全。早期的网络基本上是封闭的,由受信任的研究和政府机构操作节点。但在今天的开放互联网中,这种假设根本不成立——许多类型的恶意行为者参与了网络。

互联网的基础协议——IP、UDP 和 TCP——没有规定对传输发起者进行身份验证,也没有检测网络中间节点对数据的故意恶意修改。许多更高级的协议,如 HTTP 或 DNS,天生就容易受到网络中恶意参与者的各种攻击。随着时间的推移,已经开发了安全协议或协议扩展来抵御此类攻击。例如,HTTPS 通过在经过身份验证的安全通道上传输数据来增强 HTTP。在 IP 层,IPsec 通过加密对网络级对等体进行身份验证,并提供数据完整性和保密性。IPsec 可用于在不受信任的 IP 网络上建立 VPN。

然而,广泛部署这些安全协议已被证明相当困难。我们现在大约已经进入互联网历史的第 50 年,互联网的重要商业用途可能始于 25 或 30 年前,但仍有相当大比例的网络流量不使用 HTTPS。

另一个关于初始和持续速度之间权衡的例子(在这种情况下来自安全和可靠性领域之外),请考虑敏捷开发流程。敏捷开发工作流的主要目标是增加开发和部署速度,特别是减少功能规范和部署之间的延迟。然而,敏捷工作流通常依赖于相当成熟的单元和集成测试实践以及稳固的持续集成基础设施,这需要前期投资来建立,以换取长期的速度和稳定性。

更一般地,您可以选择将初始项目速度置于一切之上——您可以开发 Web 应用的第一个迭代而不进行测试,并且发布过程相当于将 tarballs 复制到生产主机。您可能会相对快速地完成第一个演示,但到第三个发布时,您的项目很可能会拖后腿,并且负担着技术债务。

我们已经提到了可靠性和速度之间的一致性:投资于成熟的持续集成/持续部署(CI/CD)工作流和基础设施支持频繁的生产发布,同时管理和接受可靠性风险(参见第七章)。但是设置这样的工作流需要一些前期投资——例如,你将需要以下内容:

  • 足够健壮的单元和集成测试覆盖率,以确保生产发布的缺陷风险可接受,而不需要进行主要的人工发布资格工作

  • 本身可靠的 CI/CD 流水线

  • 经常使用的、可靠的基础设施,用于分阶段的生产发布和回滚

  • 允许代码和配置的解耦部署的软件架构(例如,“功能标志”)

这种投资在产品生命周期的早期进行时通常是适度的,而且只需要开发人员持续付出一些增量努力来保持良好的测试覆盖率和持续构建的“绿色”。相比之下,具有较差的测试自动化、依赖于部署中的手动步骤和长周期发布的开发工作流往往会在项目变得复杂时最终拖慢项目的速度。在那时,为成熟系统添加测试和发布自动化往往需要一次性进行大量工作,可能会进一步减慢项目的速度。此外,为成熟系统添加的测试有时会陷入陷阱,更多地测试当前存在的错误行为而不是正确的预期行为。

这些投资对各种规模的项目都是有益的。然而,较大的组织可以享受更多规模效益,因为你可以将成本分摊到许多项目中——一个单独项目的投资最终归结为承诺使用集中维护的框架和工作流程。

在做出专注于安全的设计选择以促进持续速度时,我们建议选择一个框架和工作流,提供针对相关漏洞类别的构造安全防御。这种选择可以大大减少甚至消除在应用程序代码库的持续开发和维护过程中引入此类漏洞的风险(参见第6 章12 章)。这种承诺通常不需要重大的前期投资——相反,它需要持续的、通常是适度的努力来遵守框架的约束。作为回报,你大大降低了系统意外停机或安全响应火灾演习对部署计划造成混乱的风险。此外,你的发布时安全性和生产准备审查更有可能顺利进行。

结论

设计和构建安全可靠的系统并不容易,特别是因为安全性和可靠性主要是整个开发和运营工作流的新兴属性。这项工作涉及思考许多相当复杂的主题,其中许多起初似乎与解决服务的主要功能要求并不那么相关。

你的设计过程将涉及安全性、可靠性和功能要求之间的许多权衡。在许多情况下,这些权衡起初似乎是直接冲突的。在项目的早期阶段避开这些问题可能会很诱人,然后“以后再处理”——但这样做往往会对项目造成重大的成本和风险:一旦你的服务上线,可靠性和安全性就不是可选的。如果你的服务中断,你可能会失去业务;如果你的服务受到损害,应对将需要全员出动。但通过良好的规划和谨慎的设计,通常可以满足这三个方面。更重要的是,你可以在较少的额外前期成本和通常减少系统寿命内的总工程工作量的情况下做到这一点。

¹ For a more formal treatment, see The MITRE Systems Engineering Guide and ISO/IEC/IEEE 29148-2018(E).

² For the purposes of the example, it’s not relevant what exactly is being sold—a media outlet might require payments for articles, a mobility company might require payments for transportation, an online marketplace might enable the purchase of physical goods that are shipped to consumers, or a food-ordering service might facilitate the delivery of takeout orders from local restaurants.

³ See, for example, McCallister, Erika, Tim Grance, and Karen Scarfone. 2010. NIST Special Publication 800-122, “Guide to Protecting the Confidentiality of Personally Identifiable Information (PII).” https://oreil.ly/T9G4D.

⁴ Note that whether or not this is appropriate may depend on regulatory frameworks your organization is subject to; these regulatory matters are outside the scope of this book.

⁵ See, e.g., the Sandboxed API project.

⁶ For more on this subject, see Zalewski, Michał. 2011. The Tangled Web: A Guide to Securing Modern Web Applications. San Francisco, CA: No Starch Press.

⁷ See, e.g., the OWASP Top 10 and CWE/SANS TOP 25 Most Dangerous Software Errors.

⁸ See Kern, Christoph. 2014. “Securing the Tangled Web.” Communications of the ACM 57(9): 38–47. doi:10.1145/2643134.

⁹ At Google, software is typically built from the HEAD of a common repository, which causes all dependencies to be updated automatically with every build. See Potvin, Rachel, and Josh Levenberg. 2016. “Why Google Stores Billions of Lines of Code in a Single Repository.” Communications of the ACM 59(7): 78–87. https://oreil.ly/jXTZM.

¹⁰ See the discussion of tactical programming versus strategic programming in Ousterhout, John. 2018. A Philosophy of Software Design. Palo Alto, CA: Yaknyam Press. Martin Fowler makes similar observations.

¹¹ See RFC 2235 and Leiner, Barry M. et al. 2009. “A Brief History of the Internet.” ACM SIGCOMM Computer Communication Review 39(5): 22–31. doi:10.1145/1629607.1629613.

¹² Baran, Paul. 1964. “On Distributed Communications Networks.” IEEE Transactions on Communications Systems 12(1): 1–9. doi:10.1109/TCOM.1964.1088883.

¹³ Roberts, Lawrence G., and Barry D. Wessler. 1970. “Computer Network Development to Achieve Resource Sharing.” Proceedings of the 1970 Spring Joint Computing Conference: 543–549. doi:10.1145/1476936.1477020.

¹⁴ 费尔特,阿德里安·波特,理查德·巴恩斯,艾普里尔·金,克里斯·帕尔默,克里斯·本策尔和帕里萨·塔布里兹。2017 年。 “测量网络上的 HTTPS 采用情况。” 第 26 届 USENIX 安全研讨会论文集:1323–1338。https://oreil.ly/G1A9q

第五章:面向最小特权的设计

原文:5. Design for Least Privilege

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Oliver Barrett,Aaron Joyner 和 Rory Ward‎

与 Guy Fischman 和 Betsy Beyer 合著

公司通常希望假设他们的工程师有最好的意图,并依赖他们无缺地执行艰巨的任务。这不是一个合理的期望。作为一种思考练习,想想如果你做一些邪恶的事情,你可以对你的组织造成什么样的伤害。你能做什么?你会怎么做?你会被发现吗?你能掩盖你的踪迹吗?或者,即使你的意图是好的,你(或具有等效访问权限的人)可能犯下的最严重错误是什么?在调试、应对故障或执行紧急响应时,你或你的同事使用的多少临时手动命令离造成或加剧故障只有一个打字错误或复制粘贴失败的距离?

因为我们不能依赖人类的完美,我们必须假设任何可能的坏行为或结果都可能发生。因此,我们建议设计系统以最小化或消除这些坏行为的影响。

即使我们通常信任访问我们系统的人类,我们也需要限制他们的特权和我们对他们凭证的信任。事情可能会出错。人们会犯错,误操作命令,被攻击,上当受骗。期望完美是不现实的假设。换句话说,引用 SRE 的格言——希望不是一种策略。

概念和术语

在我们深入探讨设计和操作访问控制系统的最佳实践之前,让我们为行业和谷歌使用的一些特定术语建立工作定义。

最小特权

最小特权是安全行业中已经确立的一个广泛概念。本章的高级最佳实践可以为系统奠定为任何特定任务或行动路径授予最小特权的基础。这一目标适用于分布式系统包括的人类、自动化任务和个别机器。最小特权的目标应该贯穿系统的所有身份验证和授权层。特别是,我们推荐的方法拒绝将隐含的权限扩展给工具(如“工作示例:配置分发”中所示),并努力确保用户尽可能不具有环境权限——例如,以 root 用户登录的能力。

零信任网络

我们讨论的设计原则始于零信任网络——即用户的网络位置(在公司网络内)不授予任何特权访问的概念。例如,在会议室的网络端口插入并不比在互联网其他地方连接获得更多访问权限。相反,系统基于用户凭证和设备凭证的组合来授予访问权限——我们对用户和设备的了解。谷歌通过其BeyondCorp 计划成功实现了大规模的零信任网络模型。

零接触

谷歌的 SRE 组织正在努力通过自动化来建立在最小特权概念之上,目标是实现我们所称的零接触接口。这些接口的具体目标——比如第三章中描述的零接触生产(ZTP)和零信任网络(ZTN)——是通过工具和自动化使谷歌更安全,减少故障,从而消除人类对生产角色的直接访问。这种方法需要大量的自动化、新的安全 API 和弹性的多方批准系统。

基于风险分类访问

任何风险降低策略都伴随着权衡。减少人为因素引入的风险可能需要额外的控制或工程工作,并可能会对生产力产生权衡;它可能会增加工程时间,流程变更,运营工作或机会成本。您可以通过明确定义和优先考虑您想要保护的内容来限制这些成本。

并非所有数据或操作都是平等的,您的访问组成可能会根据系统的性质而有很大不同。因此,您不应该以相同程度保护所有访问。为了应用最合适的控制措施并避免全有或全无的心态,您需要根据影响,安全风险和/或重要性对访问进行分类。例如,您可能需要以不同方式处理对不同类型数据(公开可用数据与公司数据与用户数据与加密密钥)的访问。同样,您可能需要以不同方式处理可以删除数据的管理 API 与特定服务的读取 API。

您的分类应该清晰定义,一贯应用,并广泛理解,以便人们可以设计“讲”这种语言的系统和服务。您的分类框架将根据系统的大小和复杂性而变化:您可能只需要依赖临时标记的两种或三种类型,或者您可能需要一个强大和程序化的系统来对系统的部分(API 分组,数据类型)进行分类。这些分类可能适用于用户在工作过程中可能访问的数据存储,API,服务或其他实体。确保您的框架可以处理系统中最重要的实体。

一旦您建立了分类的基础,您应该考虑每个分类中的控制措施。您需要考虑几个方面:

  • 谁应该有访问权限?

  • 该访问应该受到多大程度的控制?

  • 用户需要什么类型的访问(读/写)?

  • 基础设施控制措施是什么?

例如,如表 5-1 所示,一家公司可能需要三种分类:公开敏感高度敏感。该公司可能会根据访问如果被不当授予可能造成的损害程度,将安全控制分类为低风险中风险高风险

表 5-1。基于风险的访问分类示例

描述 读取访问 写入访问 基础设施访问ᵃ
公开 对公司内任何人开放 低风险 低风险 高风险
敏感 仅限于具有业务目的的群体 中/高风险 中风险 高风险
高度敏感 无永久访问 高风险 高风险 高风险
ᵃ 绕过正常访问控制的管理能力。例如,减少日志级别,更改加密要求,获得对机器的直接 SSH 访问,重新启动和重新配置服务选项,或以其他方式影响服务的可用性。

您的目标应该是构建一个访问框架,从中您可以应用适当的控制,以实现生产力,安全性和可靠性的正确平衡。最小特权应该适用于所有数据访问和操作。基于这个基础框架,让我们讨论如何设计具有最小特权原则和控制的系统。

最佳实践

在实现最小特权模型时,我们建议遵循这里详细介绍的几项最佳实践。

小型功能 API

使每个程序都做一件事情。要做一项新工作,最好重新构建,而不是通过添加新的“功能”来使旧程序变得复杂。

——McIlroy, Pinson, and Tague (1978)¹

正如这句引语所传达的,Unix 文化的核心是围绕着可以组合的小而简单的工具。因为现代分布式计算是从 20 世纪 70 年代的单一时间共享计算系统发展而来的,演变成了全球范围内连接的分布式系统,所以作者的建议在 40 多年后仍然是正确的。为了适应当前的计算环境,人们可能会说,“让每个 API 端点都做好一件事。”在构建系统时,要考虑安全性和可靠性,避免开放式的交互式接口,而是设计小型的功能性 API。这种方法使您能够应用最小权限的经典安全原则,并授予执行特定功能所需的最低权限。

我们所说的API究竟是什么意思?每个系统都有一个 API:它只是系统呈现的用户界面。有些 API 非常大(比如 POSIX API 或 Windows API),有些相对较小(比如 memcached 和 NATS),有些非常小(比如 World Clock API、TinyURL 和 Google Fonts API)。当我们谈论分布式系统的 API 时,我们只是指您可以查询或修改其内部状态的所有方式的总和。API 设计在计算文献中已经得到很好的覆盖;本章重点介绍如何通过暴露具有少量明确定义基元的 API 端点来设计和安全地维护安全系统。例如,您评估的输入可能是对唯一 ID 的 CRUD(创建、读取、更新和删除)操作,而不是接受编程语言的 API。

除了面向用户的 API,还要特别关注管理 API。管理 API 对于应用程序的可靠性和安全性同样重要(可以说更重要)。在使用这些 API 时出现拼写错误和错误可能导致灾难性的中断或暴露大量数据。因此,管理 API 也是恶意行为者最感兴趣的攻击面之一。

管理 API 只能由内部用户和工具访问,因此相对于面向用户的 API,它们可能更快速和更容易更改。然而,一旦内部用户和工具开始构建任何 API,更改它仍然会有成本,因此我们建议仔细考虑其设计。管理 API 包括以下内容:

  • 设置/拆卸 API,例如用于构建、安装和更新软件或提供其运行的容器的 API

  • 维护和紧急 API,例如管理访问以删除损坏的用户数据或状态,或重新启动行为不端的进程

在访问和安全性方面,API 的大小是否重要?考虑一个熟悉的例子:POSIX API,这是我们之前提到的一个非常大的 API。这个 API 很受欢迎,因为它灵活并且为许多人所熟悉。作为一个生产机器管理 API,它通常用于相对明确定义的任务,比如安装软件包、更改配置文件或重新启动守护进程。

用户通常通过交互式的 OpenSSH 会话或使用针对 POSIX API 的脚本工具执行传统的 Unix 主机设置和维护。这两种方法都向调用者公开整个 POSIX API。在交互式会话期间难以限制和审计用户的操作。特别是如果用户恶意尝试规避控制,或者连接的工作站受到损害。

您可以使用各种机制来限制通过 POSIX API 授予用户的权限¹⁰,但这是暴露非常庞大的 API 的基本缺陷。相反,最好将这个庞大的管理 API 减少和分解成更小的部分。然后,您可以遵循最小权限原则,仅授予特定调用者所需的特定操作的权限。

注意

暴露的 POSIX API 不应与 OpenSSH API 混淆。可以利用 OpenSSH 协议及其身份验证、授权和审计(AAA)控件,而无需暴露整个 POSIX API;例如,使用git-shell

紧急功能

以火警拉环的名字命名,指示用户“在紧急情况下打破玻璃”的紧急功能机制在紧急情况下提供对系统的访问,并完全绕过您的授权系统。这对于从意想不到的情况中恢复很有用。有关更多上下文,请参见“优雅失败和紧急功能机制”和“诊断访问拒绝”。

审计

审计主要用于检测错误的授权使用。这可能包括恶意系统操作员滥用其权力,外部参与者窃取用户凭据,或者恶意软件对另一个系统采取意外行动。您对审计和有意义地检测噪音中的信号的能力在很大程度上取决于您审计的系统的设计:

  • 访问控制决策的粒度有多大,或者是否被绕过?(什么?在哪里?

  • 您能清楚地捕获与请求相关的元数据吗?(谁?何时?为什么?

以下提示将有助于制定健全的审计策略。最终,您的成功也将取决于与审计相关的文化。

收集良好的审计日志

使用小型功能 API(如“小型功能 API”中讨论的)对您的审计能力提供了最大的单一优势。最有用的审计日志捕获了一系列细粒度的操作,例如“推送了一个带有密码哈希 123DEAD...BEEF456 的配置”或“执行了<x>命令”。考虑如何向客户显示和证明您的管理操作也有助于使您的审计日志更具描述性,从而在内部更有用。细粒度的审计日志信息使您能够对用户执行或未执行的操作做出强有力的断言,但一定要专注于捕获有用部分的操作。

特殊情况需要特殊访问权限,这需要强大的审计文化。如果您发现现有的小型功能 API 界面不足以恢复系统,您有两个选择:

  • 提供紧急功能,允许用户打开与强大且灵活的 API 的交互式会话。

  • 允许用户以一种无法合理审计其使用情况的方式直接访问凭据。

在这两种情况下,您可能无法构建粒度细致的审计跟踪。记录用户打开与大型 API 的交互式会话并不能有意义地告诉您他们做了什么。一个有动机和知识的内部人员可以轻松地绕过许多捕获交互式会话的会话日志的解决方案,例如记录 bash 命令历史。即使您可以捕获完整的会话记录,有效地审计它可能会非常困难:使用 ncurses 的可视应用程序需要重播才能被人类读取,并且诸如 SSH 多路复用之类的功能可能进一步复杂化捕获和理解交织状态。

对过于宽泛的 API 和/或频繁的紧急访问的解药是培养重视仔细审计的文化。这对可靠性和安全性都至关重要,你可以利用这两种动机来吸引负责任的人。两双眼睛有助于避免拼写错误和错误,你应该始终防范对用户数据的单方面访问。

最终,建立管理 API 和自动化的团队需要以促进审计的方式设计它们。经常访问生产系统的人应该有动力共同解决这些问题,并理解良好审计日志的价值。如果没有文化的强化,审计可能会变成橡皮图章,而紧急访问可能会变成每天的事情,失去了其重要性或紧迫性。文化是确保团队选择、构建和使用支持审计的系统的关键;这些事件只会偶尔发生;审计事件得到应有的审查。

选择审计员

一旦你收集了一个良好的审计日志,你需要选择合适的人来检查(希望是罕见的)记录事件。审计员需要具有正确的上下文和正确的目标。

在上下文方面,审计员需要知道特定操作的作用,最好知道执行该操作的原因。因此,审计员通常会是一个队友、经理或熟悉需要执行该操作的工作流程的人。你需要在充分的上下文和客观性之间取得平衡:虽然内部审查员可能与生成审计事件的人有密切的个人关系和/或希望组织取得成功,但外部私人审计员可能希望继续受雇于一个组织。

选择具有正确目标的审计员取决于审计的目的。在谷歌,我们进行两种广泛的审计:

  • 审计以确保遵循最佳实践

  • 审计以识别安全漏洞

一般来说,“最佳实践”审计支持我们的可靠性目标。例如,一个 SRE 团队可能会选择在每周团队会议期间审计上周的紧急访问事件。这种做法提供了一种文化上的同行压力,以使用和改进更小的服务管理 API,而不是使用紧急使用 API 来访问更灵活的紧急使用 API。广泛范围的紧急访问通常会绕过一些或全部安全检查,使服务面临更高的人为错误风险。

谷歌通常将紧急访问审查下放到团队级别,这样我们就可以利用伴随团队审查的社会规范。进行审查的同行具有上下文,使他们能够发现即使是伪装得很好的行为,这对于防止内部滥用和阻止恶意内部人员至关重要。例如,同事很容易注意到如果一个同事反复使用紧急访问操作来访问一个他们可能实际上并不需要的不寻常资源。这种团队审查还有助于发现管理 API 的不足。当特定任务需要紧急访问时,通常表明需要提供一种更安全或更安全的方式来执行该任务作为正常 API 的一部分。您可以在第二十一章中阅读更多关于这个主题的内容。

在谷歌,我们倾向于集中第二类审计,因为识别外部安全漏洞有利于对组织的广泛视野。一个高级攻击者可能会侵入一个团队,然后利用该访问来侵入另一个团队、服务或角色。每个单独的团队可能不会注意到一些异常的行为,并且没有跨团队的视野来连接不同的行为集。

中央审计团队也可以配备额外的信号和添加代码以进行额外的审计事件,这些事件可能并不广为人知。这些类型的警报在早期检测中可能特别有用,但您可能不希望广泛共享其实现细节。您可能还需要与组织中的其他部门(如法律和人力资源)合作,以确保审计机制是适当的、范围适当的并且有文档记录。

我们在谷歌使用结构化理由将审计日志事件与结构化数据关联起来。当发生生成审计日志的事件时,我们可以将其与结构化引用(例如 bug 编号、工单编号或客户案例编号)关联起来。这样做可以让我们构建审计日志的程序检查。例如,如果支持人员查看客户的付款详情或其他敏感数据,他们可以将这些数据与特定客户案例相关联。因此,我们可以确保观察到的数据属于开启案例的客户。如果我们依赖自由文本字段,要自动化日志验证将会更加困难。结构化理由对于扩展我们的审计工作至关重要,它为中央审计团队提供了关键的上下文,对于有效的审计和分析至关重要。

测试和最小权限

适当的测试是任何良好设计系统的基本属性。测试在最小权限方面有两个重要的维度:

  • 最小权限的测试,以确保访问仅被正确授予必要的资源

  • 最小权限的测试,以确保测试基础设施只具有其所需的访问权限

最小权限的测试

在最小权限的背景下,您需要能够测试明确定义的用户配置文件(例如,数据分析师、客户支持、SRE)是否具有足够的权限来执行其角色,但不会过多。

您的基础设施应该让您做到以下几点:

  • 描述特定用户配置文件在其工作角色中需要能够做什么。这定义了他们在角色中所需的最小访问权限(API 和数据)以及访问类型(读或写,永久或临时)。

  • 描述一组场景,在这些场景中,用户配置文件尝试在您的系统上执行操作(例如读取、批量读取、写入、删除、批量删除、管理),并描述对您的系统的预期结果/影响。

  • 运行这些场景,并将实际结果/影响与预期结果/影响进行比较。

理想情况下,为了防止对生产系统产生不利影响,这些测试应该在代码或 ACL 更改之前运行。如果测试覆盖不完整,您可以通过监控访问和警报系统来减轻过于广泛的访问。

最小权限的测试

测试应该允许您验证预期的读/写行为,而不会危及服务的可靠性、敏感数据或其他关键资产。但是,如果您没有适当的测试基础设施,即考虑到各种环境、客户端、凭证、数据集等的基础设施,那么需要读/写数据或改变服务状态的测试可能会带来风险。

考虑将配置文件推送到生产环境的示例,我们将在下一节中返回到这个示例。在为此配置推送设计测试策略的第一步是提供一个使用自己凭证的单独环境。这样的设置可以确保在编写或执行测试时出现错误不会影响生产环境,例如覆盖生产数据或使生产服务崩溃。

或者,假设您正在开发一个键盘应用程序,允许用户一键发布表情包。您希望分析用户的行为和历史,以便自动推荐表情包。如果缺乏适当的测试基础设施,您需要在生产环境中给数据分析师读/写访问权限,以执行分析和测试。

适当的测试方法应考虑限制用户访问和降低风险的方式,但仍允许数据分析师执行他们需要完成工作的测试。他们需要写入权限吗?您可以使用匿名化的数据集来执行他们需要执行的任务吗?您可以使用测试账户吗?您可以在具有匿名化数据的测试环境中操作吗?如果此访问受到威胁,哪些数据会被暴露?

您可以通过从小处开始来处理测试基础设施——不要让完美成为良好的敌人。首先考虑您最容易实现的方式

  • 分离环境和凭证

  • 限制访问类型

  • 限制数据的暴露

最初,也许您可以在云平台上快速进行短暂的测试,而不是构建整个测试基础设施堆栈。一些员工可能只需要读取或临时访问。在某些情况下,您还可以使用代表性或匿名化的数据集。

虽然这些测试最佳实践在理论上听起来很棒,但在这一点上,您可能会被构建适当的测试基础设施的潜在成本所压倒。做对这件事并不便宜。然而,请考虑没有适当的测试基础设施的成本:您能确定每次关键操作的测试不会导致生产中断吗?您能接受数据分析师具有本来可以避免的访问敏感数据的特权吗?您是否依赖于完美的人类和完美执行的完美测试?

对于您特定的情况,进行适当的成本效益分析非常重要。最初构建“理想”解决方案可能并不合理。但是,请确保您构建的框架会被人们使用。人们需要进行测试。如果您没有提供足够的测试框架,他们将在生产环境中进行测试,绕过您制定的控制措施。

诊断访问拒绝

在一个复杂的系统中,最小权限被强制执行,客户必须通过第三方因素、多方授权或其他机制(见“高级控制”)来赢得信任,策略在多个级别和细粒度上被执行。因此,策略拒绝也可能以复杂的方式发生。

考虑一个理智的安全策略正在执行的情况,您的授权系统拒绝访问。可能会出现三种可能的结果之一:

  • 客户被正确拒绝,您的系统行为正确。最小权限已被执行,一切都很好。

  • 客户被正确拒绝,但可以使用高级控制(如多方授权)来获得临时访问。

  • 客户认为他们被错误拒绝了,并可能向您的安全策略团队提交支持工单。例如,如果客户最近被从授权组中移除,或者策略以微妙或可能不正确的方式发生了变化,这可能会发生。

在所有情况下,呼叫者对拒绝的原因一无所知。但系统是否可以向客户提供更多信息?根据呼叫者的权限级别,它可以。

如果客户端没有或权限非常有限,拒绝应该保持盲目 - 您可能不希望暴露 403 访问被拒绝错误代码(或其等效),因为有关拒绝原因的详细信息可能被利用以获取有关系统的信息,甚至找到获得访问权限的方法。但是,如果调用者具有某些最低权限,您可以提供与拒绝相关联的令牌。调用者可以使用该令牌调用高级控件以获取临时访问权限,或通过支持渠道将令牌提供给安全策略团队,以便他们用于诊断问题。对于更具特权的调用者,您可以提供与拒绝相关联的令牌一些纠正信息。然后,调用者可以尝试自行纠正,然后再调用支持渠道。例如,调用者可能会得知访问被拒绝是因为他们需要成为特定组的成员,然后他们可以请求访问该组。

在公开多少纠正信息和安全策略团队可以处理多少支持负载之间总会存在紧张关系。但是,如果您公开了太多信息,客户端可能能够从拒绝信息中重新设计策略,从而使恶意行为者更容易制定使用策略的请求的方式。考虑到这一点,我们建议在实现零信任模型的早期阶段,您使用令牌,并要求所有客户端调用支持渠道。

优雅的失败和破玻璃机制

理想情况下,您总是在处理一个实现合理策略的工作授权系统。但实际上,您可能会遇到导致大规模访问拒绝的场景(可能是由于糟糕的系统更新)。作为回应,您需要能够通过破玻璃机制绕过授权系统,以便您可以修复它。

在使用破玻璃机制时,请考虑以下准则:

  • 使用破玻璃机制的能力应受到严格限制。通常情况下,它应仅对负责系统运行 SLA 的 SRE 团队可用。

  • 零信任网络的破玻璃机制应仅在特定位置可用。这些位置是您的恐慌室,具有额外物理访问控制的特定位置,以抵消其连接性所放置的增加信任。 (细心的读者会注意到,零信任网络的备用机制是不信任网络位置的策略,实际上是信任网络位置,但具有额外的物理访问控制。)

  • 所有使用破玻璃机制的情况都应受到密切监控。

  • 破玻璃机制应该由负责生产服务的团队定期测试,以确保在需要时它能正常运行。

当成功利用破玻璃机制使用户恢复访问权限时,您的 SREs 和安全策略团队可以进一步诊断和解决潜在问题。第八章和第九章讨论了相关策略。

工作示例:配置分发

让我们来看一个现实世界的例子。将配置文件分发给一组 Web 服务器是一个有趣的设计问题,可以通过一个小型的功能 API 实际实现。管理配置文件的最佳实践是:

  1. 将配置文件存储在版本控制系统中。

  2. 对文件进行代码审查更改。

  3. 首先自动将配置文件分发给一组金丝雀,对金丝雀进行健康检查,然后在逐渐将文件推送到 Web 服务器群中继续对所有主机进行健康检查。这一步需要授予自动化访问权限以远程更新配置文件。

有许多方法可以公开一个小型 API,每种方法都针对更新 Web 服务器配置的功能进行了定制。表 5-2 总结了您可能考虑的一些 API 及其权衡。接下来的部分将更深入地解释每种策略。

表 5-2. 更新 Web 服务器配置的 API 及其权衡

通过 OpenSSH 的 POSIX API 软件更新 API 自定义 OpenSSH ForceCommand 自定义 HTTP 接收器
API 表面 各种
现有的 可能 不太可能 不太可能
复杂性 中等
可扩展性 中等 中等,但可重复使用 困难 中等
可审计性
可以表达最小特权 各种
ᵃ 这表明您已经拥有或不太可能拥有这种类型的 API 作为现有 Web 服务器部署的一部分。

通过 OpenSSH 的 POSIX API

您可以允许自动化通过 OpenSSH 连接到 Web 服务器主机,通常以 Web 服务器运行的本地用户身份连接。然后,自动化可以编写配置文件并重新启动 Web 服务器进程。这种模式简单且常见。它利用了可能已经存在的管理 API,因此需要很少的额外代码。不幸的是,利用大型现有的管理 API 会引入一些风险:

  • 运行自动化的角色可以永久停止 Web 服务器,启动另一个二进制文件代替它,读取其访问权限下的任何数据等。

  • 自动化中的错误隐含地具有足够的访问权限,可以导致所有 Web 服务器的协调停机。

  • 自动化凭据的妥协等同于所有 Web 服务器的妥协。

软件更新 API

您可以将配置作为打包的软件更新分发,使用与更新 Web 服务器二进制相同的机制。有许多打包和触发二进制更新的方法,使用不同大小的 API。一个简单的例子是从中央仓库拉取的 Debian 软件包(.deb),由cron调用的周期性apt-get。您可以构建一个更复杂的示例,使用以下部分讨论的模式之一来触发更新(而不是使用cron),然后可以重用该更新来更新配置和二进制文件。随着您不断完善二进制分发机制以增加安全性和安全性,这些好处也会应用到配置上,因为两者使用相同的基础设施。为了集中协调金丝雀流程、协调健康检查或提供签名/来源/审计等工作同样对这两种工件都有好处。

有时,二进制和配置更新系统的需求不一致。例如,您可能在配置中分发一个 IP 拒绝列表,需要尽快收敛,同时将 Web 服务器二进制构建到容器中。在这种情况下,以您希望分发配置更新的速度建立、启动和关闭一个新的容器可能太昂贵或具有破坏性。这种类型的冲突要求可能需要两种分发机制:一种用于二进制文件,另一种用于配置更新。

有关此模式的更多想法,请参见第九章。

自定义 OpenSSH ForceCommand

您可以编写一个简短的脚本来执行这些步骤:

  1. STDIN接收配置。

  2. 对配置进行合理性检查。

  3. 重新启动 Web 服务器以更新配置。

然后,您可以通过在authorized_keys文件中绑定特定条目与ForceCommand选项来通过 OpenSSH 公开此命令。¹² 这种策略向调用者呈现了一个非常小的 API,可以通过经过严格测试的 OpenSSH 协议连接,其中唯一可用的操作是提供配置文件的副本。记录文件(或其哈希值^(13))合理地捕获了会话的整个操作,以供以后审计。

您可以实现尽可能多的这些唯一的密钥/ForceCommand组合,但是这种模式很难扩展到许多唯一的管理操作。虽然您可以在 OpenSSH API 的基础上构建基于文本的协议(例如git-shell),但这样做将开始构建自己的 RPC 机制。您可能最好直接跳到该道路的尽头,构建在现有框架(如gRPCThrift)之上。

自定义 HTTP 接收器(边车)

您可以编写一个小的边车守护程序——非常类似于ForceCommand解决方案,但使用另一个 AAA 机制(例如,带 SSL 的 gRPC,SPIFFE或类似机制)——来接受配置。这种方法不需要修改服务二进制文件,非常灵活,但需要引入更多的代码和另一个守护程序来管理。

自定义 HTTP 接收器(进程内)

您还可以修改 Web 服务器以直接公开 API 来更新其配置,接收配置并将其写入磁盘。这是最灵活的方法之一,与我们在 Google 管理配置的方式非常相似,但需要将代码合并到服务二进制文件中。

权衡

除了表 5-2 中的大选项外,其他选项都提供了向自动化添加安全性和安全性的机会。攻击者仍然可能通过推送任意配置来破坏 Web 服务器的角色;但是,选择较小的 API 意味着推送机制不会隐含允许这种妥协。

您可能还可以通过独立于推送它的自动化对配置进行签名来进一步设计最小特权。这种策略将信任分割为角色之间的信任,保证如果推送配置的自动化角色受到破坏,自动化也不能通过发送恶意配置来破坏 Web 服务器。回想麦克罗伊、平森和塔格的建议,设计系统的每个部分执行一个任务并且执行得很好,可以让您隔离信任。

狭窄 API 呈现的更精细的控制界面还允许您添加对自动化中的错误的保护。除了需要签名来验证配置之外,您还可以要求从中央速率限制器获取持有者令牌¹⁴,该速率限制器独立于您的自动化创建,并针对推出的每个主机。您可以非常小心地对这个通用速率限制器进行单元测试;如果速率限制器是独立实现的,那么可能不会同时影响推出自动化的错误也不会影响它。独立的速率限制器也可以方便地重复使用,因为它可以对 Web 服务器的配置推出进行速率限制,对相同服务器的二进制推出进行速率限制,对服务器的重新启动进行速率限制,或者对您希望添加安全检查的任何其他任务进行速率限制。

用于认证和授权决策的策略框架

认证 [名词]:验证用户或进程的身份

授权 [名词]:评估特定经过身份验证的一方的请求是否应该被允许

前一节提倡为您的服务设计一个狭窄的管理 API,这样您可以授予尽可能少的特权来实现给定的操作。一旦存在该 API,您必须决定如何控制对其的访问。访问控制涉及两个重要但不同的步骤。

首先,您必须验证谁在连接。认证机制的复杂程度可以不同:

简单:接受通过 URL 参数传递的用户名

示例:/service?username=admin

更复杂:呈现预共享密钥

示例:WPA2-PSK,HTTP cookie

更复杂:复杂的混合加密和证书方案

示例:TLS 1.3,OAuth

一般来说,您应该更倾向于重用现有的强大的加密身份验证机制来识别 API 的调用者。这个身份验证决定的结果通常表示为用户名、常用名称、“主体”、“角色”等。在本节中,我们使用角色来描述身份验证的可互换结果。

接下来,您的代码必须做出决定:这个角色是否被授权执行所请求的操作?您的代码可能会考虑请求的许多属性,例如以下内容:

所请求的具体操作

示例:URL,正在运行的命令,gRPC 方法

请求的操作参数

示例:URL 参数,argv,gRPC 请求

请求的来源

示例:IP 地址,客户端证书元数据

经过身份验证的角色的元数据

示例:地理位置,法律管辖区,风险的机器学习评估

服务器端上下文

示例:类似请求的速率,可用容量

本节的其余部分讨论了谷歌发现有用的几种技术,以改进和扩展身份验证和授权决策的基本要求。

使用高级授权控制

给定资源的访问控制列表是实现授权决策的一种熟悉方式。最简单的 ACL 是与经过身份验证的角色匹配的字符串,通常与某种分组概念结合在一起,例如一组角色,例如“管理员”,它扩展为更大的角色列表,例如用户名。当服务评估传入请求时,它会检查经过身份验证的角色是否是 ACL 的成员。

更复杂的授权要求,例如多因素授权(MFA)或多方授权(MPA),需要更复杂的授权代码(有关三因素授权和 MPA 的更多信息,请参见“高级控制”)。此外,一些组织在设计授权策略时可能需要考虑其特定的监管或合同要求。

这段代码很难正确实现,如果许多服务都实现了自己的授权逻辑,其复杂性可能会迅速增加。根据我们的经验,使用像AWSGCP身份和访问管理(IAM)等框架可以将授权决策的复杂性与核心 API 设计和业务逻辑分离。在谷歌,我们还广泛使用 GCP 授权框架的变体来内部服务。¹⁵

安全策略框架允许我们的代码进行简单的检查(例如“X 是否可以访问资源 Y?”),并将这些检查与外部提供的策略进行评估。如果我们需要向特定操作添加更多的授权控制,我们可以简单地更改相关的策略配置文件。这种低开销具有巨大的功能和速度优势。

投资于广泛使用的授权框架

您可以通过使用共享库实现授权决策,并尽可能广泛地使用一致的接口来实现规模化的身份验证和授权更改。在安全领域应用这一经典的模块化软件设计建议会产生意想不到的好处。例如:

  • 您可以通过单个库更改为所有服务端点添加对 MFA 或 MPA 的支持。

  • 然后,您可以通过单个配置更改在所有服务中的少量操作或资源上实现此支持。

  • 通过要求所有允许潜在不安全操作的操作都需要 MPA,您可以提高可靠性,类似于代码审查系统。这种流程改进可以提高对内部风险威胁的安全性(有关对手类型的更多信息,请参见第二章),通过促进快速事件响应(绕过修订控制系统和代码审查依赖)而不允许广泛的单方面访问。

随着组织的发展,标准化是您的朋友。统一的授权框架有助于团队流动性,因为更多的人知道如何使用共同的框架编写代码并实现访问控制。

避免潜在陷阱

设计复杂的授权策略语言是困难的。如果策略语言过于简单,它将无法实现其目标,并且您最终将授权决策分散在框架策略和主要代码库中。如果策略语言过于一般化,它将非常难以理解。为了减轻这些问题,您可以应用标准的软件 API 设计实践,特别是迭代设计方法,但我们建议谨慎进行,以避免这两个极端。

仔细考虑授权策略是如何与二进制文件一起交付的。您可能希望更新授权策略,这可能会成为配置中最敏感的部分之一,独立于二进制文件。有关配置分发的更多讨论,请参见本书的上一节中的示例,第九章和第十四章,SRE 书中的第八章,以及SRE 工作手册中的第十四章和第十五章。

应用程序开发人员将需要协助制定将编码在此语言中的策略决策。即使您避免了这里描述的陷阱,并创建了一种富有表现力和易于理解的策略语言,往往仍需要应用程序开发人员实现管理 API 和具有关于生产环境的特定领域知识的安全工程师和 SRE 之间的合作,以在安全性和功能性之间找到合适的平衡。

深入探讨:高级控制

虽然许多授权决策是二进制的是/否,但在某些情况下更灵活性是有用的。与其要求严格的是/否,不如配备一个“可能”的逃生阀,再加上额外的检查,可以极大地减轻系统的压力。这里描述的许多控制可以单独使用或组合使用。适当的使用取决于数据的敏感性,行动的风险以及现有的业务流程。

多方授权(MPA)

引入另一个人是确保正确访问决策的一种经典方式,培养安全和可靠性文化(参见第二十一章)。这种策略提供了几个好处:

  • 防止错误或意外违反可能导致安全或隐私问题的策略。

  • 阻止恶意行为者尝试进行恶意更改。这包括员工,他们面临纪律行动的风险,以及外部攻击者,他们面临被发现的风险。

  • 通过要求至少牵涉到另一个人的妥协或经过仔细构建的更改经过同行审查,可以增加攻击的成本

  • 审计过去的行动,以进行事件响应或事后分析,假设审查是永久记录并以防篡改的方式记录的。

  • 提供客户舒适感。您的客户可能更愿意使用您的服务,因为他们知道没有单个人可以独自进行更改。

MPA 通常用于广泛的访问级别,例如要求批准加入一个授予对生产资源访问权限的组,或者扮演特定角色或凭证的能力。广泛的 MPA 可以作为一个有价值的紧急机制,以启用非常不寻常的操作,对于这些操作你可能没有特定的工作流程。在可能的情况下,你应该尽量提供更细粒度的授权,这可以提供更强的可靠性和安全性保证。如果第二方批准针对小型功能 API 的操作(见[“小型功能 API”]),他们可以更加确信他们正在授权的内容。

关于批准的社会压力也可能导致糟糕的决定。例如,如果一个工程师不愿意拒绝一个可疑请求,因为它是由经理、高级工程师或者站在他们办公桌旁的人发出的。为了减轻这些压力,你可以提供将批准事后升级给安全或调查团队的选项。或者,你可以制定一个政策,要求某种类型的批准全部(或一部分)独立审计。

在构建多方授权系统之前,确保技术和社会动态允许某人说不。否则,系统就没有多少价值。

三因素授权(3FA)

在大型组织中,MPA 通常存在一个关键弱点,可以被决心和坚持的攻击者利用:所有的“多方”使用同一台中央管理的工作站。工作站车队越同质化,攻击者就越有可能攻击一台工作站就能攻击几台甚至全部工作站。

一种经典的方法是为了加固工作站车队免受攻击,用户需要维护两个完全独立的工作站:一个用于一般用途,比如浏览网页和发送/接收电子邮件,另一个更可信的工作站用于与生产环境通信。根据我们的经验,用户最终希望这些工作站具有类似的功能和能力,为了维护这些需要更高级别保护的有限用户集的两套工作站基础设施,既昂贵又难以长期维持。一旦这个问题不再是管理层关注的焦点,人们就没有动力去维护这些基础设施。

减轻单个受损平台可能破坏所有授权的风险需要以下措施:

  • 至少维护两个平台

  • 在两个平台上批准请求的能力

  • (最好)至少有一个平台具有硬化能力

考虑到这些要求,另一个选择是要求从一个硬化的移动平台对某些非常危险的操作进行授权。为了简单和方便起见,你只允许 RPCs 从完全管理的桌面工作站发起,然后要求移动平台进行三因素授权。当一个生产服务接收到一个敏感的 RPC 时,策略框架(在[“用于认证和授权决策的策略框架”]中描述)要求从独立的 3FA 服务获得加密签名的批准。然后该服务指示它将 RPC 发送到移动设备,显示给发起用户,并且他们确认了请求。

硬化移动平台比硬化通用工作站要容易一些。我们发现用户通常更容忍移动设备上的某些安全限制,比如额外的网络监控,只允许一部分应用程序,并且只能连接有限数量的 HTTP 端点。这些策略在现代移动平台上也很容易实现。

一旦您有了一个坚固的移动平台来显示拟议的生产变更,您必须将请求发送到该平台并显示给用户。在谷歌,我们重用了将通知传递到安卓手机的基础设施,以授权和报告我们用户的谷歌登录尝试。如果您有类似的坚固基础设施,那么将其扩展以支持此用例可能是有用的,但如果没有,基本的基于网络的解决方案相对容易创建。3FA 系统的核心是一个简单的 RPC 服务,它接收要授权的请求,并通过受信任的客户端公开请求以进行授权。请求 3FA 受保护的 RPC 的用户会从其移动设备访问 3FA 服务的网络 URL,并收到授权请求。

重要的是要区分 MPA 和 3FA 保护的威胁,以便您可以决定何时应用一致的策略。MPA 不仅保护免受单方面内部风险的威胁,还保护免受个人工作站的妥协(通过需要第二次内部批准)。3FA 保护免受内部工作站的广泛妥协,但在单独使用时不提供任何对内部威胁的保护。要求发起者进行 3FA,并要求第二方进行简单的基于网络的 MPA,可以提供对大多数这些威胁的组合的非常强大的防御,而组织开销相对较小。

业务理由

如“选择审计员”中所述,您可以通过将访问权限与结构化的业务理由(例如错误、事件、工单、案例 ID 或分配的帐户)联系起来来强制执行授权。但是构建验证逻辑可能需要额外的工作,也可能需要对值班或客户服务人员进行流程更改。

例如,考虑客户服务工作流程。在一些小型或不成熟组织中有时会发现的反模式中,基本和初期的系统可能会给客户服务代表访问所有客户记录,要么是出于效率原因,要么是因为不存在控制。更好的选择是默认情况下阻止访问,并且只有在您可以验证业务需求时才允许访问特定数据。这种方法可能是随时间实现的控制梯度。例如,它可能从仅允许分配了一个开放工单的客户服务代表开始。随着时间的推移,您可以改进系统,仅允许访问特定客户以及这些客户的特定数据,并且在经过一段时间后需要客户批准。

当正确配置时,这种策略可以提供强大的授权保证,以确保访问是适当的并且范围适当。结构化的理由允许自动化要求 Ticket #12345 不是随意输入的随机数字,以满足简单的正则表达式检查。相反,理由满足一组平衡运营业务需求和系统能力的访问策略。

临时访问

您可以通过向资源授予临时访问来限制授权决策的风险。当没有针对每个操作的细粒度控制时,这种策略通常是有用的,但您仍希望尽可能使用可用的工具授予最低特权。

您可以以结构化和计划的方式(例如,在值班轮换期间或通过过期的组成员资格)或按需方式授予临时访问(用户明确请求访问)来授予临时访问。您还可以将临时访问与多方授权请求、业务理由或其他授权控制结合使用。临时访问还创建了一个逻辑点进行审计,因为您可以清楚地记录任何给定时间具有访问权限的用户。它还提供了关于临时访问发生位置的数据,因此您可以随时间优先处理并减少这些请求。

临时访问还会减少环境权限。这是管理员更喜欢使用sudo或“以管理员身份运行”而不是作为 Unix 用户root或 Windows 管理员帐户操作的一个原因——当您意外地发出删除所有数据的命令时,您拥有的权限越少,越好!

代理

当后端服务的细粒度控制不可用时,您可以退而使用受到严格监控和限制的代理机器(或堡垒)。只有来自这些指定代理的请求才被允许访问敏感服务。该代理可以限制危险操作,限制操作速度,并执行更高级的日志记录。

例如,您可能需要执行紧急回滚以撤销错误的更改。鉴于错误更改可能发生的无限方式,以及解决错误更改可能的无限方式,执行回滚所需的步骤可能不会在预定义的 API 或工具中提供。您可以给系统管理员灵活性来解决紧急情况,但引入限制或额外的控制以减轻风险。例如:

  • 每个命令可能需要同行批准。

  • 管理员可能只连接到相关的机器。

  • 管理员使用的计算机可能无法访问互联网。

  • 您可以启用更彻底的日志记录。

与此同时,实现任何这些控制都会带来集成和运营成本,如下一节所讨论的。

权衡和紧张关系

采用最小权限访问模型肯定会改善您组织的安全姿态。然而,您必须权衡前面部分中概述的好处与实现该姿态的潜在成本。本节考虑了其中一些成本。

增加安全复杂性

高度细粒度的安全姿态是一个非常强大的工具,但也很复杂,因此难以管理。拥有一套全面的工具和基础设施来帮助您定义、管理、分析、推送和调试安全策略非常重要。否则,这种复杂性可能变得令人不堪重负。您应该始终力求能够回答这些基本问题:“给定用户是否有权访问给定服务/数据?”和“对于给定服务/数据,谁有权限访问?”

对协作和公司文化的影响

虽然最小权限模型可能适用于敏感数据和服务,但在其他领域采取更宽松的方法可以提供切实的好处。

例如,为软件工程师提供对源代码的广泛访问权带来一定的风险。然而,这可以通过工程师能够根据自己的好奇心在工作中学习,并在能够借用他们的注意力和专业知识时在正常角色之外贡献功能和错误修复来平衡。更不明显的是,这种透明度也使工程师更难以编写不合适的代码而不被注意到。

在您的数据分类工作中包括源代码和相关工件可以帮助您形成一个有原则的方法来保护敏感资产,同时又能从对不太敏感资产的可见性中受益,您可以在第二十一章中了解更多。

影响安全的高质量数据和系统

在零信任环境中,最小特权的基础上,每个细粒度的安全决策都取决于两件事:正在执行的策略和请求的上下文。上下文受到大量数据的影响,其中一些可能是动态的,这些数据可能会影响决策。例如,数据可能包括用户的角色、用户所属的组、发出请求的客户端的属性、输入到机器学习模型的训练集,或者被访问的 API 的敏感性。您应该审查产生这些数据的系统,以确保安全影响数据的质量尽可能高。低质量的数据将导致不正确的安全决策。

对用户生产力的影响

您的用户需要尽可能高效地完成其工作流程。最佳的安全姿态是您的最终用户不会注意到的。然而,引入新的三因素和多方授权步骤可能会影响用户的生产力,特别是如果用户必须等待获得授权。您可以通过确保新步骤易于导航来减轻用户的痛苦。同样,最终用户需要一种简单的方式来理解访问拒绝,可以通过自助诊断或快速访问支持渠道。

对开发人员复杂性的影响

随着最小特权模型在您的组织中得到采用,开发人员必须遵守它。这些概念和政策必须易于被不太懂安全的开发人员消化,因此您应该提供培训材料并充分记录您的 API。在他们应对新要求时,为开发人员提供便捷快速地访问安全工程师进行安全审查和一般咨询。在这种环境中部署第三方软件需要特别小心,因为您可能需要将软件包装在一个可以执行安全策略的层中。

结论

在设计复杂系统时,最小特权模型是确保客户能够完成所需工作,但不多的最安全方式。这是一种强大的设计范式,可以保护您的系统和数据免受已知或未知用户造成的恶意或意外损害。谷歌已经花费了大量时间和精力来实现这个模型。以下是关键组件:

  • 对系统功能的全面了解,以便根据每个部分的安全风险水平对其进行分类。

  • 基于这种分类,对系统和数据进行尽可能细致的分区。最小特权需要小型功能 API。

  • 用于验证用户凭据的身份验证系统,当他们尝试访问您的系统时。

  • 强制执行明确定义的安全策略的授权系统,可以轻松附加到您的精细分区系统上。

  • 一套用于微妙授权的高级控制。例如,这些控制可以提供临时、多因素和多方批准。

  • 系统支持这些关键概念的操作要求。至少,您的系统需要以下内容:

  • 审计所有访问并生成信号,以便您可以识别威胁并进行历史取证分析。

  • 推理、定义、测试和调试安全策略的手段,并为此策略提供最终用户支持

  • 当您的系统表现不如预期时,提供一个紧急访问机制

使所有这些组件以对用户和开发人员易于采用的方式工作,并且不会对其生产力产生显着影响,还需要组织承诺尽可能无缝地采用最低特权。这一承诺包括一个专注的安全功能,通过安全咨询、策略定义、威胁检测和安全相关问题的支持,拥有您的安全姿态并与用户和开发人员进行接口。

虽然这可能是一项艰巨的任务,但我们坚信这是对安全姿态执行现有方法的重大改进。

McIlroy, M.D., E.N. Pinson, and B.A. Tague. 1978. “UNIX Time-Sharing System: Foreword.” The Bell System Technical Journal 57(6): 1899–1904. doi:10.1002/j.1538-7305.1978.tb02135.x。

POSIX 代表可移植操作系统接口,是由大多数 Unix 变体提供的 IEEE 标准化接口。有关概述,请参阅Wikipedia

Windows API 包括熟悉的图形元素,以及编程接口,如DirectXCOM等。

Memcached是一个高性能的分布式内存对象缓存系统。

NATS是一个基于文本协议构建的基本 API 的示例,而不是像 gRPC 这样的复杂 RPC 接口。

TinyURL.com API 文档不够完善,但它本质上是一个返回缩短 URL 作为响应主体的单个 GET URL。这是一个具有微小 API 的可变服务的罕见示例。

Fonts API只是列出当前可用的字体。它只有一个端点。

一个很好的起点是 Gamma, Erich et al. 1994. Design Patterns: Elements of Reusable Object-Oriented Software. Boston, MA: Addison-Wesley. 另请参阅 Bloch, Joshua. 2006. “How to Design a Good API and Why It Matters.” Companion to the 21st ACM SIGPLAN Symposium on Object-Oriented Programming Systems, Languages, and Applications: 506–507. doi:10.1145/1176617.1176622。

尽管我们以 Unix 主机为例,但这种模式并不局限于 Unix。传统的 Windows 主机设置和管理遵循类似的模型,其中 Windows API 的交互暴露通常是通过 RDP 而不是 OpenSSH。

流行的选项包括以非特权用户身份运行,然后通过sudo对允许的命令进行编码,仅授予必要的capabilities(7),或者使用类似 SELinux 的框架。

Canarying是指将更改慢慢地推出到生产环境,从一小部分生产端点开始。就像煤矿中的金丝雀一样,它会在出现问题时提供警告信号。有关更多信息,请参阅SRE 书中的第 27 章

ForceCommand是一种配置选项,用于限制特定授权身份仅运行单个命令。有关更多详细信息,请参阅sshd_config manpage

¹³ 在规模上,记录和存储许多重复文件副本可能是不切实际的。记录哈希值可以让您将配置关联回修订控制系统,并在审计日志中检测未知或意外的配置。作为额外的保险,如果空间允许,您可能希望存储被拒绝的配置,以帮助进行后续调查。理想情况下,所有配置应该被签名,表明它们来自具有已知哈希值的修订控制系统,或者被拒绝。

¹⁴ 持票令牌只是一个由速率限制器签名的加密签名,可以呈现给任何人,该人具有速率限制器的公钥。他们可以使用该公钥验证速率限制器在令牌的有效期内批准了此操作。

¹⁵ 我们的内部变体支持我们的内部身份验证原语,避免了一些循环依赖的问题等。

¹⁶ 参见“用于身份验证和授权决策的策略框架”。

第六章:面向可理解性的设计

原文:6. Design for Understandability

译者:飞龙

协议:CC BY-NC-SA 4.0

由 Julien Boeuf‎、Christoph Kern‎和 John Reese‎

与 Guy Fischman、Paul Blankinship、Aleksandra Culver、Sergey Simakov、Peter Valchev 和 Douglas Colish 一起

为了本书的目的,我们将系统的“可理解性”定义为具有相关技术背景的人能够准确自信地推理以下两点的程度:

  • 系统的操作行为

  • 系统的不变性,包括安全性和可用性

为什么可理解性很重要?

设计一个可理解的系统,并在一段时间内保持其可理解性,需要努力。一般来说,这种努力是一种投资,以持续的项目速度回报(如第四章中所讨论的)。更具体地说,可理解的系统具有具体的好处:

降低安全漏洞或韧性故障的可能性

无论何时您修改系统或软件组件,例如添加功能,修复错误或更改配置,都存在您可能会意外引入新的安全漏洞或损害系统操作韧性的风险。系统越不易理解,修改它的工程师犯错的可能性就越大。该工程师可能会误解系统的现有行为,或者可能不知道与更改冲突的隐藏、隐含或未记录的要求。

促进有效的事故响应

在事故发生时,响应者能够快速准确地评估损害,控制事故,并确定和纠正根本原因至关重要。一个复杂、难以理解的系统显著阻碍了这一过程。

增加对系统安全状况的断言的信心

关于系统安全性的断言通常以“不变性”来表达:系统的所有可能行为都必须满足的属性。这包括系统对其外部环境的意外交互的响应,例如,当系统接收到格式错误或恶意制作的输入时。换句话说,系统对恶意输入的响应不得违反所需的安全属性。在难以理解的系统中,很难或有时不可能以高度自信来验证这样的断言是否成立。测试通常不足以证明“对于所有可能的行为”属性成立——测试通常只对系统进行了相对较小比例的行为的练习,这些行为对应于典型或预期的操作。¹ 您通常需要依赖对系统的抽象推理来建立这样的属性作为不变性。

系统不变性

系统不变性是一个属性,无论系统的环境如何行为或不当行为,它始终为真。系统完全负责确保所需的属性实际上是不变的,即使系统的环境以任意意外或恶意的方式行为不当。该环境包括您无法直接控制的一切,从用恶意制作的请求击中您的服务前端的恶意用户到导致随机崩溃的硬件故障。分析系统的主要目标之一是确定特定所需属性是否实际上是不变的。

以下是系统所需的一些安全性和可靠性属性的一些示例:

  • 只有经过身份验证并得到适当授权的用户才能访问系统的持久数据存储。

  • 系统的持久数据存储中对敏感数据的所有操作都根据系统的审计政策记录在审计日志中。

  • 系统信任边界之外接收到的所有值在传递给易受注入漏洞的 API(例如 SQL 查询 API 或用于构建 HTML 标记的 API)之前都经过适当的验证或编码。

  • 系统后端接收到的查询数量与系统前端接收到的查询数量成比例增长。

  • 如果系统后端在预定时间后未能响应查询,系统前端将优雅地降级,例如通过返回一个近似答案。

  • 当任何组件的负载大于该组件可以处理的负载时,为了减少级联故障的风险,该组件将提供超载错误而不是崩溃。

  • 系统只能从一组指定系统接收 RPC,并且只能向一组指定系统发送 RPC。

如果您的系统允许违反所需安全属性的行为,换句话说,如果所述属性实际上不是不变的,那么系统就存在安全弱点或漏洞。例如,想象一下,列表中的属性 1 对于您的系统来说并不成立,因为请求处理程序缺少访问检查,或者因为这些检查实现不正确。现在您的系统存在安全漏洞,可能允许攻击者访问用户的私人数据。

同样,假设您的系统不满足第四个属性:在某些情况下,系统为每个传入的前端请求生成过多的后端请求。例如,也许前端如果后端请求失败或花费太长时间会快速生成多个重试(并且没有适当的退避机制)。您的系统存在潜在的可用性弱点:一旦系统达到这种状态,其前端可能会完全压倒后端并使服务无响应,形成一种自我造成的拒绝服务场景。

分析不变量

在分析系统是否满足给定的不变量时,存在违反该不变量可能造成的潜在危害与您花费在满足不变量和验证其实际成立的努力之间的权衡。在这个光谱的一端,这种努力可能涉及运行一些测试并阅读源代码的部分,以寻找可能导致违反不变量的错误,例如遗忘的访问检查。这种方法并不能带来特别高的信心。很可能,许多情况下未经测试或深入代码审查的行为将存在错误。值得注意的是,像 SQL 注入、跨站脚本(XSS)和缓冲区溢出等众所周知的常见软件漏洞类别一直在“顶级漏洞”列表中保持领先地位。[2] 缺乏证据并不意味着证据的缺失。

另一方面,您可能会进行基于可证明的合理形式推理的分析:系统和所声称的属性在一个正式逻辑中建模,并且您构建一个逻辑证明(通常在自动证明助手的帮助下),证明该属性对系统成立。[3] 这种方法很困难,需要大量工作。例如,迄今为止最大的软件验证项目之一构建了一个证明,证明了微内核实现在机器代码级别的全面正确性和安全性属性;该项目大约耗费了 20 人年的工作。[4] 尽管形式验证在某些情况下正在变得实用,比如微内核或复杂的加密库代码,[5] 但对于大规模应用软件开发项目来说通常是不可行的。

本章旨在提出一个实用的中间立场。通过设计一个明确的可理解性目标的系统,您可以支持有原则的(但仍然是非正式的)论证,即系统具有某些不变量,并且在合理的努力下对这些断言有相当高的信心。在谷歌,我们发现这种方法对大规模软件开发非常实用,并且在减少常见漏洞发生方面非常有效。有关测试和验证的更多讨论,请参见第十三章。

心理模型

高度复杂的系统对人类来说很难以整体方式进行推理。在实践中,工程师和主题专家经常构建解释系统相关行为的心理模型,同时忽略不相关的细节。对于复杂系统,您可能构建多个相互补充的心理模型。这样,当思考给定系统或子系统的行为或不变量时,您可以抽象出其周围和底层组件的细节,而代之以它们各自的心理模型。

心理模型很有用,因为它们简化了对复杂系统的推理。出于同样的原因,心理模型也是有限的。如果您根据系统在典型操作条件下的经验形成了心理模型,那么该模型可能无法预测系统在不寻常情况下的行为。在很大程度上,安全性和可靠性工程关注的是在这些不寻常条件下分析系统,例如当系统处于主动攻击、过载或组件故障场景时。

考虑一个系统,其吞吐量通常会随着传入请求的速率可预测地逐渐增加。然而,在某个负载阈值之上,系统可能会达到一个状态,其响应方式截然不同。例如,内存压力可能导致在虚拟内存或堆/垃圾收集器级别出现抖动,使系统无法跟上额外负载。太多的额外负载甚至可能导致减少吞吐量。在这种状态下排除系统故障时,除非您明确意识到该模型不再适用,否则您可能会被系统的过度简化心理模型误导。

在设计系统时,考虑软件、安全和可靠性工程师不可避免地会为自己构建的心理模型是很有价值的。在设计要添加到较大系统中的新组件时,理想情况下,其自然形成的心理模型应与人们为类似的现有子系统形成的心理模型一致。

在可能的情况下,您还应设计系统,使其心理模型在系统在极端或不寻常条件下运行时仍然具有预测性和实用性。例如,为了避免抖动,您可以配置生产服务器在没有磁盘虚拟内存交换空间的情况下运行。如果生产服务无法分配所需的内存来响应请求,它可以以可预测的方式快速返回错误。即使有错误或行为不端的服务无法处理内存分配失败并崩溃,您至少可以清楚地将故障归因于潜在问题——在这种情况下是内存压力;这样,观察系统的人的心理模型仍然是有用的。

设计可理解的系统

本章的其余部分讨论了一些具体措施,可以使系统更易理解,并在系统随时间演变时保持其可理解性。我们将首先考虑复杂性问题。

复杂性与可理解性

可理解性的主要敌人是未受控制的复杂性

由于现代软件系统的规模(尤其是分布式系统)及其解决的问题,某种程度的复杂性通常是内在的和不可避免的。例如,谷歌雇佣了数万名工程师,他们在一个包含超过 10 亿行代码的源代码库中工作。这些代码共同实现了大量的用户服务以及支持它们的后端和数据管道。即使是只提供单一产品的较小组织,也可能在数十万行代码中实现数百个功能和用户故事,由数十甚至数百名工程师编辑。

让我们以 Gmail 为例,这是一个具有重要内在特性复杂性的系统。你可以简要总结 Gmail 为基于云的电子邮件服务,但这一概括掩盖了它的复杂性。在其众多功能中,Gmail 提供以下功能:

  • 多个前端和用户界面(桌面 Web、移动 Web、移动应用)

  • 允许第三方开发人员开发附加组件的几个 API

  • 入站和出站 IMAP 和 POP 接口

  • 与云存储服务集成的附件处理

  • 以多种格式呈现附件,如文档和电子表格

  • 离线可用的 Web 客户端和底层同步基础设施

  • 垃圾邮件过滤

  • 自动消息分类

  • 用于提取关于航班、日历事件等结构化信息的系统

  • 拼写纠正

  • 智能回复和智能撰写

  • 提醒回复消息

具有这些功能的系统本质上比没有这些功能的系统更复杂,但我们不能告诉 Gmail 的产品经理这些功能增加了太多复杂性,并要求他们出于安全性和可靠性的考虑将其删除。毕竟,这些功能提供了价值,并在很大程度上定义了 Gmail 作为产品。但如果我们努力管理这种复杂性,系统仍然可以足够安全和可靠。

如前所述,可理解性与系统和子系统的特定行为和属性相关。我们的目标必须是构建系统设计,以便以一种允许人类高度准确地推理这些特定、相关系统属性和行为的方式来分隔和包含这种内在复杂性。换句话说,我们必须特别管理妨碍可理解性的复杂性方面。

当然,说起来容易做起来难。本节的其余部分将探讨未受管理的复杂性的常见来源以及相应的降低可理解性的设计模式,以及可以帮助控制复杂性并使系统更易理解的设计模式。

虽然我们的主要关注点是安全性和可靠性,但我们讨论的模式在很大程度上并不特定于这两个领域,它们与旨在管理复杂性和促进可理解性的一般软件设计技术非常一致。你可能还想参考有关系统和软件设计的一般文本,如约翰·奥斯特豪特的《软件设计哲学》(Yaknyam Press,2018)。

分解复杂性

要理解复杂系统行为的所有方面,你需要内化并维护一个庞大的心智模型。人类在这方面并不擅长。

通过由较小的组件组合而成,可以使系统更易理解。你应该能够独立地推理每个组件,并以这样的方式组合它们,以便从组件属性推导出整个系统的属性。这种方法使你能够建立整个系统的不变性,而无需一次性考虑整个系统。

这种方法在实践中并不简单。您能够建立子系统的属性,并将子系统的属性组合成系统范围的属性,取决于整个系统如何被结构化为组件以及这些组件之间的接口和信任关系的性质。我们将在“系统架构”中讨论这些关系和相关考虑。

集中负责安全和可靠性要求

如第四章所述,安全和可靠性要求通常横跨系统的所有组件。例如,安全要求可能规定,对于响应用户请求执行的任何操作,系统必须完成一些常见任务(例如审计日志记录和操作指标收集)或检查某些条件(例如身份验证和授权)。

如果每个单独的组件都负责独立实现常见任务和检查,很难确定最终系统是否真正满足要求。您可以通过将常见功能的责任移交给集中组件(通常是库或框架)来改进这种设计。例如,RPC 服务框架可以确保系统根据为整个服务集中定义的策略为每个 RPC 方法实现身份验证、授权和日志记录。有了这种设计,单个服务方法不再负责这些安全功能,应用程序开发人员也不会忘记实现它们或者实现不正确。此外,安全审阅者可以理解服务的身份验证和授权控制,而无需阅读每个单独的服务方法实现。相反,审阅者只需理解框架并检查特定于服务的配置。

再举一个例子:为了防止负载下的级联故障,传入请求应该受到超时和截止日期的限制。任何重试由于过载引起的故障的逻辑都应受到严格的安全机制的约束。为了实现这些策略,您可能依赖应用程序或服务代码来配置子请求的截止日期并适当处理故障。单个应用程序中任何相关代码的错误或遗漏都可能导致整个系统的可靠性弱点。通过在底层 RPC 服务框架中包含支持自动截止日期传播和请求取消的集中处理机制,您可以使系统更加健壮和可理解。⁷

这些例子突出了集中负责安全和可靠性要求的两个好处:

提高系统的可理解性

审阅者只需查看一个地方,就能理解和验证安全/可靠性要求是否正确实现。

增加最终系统实际正确的可能性

这种方法消除了应用代码中对要求的临时实现不正确或缺失的可能性。

虽然在构建和验证应用程序框架或库的集中实现时存在前期成本,但这些成本可以分摊到基于该框架构建的所有应用程序中。

系统架构

将系统结构化为层和组件是管理复杂性的关键工具。使用这种方法,您可以按块来思考系统,而不必一次性理解整个系统的每个细节。

您还需要仔细考虑如何将系统分解为组件和层。过于紧密耦合的组件和层与单片系统一样难以理解。要使系统可理解,您必须像关注组件本身一样关注组件之间的边界和接口。

有经验的软件开发人员通常知道系统必须将来自外部环境的输入(和交互序列)视为不可信,并且系统不能对这些输入做出假设。相比之下,很容易将内部、较低层 API 的调用者(如进程内服务对象的 API 或内部后端微服务暴露的 RPC)视为可信任,并依赖这些调用者遵守 API 使用的文档约束。

假设系统的安全属性取决于内部组件的正确操作。另外,假设其正确操作又取决于组件 API 的调用者确保的前提条件,比如操作的正确顺序,或者方法参数的值的约束。确定系统是否实际具有所需的属性不仅需要理解 API 的实现,还需要理解整个系统中 API 的每个调用点,以及每个调用点是否确保了所需的前提条件。

组件对其调用者做出的假设越少,就越容易独立推理该组件。理想情况下,组件对其调用者不做任何假设。

如果组件被迫对其调用者做出假设,重要的是要在接口设计中明确捕获这些假设,或者在环境的其他约束中明确捕获这些假设,例如限制可以与组件交互的主体集。

可理解的接口规范

结构化接口、一致的对象模型和幂等操作有助于系统的可理解性。如下节所述,这些考虑因素使得更容易预测输出行为以及接口之间的交互方式。

更喜欢提供较少解释空间的窄接口

服务可以使用许多不同的模型和框架来公开接口。举几个例子:

  • 带有 OpenAPI 的 RESTful HTTP 和 JSON

  • gRPC

  • Thrift

  • W3C Web Services(XML/WSDL/SOAP)

  • CORBA

  • DCOM

其中一些模型非常灵活,而其他模型提供更多结构。例如,使用 gRPC 或 Thrift 的服务定义了它支持的每个 RPC 方法的名称,以及该方法的输入和输出类型。相比之下,自由格式的 RESTful 服务可能接受任何 HTTP 请求,而应用代码验证请求体是否为具有预期结构的 JSON 对象。

支持用户定义类型的框架(如 gRPC、Thrift 和 OpenAPI)使得更容易创建工具,用于增强 API 表面的可发现性和可理解性,比如交叉引用和一致性检查。这些框架通常也允许 API 表面随着时间的推移更安全地演化。例如,OpenAPI 具有 API 版本控制作为内置功能。用于声明 gRPC 接口的协议缓冲区有关如何更新消息定义以保持向后兼容性的详细文档指南

相比之下,基于自由格式 JSON 字符串构建的 API 在不检查其实现代码和核心业务逻辑的情况下可能难以理解。这种无约束的方法可能导致安全或可靠性事件。例如,如果客户端和服务器独立更新,它们可能以不同方式解释 RPC 有效负载,这可能导致其中一个崩溃。

缺乏明确的 API 规范也使得评估服务的安全姿态变得困难。例如,除非您可以访问 API 定义,否则很难构建一个自动安全审计系统,将授权框架(如Istio 授权策略中描述的策略与服务实际暴露的表面积相关联。

优先选择强制实现通用对象模型的接口

管理多种类型资源的系统可以从一个通用的对象模型中受益,比如Kubernetes使用的模型。通用对象模型让工程师可以使用单一的思维模型来理解系统的大部分内容,而不是单独处理每种资源类型。例如:

  • 系统中的每个对象都可以保证满足一组预定义的基本属性(不变量)。

  • 系统可以提供标准的方式来范围、注释、引用和分组所有类型的对象。

  • 操作可以在所有类型的对象上具有一致的行为。

  • 工程师可以创建自定义对象类型来支持他们的用例,并且可以使用与内置类型相同的思维模型来推理这些对象类型。

Google 提供了关于设计面向资源的 API 的一般指南

注意幂等操作

幂等操作在多次应用时会产生相同的结果。例如,如果一个人在电梯里按下二楼的按钮,电梯每次都会到达二楼。再次按下按钮,甚至多次按下,都不会改变结果。

在分布式系统中,幂等性很重要,因为操作可能以无序的方式到达,或者服务器在完成操作后的响应可能永远不会到达客户端。如果一个 API 方法是幂等的,客户端可以重试操作,直到收到成功的结果。如果一个方法不是幂等的,系统可能需要使用次要方法,比如轮询服务器来查看新创建的对象是否已经存在。

幂等性也会影响工程师的思维模型。API 的实际行为与预期行为之间的不匹配可能导致不可靠或不正确的结果。例如,假设客户端想要向数据库添加一条记录。虽然请求成功,但由于连接重置,响应未被传递。如果客户端代码的作者认为该操作是幂等的,客户端很可能会重试该请求。但如果操作实际上不是幂等的,系统将创建一个重复的记录。

虽然非幂等操作可能是必要的,但幂等操作通常会导致更简单的思维模型。当操作是幂等的时,工程师(包括开发人员和事件响应者)不需要跟踪操作何时开始;他们可以简单地不断尝试操作,直到知道它成功为止。

一些操作自然是幂等的,通过重新构造其他操作也可以使其成为幂等。在前面的例子中,数据库可以要求客户端在每次变异的 RPC 中包含一个唯一标识符(例如 UUID)。如果服务器收到具有相同唯一标识符的第二次变异,它就知道该操作是重复的,并可以相应地做出响应。

可理解的身份、认证和访问控制

任何系统都应该能够确定谁有权访问哪些资源,特别是如果这些资源非常敏感。例如,支付系统的审计员需要了解哪些内部人员可以访问客户的个人身份信息。通常,系统具有授权和访问控制策略,限制特定实体在特定上下文中对特定资源的访问——在这种情况下,策略将限制员工在处理信用卡时对 PII 数据的访问。当发生这种特定访问时,审计框架可以记录访问。稍后,您可以自动分析访问日志,作为例行检查的一部分,或作为事故调查的一部分。

身份

身份是与实体相关的属性或标识符集合。凭证断言了特定实体的身份。凭证可以采用不同的形式,例如简单密码、X.509 证书或 OAuth2 令牌。凭证通常使用定义好的认证协议发送,访问控制系统用于识别访问资源的实体。识别实体并选择一个用于识别它们的模型可能是复杂的。虽然系统识别人类实体(包括客户和管理员)相对容易,但大型系统需要能够识别所有实体,而不仅仅是人类实体。

大型系统通常由一系列相互调用的微服务组成,无论是否涉及人类。例如,数据库服务可能希望定期快照到较低级别的磁盘服务。这个磁盘服务可能需要调用配额服务,以确保数据库服务有足够的磁盘配额来存储需要快照的数据。或者,考虑一个客户对食品订购前端服务进行身份验证。前端服务调用后端服务,后端服务再调用数据库来检索客户的食品偏好。一般来说,活动实体是系统中相互交互的人类、软件组件和硬件组件的集合。

注意

传统的网络安全实践有时使用 IP 地址作为访问控制和日志记录和审计(例如防火墙规则)的主要标识符。不幸的是,在现代微服务系统中,IP 地址存在许多缺点。因为它们缺乏稳定性和安全性(并且很容易被伪造),IP 地址简单地不能提供一个适当的标识符来识别服务并模拟它们在系统中的特权级别。首先,微服务部署在主机池上,多个服务托管在同一主机上。端口并不提供一个强大的标识符,因为它们可以随着时间的推移被重复使用,或者更糟糕的是,由运行在主机上的不同服务任意选择。一个微服务也可能为在不同主机上运行的不同实例提供服务,这意味着您不能使用 IP 地址作为稳定的标识符。

访问控制和审计机制的质量取决于系统中使用的身份的相关性和它们的信任关系。在系统中为所有活动实体附加有意义的标识符是理解能力的基本步骤,无论是在安全性还是可靠性方面。在安全性方面,标识符帮助您确定谁可以访问什么。在可靠性方面,标识符帮助您规划和执行共享资源的使用,如 CPU、内存和网络带宽。

组织范围的身份系统强化了共同的心智模型,意味着整个员工队伍在提及实体时可以使用相同的语言。对于相同类型的实体存在竞争性的身份系统,例如全球和本地实体的共存系统,会使工程师和审计员的理解变得不必要复杂。

类似于第四章中小部件订购示例中的外部支付处理服务,公司可以将其身份子系统外部化。OpenID Connect(OIDC)提供了一个框架,允许特定提供者断言身份。组织只需配置接受哪些提供者,而不是实现自己的身份子系统。然而,与所有依赖关系一样,这里需要考虑一个权衡——在这种情况下,是这种模型的简单性还是受信任提供者的感知安全性和可靠性的稳健性之间的权衡。

示例:谷歌生产系统的身份模型

谷歌通过使用不同类型的活动实体来建模身份:

管理员

人类(谷歌工程师)可以采取行动改变系统的状态,例如推送新版本或修改配置。

机器

谷歌数据中心的物理机器。这些机器运行实现我们服务的程序(如 Gmail),以及系统本身需要的服务(例如内部时间服务)。

工作负载

这些实体由类似于 Kubernetes 的 Borg 编排系统在机器上调度。大多数情况下,工作负载的身份与其运行的机器的身份不同。

客户

谷歌客户访问由谷歌提供的服务。

管理员是生产系统内所有交互的基础。在工作负载之间的交互中,管理员可能不会主动修改系统的状态,但他们在引导阶段启动工作负载的行为(可能会启动另一个工作负载)。

如第五章中所述,您可以使用审计来追溯所有操作到管理员(或一组管理员),以便建立责任和分析特定员工的特权级别。管理员和他们管理的实体的有意义的身份使审计成为可能。

管理员由全局目录服务集成的单点登录管理。全局组管理系统可以将管理员分组以代表团队的概念。

机器在全局库存服务/机器数据库中进行分类。在谷歌生产网络上,可以使用 DNS 名称寻址机器。我们还需要将机器身份与管理员联系起来,以表示谁可以修改机器上运行的软件。实际上,我们通常将发布机器软件镜像的组与可以以 root 身份登录到机器的组合在一起。

谷歌数据中心的每台生产机器都有一个身份。身份指的是机器的典型用途。例如,专用于测试的实验室机器与运行生产工作负载的机器具有不同的身份。运行机器上的核心应用程序的机器管理守护程序引用此身份。

工作负载使用编排框架在机器上调度。每个工作负载都有一个由请求者选择的身份。编排系统负责确保发出请求的实体有权发出请求,特别是请求者有权以所请求的身份调度运行工作负载。编排系统还对工作负载可以调度到哪些机器施加约束。工作负载本身可以执行组管理等管理任务,但不应具有底层机器的根或管理员权限。

客户身份也有一个专门的身份子系统。在内部,每当服务代表客户执行操作时,这些身份都会被使用。访问控制解释了客户身份如何与工作负载身份协调工作。在外部,Google 提供OpenID Connect 工作流程允许客户使用其 Google 身份对不受 Google 控制的端点(例如zoom.us)进行身份验证。

身份验证和传输安全

身份验证和传输安全是复杂的学科,需要对诸如密码学、协议设计和操作系统等领域有专门的知识。不合理地期望每个工程师都深入了解所有这些主题。

相反,工程师应该能够理解抽象和 API。像 Google 的应用层传输安全(ALTS)这样的系统为应用程序提供了自动的服务对服务身份验证和传输安全。这样,应用程序开发人员就不需要担心凭据是如何配置的,或者用于在连接上保护数据的具体加密算法是什么。

应用程序开发人员的心智模型很简单:

  • 应用程序以有意义的身份运行:

  • 管理员工作站上的工具通常以管理员的身份运行,用于访问生产环境。

  • 机器上的特权进程通常以该机器的身份运行。

  • 使用编排框架将应用程序部署为工作负载,通常以特定于环境和服务提供的工作负载身份运行(例如myservice-frontend-prod)。

  • ALTS 提供了零配置的传输安全。

  • 常见访问控制框架的 API 检索经过身份验证的对等信息。

ALTS 和类似系统(例如Istio 的安全模型)以可理解的方式提供身份验证和传输安全。

除非基础设施的应用程序间安全姿态采用系统化的方法,否则很难或不可能进行推理。例如,假设应用程序开发人员必须就要使用的凭据类型以及这些凭据将断言的工作负载身份做出个别选择。要验证应用程序是否正确执行身份验证,审计员需要手动阅读所有应用程序的代码。这种方法对安全性来说是不好的——它不具备可扩展性,而且代码的某部分很可能是未经审计或不正确的。

访问控制

使用框架对传入服务请求的访问控制策略进行编码和强制执行对于全局系统的可理解性是一个净利益。框架强化了共同的知识,并提供了一种统一的描述策略的方式,因此是工程师工具包的重要组成部分。

框架可以处理固有复杂的交互,例如在工作负载之间传输数据涉及的多个身份。例如,图 6-1 显示了以下内容:

  • 作为三个身份运行的工作负载链:IngressFrontendBackend

  • 进行请求的经过身份验证的客户

工作负载之间传输数据涉及的交互

图 6-1:工作负载之间传输数据涉及的交互

对于链中的每个链接,框架必须能够确定是工作负载还是客户是请求的权威。策略还必须足够表达性,以便它决定允许哪个工作负载身份代表客户检索数据。

具备一种统一的方式来捕获这种固有复杂性,大多数工程师都能理解这些控制。如果每个服务团队都有自己的临时系统来处理相同的复杂用例,理解起来将是一个挑战。

框架规定了在指定和应用声明式访问控制策略方面的一致性。这种声明式和统一的性质使工程师能够开发工具来评估基础设施中服务和用户数据的安全风险。如果访问控制逻辑是以自发方式在应用程序代码级别实现的,开发这种工具基本上是不可能的。

深入探讨:安全边界

系统的可信计算基础(TCB)是“足以确保执行安全策略的一组组件(硬件、软件、人员等)的正确功能,或者更生动地说,其失败可能导致安全策略的违反。”。因此,TCB 必须维护安全策略,即使 TCB 之外的任何实体以任意可能恶意的方式行为不端。当然,TCB 之外的区域包括您系统的外部环境(例如互联网上某处的恶意行为者),但这个区域包括不在 TCB 内的您自己系统的部分。

TCB 与“其他一切”之间的接口被称为安全边界。“其他一切”——系统的其他部分、外部环境、通过网络与其交互的系统客户端等——通过跨越这个边界进行通信与 TCB 交互。这种通信可能以进程间通信通道、网络数据包和建立在这些基础上的更高级协议的形式进行(如 gRPC)。TCB 必须对跨越安全边界的任何东西持怀疑态度——包括数据本身和其他方面,如消息排序。

构成 TCB 的系统部分取决于您所考虑的安全策略。思考安全策略及其必要的 TCB 在层面上维护的相关性可能是有用的。例如,操作系统的安全模型通常具有“用户身份”的概念,并提供规定在不同用户下运行的进程之间分离的安全策略。在类 Unix 系统中,运行在用户 A 下的进程不应能够查看或修改属于不同用户 B 的进程的内存或网络流量。在软件级别上,确保这一属性的 TCB 基本上由操作系统内核和所有特权进程和系统守护程序组成。反过来,操作系统通常依赖于底层硬件提供的机制,如虚拟内存。这些机制包括在与 OS 级用户之间的分离相关的安全策略的 TCB 中。

网络应用服务器的软件(例如,公开 Web 应用程序或 API 的服务器)是此操作系统级安全策略的 TCB 的一部分,因为它在非特权的操作系统级角色(例如httpd用户)下运行。但是,该应用程序可能会强制执行自己的安全策略。例如,假设一个多用户应用程序具有安全策略,只能通过显式文档共享控件访问用户数据。在这种情况下,应用程序的代码(或其中的部分)与该应用程序级安全策略相关的 TCB 内。

为了确保系统执行所需的安全策略,您必须了解并推理与该安全策略相关的整个 TCB。根据定义,TCB 的任何部分的失败或错误可能导致安全策略的违反。

随着 TCB 扩大以包括更多的代码和复杂性,对 TCB 的推理变得更加困难。因此,将 TCB 保持尽可能小,并排除任何实际上不涉及维护安全策略的组件是有价值的。除了损害可理解性外,将这些不相关的组件包括在 TCB 中还增加了风险:这些组件中的任何错误或故障都可能导致安全漏洞。

让我们重新审视一下第四章中的例子:一个允许用户在线购买小部件的网络应用。应用程序的 UI 结账流程允许用户输入信用卡和送货地址信息。系统存储其中一些信息,并将其他部分(如信用卡数据)传递给第三方支付服务。

我们希望确保只有用户自己可以访问他们自己的敏感用户数据,比如送货地址。我们将使用 TCB[AddressData]来表示这个安全属性的受信任计算基础。

使用许多流行的应用程序框架之一,我们可能会得到一个像图 6-2 的架构。

销售小部件的应用程序的示例架构

图 6-2:销售小部件的应用程序的示例架构

在这个设计中,我们的系统由一个整体的网络应用和一个相关的数据库组成。应用可能使用多个模块来实现不同的功能,但它们都是同一个代码库的一部分,整个应用作为一个单一的服务器进程运行。同样,应用将所有数据存储在一个单一的数据库中,服务器的所有部分都可以读取和写入整个数据库。

应用的一部分处理购物车结账和购买,数据库的一些部分存储与购买相关的信息。应用的其他部分处理与购买相关的功能,但它们本身并不依赖于购买功能(例如,管理购物车的内容)。应用的其他部分与购买无关(它们处理诸如浏览小部件目录或阅读和编写产品评论等功能)。由于所有这些功能都是单个服务器的一部分,并且所有这些数据都存储在单个数据库中,整个应用及其依赖项——例如数据库服务器和操作系统内核——都是我们想要提供的安全属性的 TCB 的一部分:执行用户数据访问策略。

风险包括目录搜索代码中的 SQL 注入漏洞,允许攻击者获取敏感用户数据,如姓名或送货地址,或者 Web 应用程序服务器中的远程代码执行漏洞,例如CVE-2010-1870,允许攻击者读取或修改应用程序数据库的任何部分。

小的 TCB 和强大的安全边界

我们可以通过将应用拆分成微服务来改进设计的安全性。在这种架构中,每个微服务处理应用功能的一个独立部分,并将数据存储在自己的独立数据库中。这些微服务通过 RPC 进行通信,并将所有传入请求视为不一定可信,即使调用者是另一个内部微服务。

使用微服务,我们可以重构应用,如图 6-3 所示。

现在,我们不再有一个整体的服务器,而是有一个网络应用前端和产品目录和与购买相关功能的独立后端。每个后端都有自己独立的数据库。前端从不直接查询数据库;相反,它发送 RPC 到适当的后端。例如,前端查询目录后端以搜索目录中的项目或检索特定项目的详细信息。同样,前端发送 RPC 到购买后端以处理购物车结账流程。正如本章前面讨论的那样,后端微服务和数据库服务器可以依赖工作负载标识和基础设施级身份验证协议,如 ALTS 来验证调用者并限制对授权工作负载的请求。¹³

小部件销售应用程序的示例微服务架构

图 6-3:小部件销售应用程序的示例微服务架构

在这种新的架构中,地址数据安全策略的受信任计算基础要小得多:它仅包括购买后端及其数据库,以及它们的相关依赖项。攻击者不再能够利用目录后端中的漏洞来获取付款数据,因为目录后端根本无法访问该数据。因此,这种设计限制了主要系统组件中漏洞的影响(这是第八章中进一步讨论的主题)。

安全边界和威胁模型

受信任的计算基础的大小和形状将取决于您想要保证的安全属性和系统的架构。您不能只是在系统的一个组件周围画一个虚线并称其为 TCB。您必须考虑组件的接口,以及它可能隐含地信任系统的其他部分的方式。

假设我们的应用程序允许用户查看和更新他们的送货地址。由于购买后端处理送货地址,该后端需要公开一个 RPC 方法,允许 Web 前端检索和更新用户的送货地址。

如果购买后端允许前端获取任何用户的送货地址,那么入侵 Web 前端的攻击者可以使用此 RPC 方法来访问或修改任何和所有用户的敏感数据。换句话说,如果购买后端比随机第三方更信任 Web 前端,那么 Web 前端就是 TCB 的一部分。

另外,购买后端可以要求前端提供所谓的终端用户上下文票证(EUC),以在特定外部用户请求的上下文中对请求进行身份验证。EUC 是由中央认证服务发行的内部短期票证,以外部凭据(例如身份验证 cookie 或与特定请求相关的令牌(例如 OAuth2))交换而来。如果后端只对具有有效 EUC 的请求提供数据,那么入侵前端的攻击者就无法完全访问购买后端,因为他们无法为任意用户获取 EUC。最坏的情况是,他们可能会在攻击期间获取正在使用应用程序的用户的敏感数据。

为了提供另一个例子,说明 TCB 相对于正在考虑的威胁模型的相关性,让我们思考一下这种架构与 Web 平台的安全模型的关系。¹⁴在这个安全模型中,Web 来源(服务器的完全合格主机名,加上协议和可选端口)代表一个信任域:在给定来源的上下文中运行的 JavaScript 可以观察或修改该上下文中存在或可用的任何信息。相反,浏览器根据称为同源策略的规则限制不同来源之间内容和代码之间的访问。

我们的 Web 前端可能会从单个 Web 来源(例如https://widgets.example.com)提供其整个 UI。这意味着,例如,通过目录显示 UI 中的 XSS 漏洞¹⁵注入到我们的来源中的恶意脚本可以访问用户的个人资料,并且甚至可能能够以该用户的名义“购买”物品。因此,在 Web 安全威胁模型中,TCB[AddressData]再次包括整个 Web 前端。

我们可以通过进一步分解系统并建立额外的安全边界来解决这种情况,这种情况是基于 Web 来源的。如图 6-4 所示,我们可以操作两个单独的 Web 前端:一个实现目录搜索和浏览,并在https://widgets.example.com提供服务,另一个负责购买配置文件和结账,在https://checkout.example.com提供服务。¹⁶ 现在,目录 UI 中的 Web 漏洞(例如 XSS)不能危害支付功能,因为该功能被隔离到其自己的 Web 来源中。

分解 Web 前端

图 6-4:分解 Web 前端

TCB 和可理解性

除了安全性的好处外,TCB 和安全边界还使系统更容易理解。为了符合 TCB 的资格,组件必须与系统的其余部分隔离。该组件必须具有明确定义的干净接口,并且您必须能够独立地推理 TCB 的实现的正确性。如果组件的正确性取决于该组件控制范围之外的假设,那么它从定义上来说就不是 TCB。

TCB 通常是其自身的故障域,这使得更容易理解应用程序在面对错误、DoS 攻击或其他操作影响时的行为。第八章更深入地讨论了将系统分隔成更多部分的好处。

软件设计

一旦您将一个大型系统结构化为由安全边界分隔的组件,您仍然需要推理给定安全边界内所有代码和子组件的所有内容,这通常仍然是一个相当大而复杂的软件部分。本节讨论了构建软件以进一步使小型软件组件(如模块、库和 API)的不变性能够推理的技术。

使用应用程序框架满足服务范围的需求

如前所述,框架可以提供可重用功能的部分。一个给定的系统可能有身份验证框架、授权框架、RPC 框架、编排框架、监控框架、软件发布框架等。这些框架可以提供很大的灵活性,通常是太多的灵活性。所有可能的框架组合以及它们可以配置的方式可能会让与服务交互的工程师(应用程序和服务开发人员、服务所有者、SRE 和 DevOps 工程师)感到不知所措。

在 Google,我们发现创建更高级的框架来管理这种复杂性很有用,我们称之为应用程序框架。有时这些被称为全栈内置电池框架。应用程序框架为各个功能模块提供了一个规范的子框架集,具有合理的默认配置,并保证所有子框架可以协同工作。应用程序框架使用户无需选择和配置一组子框架。

例如,假设一个应用程序开发人员使用他们喜欢的 RPC 框架公开了一个新的服务。他们使用自己喜欢的身份验证框架设置了身份验证,但忘记配置授权和/或访问控制。从功能上看,他们的新服务似乎运行良好。但是,没有授权策略,他们的应用程序是非常不安全的。任何经过身份验证的客户端(例如系统中的每个应用程序)都可以随意调用这个新服务,违反了最小特权原则(参见第五章)。这种情况可能导致严重的安全问题,例如,想象一下,服务公开的一个方法允许调用者重新配置数据中心中的所有网络交换机!

一个应用程序框架可以通过确保每个应用程序都有有效的授权策略,并通过提供安全的默认值来避免这个问题,从而禁止所有未经明确允许的客户端。

一般来说,应用程序框架必须提供一种有见地的方式来启用和配置应用程序开发人员和服务所有者需要的所有功能,包括(但不限于)以下内容:

  • 请求分派、请求转发和截止时间传播

  • 用户输入净化和区域设置检测

  • 身份验证、授权和数据访问审计

  • 日志记录和错误报告

  • 健康管理、监控和诊断

  • 配额执行

  • 负载平衡和流量管理

  • 二进制和配置部署

  • 集成、预发布和负载测试

  • 仪表板和警报

  • 容量规划和供应

  • 处理计划的基础设施中断

应用程序框架解决了与可靠性相关的问题,如监控、警报、负载平衡和容量规划(见第十二章)。因此,应用程序框架允许跨多个部门的工程师使用相同的语言,从而增加团队之间的可理解性和共鸣。

理解复杂的数据流

许多安全属性依赖于关于在系统中流动的断言。

例如,许多 Web 服务在各种情况下使用 URL。最初,在整个系统中将 URL 表示为字符串似乎是简单而直接的。然而,应用程序的代码和库可能会做出隐含的假设,即 URL 是格式良好的,或者 URL 具有特定的方案,如https。如果可以使用违反这些假设的 URL 调用此类代码,则此类代码是不正确的(并可能存在安全漏洞)。换句话说,存在一个隐含的假设,即从不可信的外部调用者接收输入的上游代码应用了正确和适当的验证。

然而,字符串类型的值并不附带任何关于它是否表示格式良好的 URL 的明确断言。“字符串”类型本身只表明该值是一系列特定长度的字符或代码点(具体取决于实现语言的细节)。关于该值的其他属性的任何假设都是隐含的。因此,对下游代码的正确性进行推理需要理解所有上游代码,以及该代码是否实际执行所需的验证。

通过将值表示为特定数据类型,使得对流经大型复杂系统的数据属性进行推理更加容易,该类型的合同规定了所需的属性。在更易理解的设计中,您的下游代码不是以基本字符串类型的形式使用 URL,而是作为一个类型(例如作为 Java 类实现)来表示格式良好的 URL。¹⁷ 这种类型的合同可以由类型的构造函数或工厂函数强制执行。例如,Url.parse(String)工厂函数将执行运行时验证,并返回Url的实例(表示格式良好的 URL),或者对于格式不正确的值发出错误信号或抛出异常。

有了这种设计,理解消耗 URL 的代码以及其正确性是否依赖于其格式良好性,不再需要理解所有的调用者以及它们是否执行适当的验证。相反,您可以通过理解两个较小的部分来理解 URL 处理。首先,您可以独立检查Url类型的实现。您可以观察到所有类型的构造函数都确保格式良好,并且它们保证所有类型的实例符合类型的文档合同。然后,您可以分别推理出消耗Url类型值的代码的正确性,使用类型的合同(即格式良好性)作为推理的假设。

以这种方式使用类型有助于理解,因为它可以显著减少你需要阅读和验证的代码量。没有类型,你必须理解所有使用 URL 的代码,以及所有以纯字符串形式传递 URL 到该代码的代码。通过将 URL 表示为一种类型,你只需要理解Url.parse()内部的数据验证实现(以及类似的构造函数和工厂函数),以及Url的最终用途。你不需要理解其余仅传递类型实例的应用程序代码。

在某种意义上,类型的实现行为就像 TCB 一样——它完全负责“所有 URL 都是格式良好的”属性。然而,在常用的实现语言中,接口、类型或模块的封装机制通常并不代表安全边界。因此,你不能将模块的内部视为可以抵御模块外部恶意代码行为的 TCB。这是因为在大多数语言中,模块边界“外部”的代码仍然可以修改模块的内部状态(例如,通过使用反射特性或类型转换)。类型封装允许你理解模块的行为,但只有在假设周围的代码是由非恶意开发人员编写,并且代码在未被破坏的环境中执行的情况下。这实际上是一个合理的假设;通常由组织和基础设施级别的控制来确保,例如存储库访问控制、代码审查流程、服务器加固等。但如果这个假设不成立,你的安全团队将需要解决由此产生的最坏情况(参见第四部分)。

你也可以使用类型来推理更复杂的属性。例如,防止注入漏洞(如 XSS 或 SQL 注入)取决于适当验证或编码任何外部和潜在恶意的输入,这是在接收输入和将其传递给易受注入的 API 之间的某个时刻。

断言应用程序没有注入漏洞需要理解从外部输入到所谓的注入接收器(即,如果提供不够验证或编码的输入,API 容易出现安全漏洞)的所有代码和组件。在典型应用程序中,这样的数据流可能非常复杂。通常会发现数据流通过前端接收值,通过一个或多个微服务后端层,持久化在数据库中,然后稍后读取并在注入接收器的上下文中使用。在这种情况下,常见的漏洞类别是所谓的存储型 XSS漏洞,其中不受信任的输入通过持久存储达到 HTML 注入接收器(例如 HTML 模板或浏览器端 DOM API),而没有适当的验证或转义。在合理的时间范围内审查和理解大型应用程序中所有相关流的并集通常远远超出了人类的能力,即使他们配备了工具。

防止这种注入漏洞的一种有效方法是使用类型来区分已知安全用于特定注入接收上下文的值,例如 SQL 查询或 HTML 标记:¹⁸

  • SafeSqlSafeHtml等类型的构造函数和构建器 API 负责确保这些类型的所有实例在相应的接收器上下文中确实是安全的(例如,SQL 查询 API 或 HTML 渲染上下文)。这些 API 通过潜在不受信任的值的运行时验证和正确构造的 API 设计的组合来确保类型契约。构造函数还可能依赖于更复杂的库,例如完整的 HTML 验证器/净化器或应用上下文敏感的 HTML 模板系统,这些系统对插入模板的数据应用上下文敏感的转义或验证。

  • 修改接收器以接受适当类型的值。类型契约规定其值在相应的上下文中是安全的,这使得有类型的 API 在构造时是安全的。例如,当使用仅接受SafeSql类型值(而不是String)的 SQL 查询 API 时,您不必担心 SQL 注入漏洞,因为所有SafeSql类型的值都可以安全地用作 SQL 查询。

  • 接收器也可以接受基本类型的值(例如字符串),但在这种情况下,不能对接收器注入上下文中的值的安全性做出任何假设。相反,接收器 API 本身负责验证或编码数据,以确保在运行时该值是安全的。

通过这种设计,您可以支持一个断言,即整个应用程序基于对类型实现和类型安全接收器 API 的理解单独不会受到 SQL 注入或 XSS 漏洞的影响。您不需要理解或阅读转发这些类型值的任何应用程序代码,因为类型封装确保应用程序代码无法使安全相关的类型不变。您也不需要理解和审查使用类型的安全构造器创建类型实例的应用程序代码,因为这些构造器旨在确保其类型的契约,而不假设其调用者的行为。第十二章详细讨论了这种方法。

考虑 API 的可用性

考虑 API 的采用和使用对组织开发人员及其生产力的影响是一个好主意。如果 API 使用起来很麻烦,开发人员将会缓慢或不愿采用它们。构造安全的 API 有双重好处,使您的代码更易理解,并允许开发人员专注于应用程序的逻辑,同时还可以自动将安全方法构建到组织的文化中。

幸运的是,通常可以设计库和框架,使得构造安全的 API 对开发人员是一个净好处,同时也促进了安全和可靠性的文化。作为采用您的安全 API 的回报,理想情况下遵循他们已经熟悉的已建立模式和习惯语,您的开发人员将获得不需要负责确保与 API 使用相关的安全不变量的好处。

例如,上下文自动转义的 HTML 模板系统完全负责正确验证和转义插入模板的所有数据。这是整个应用程序的一个强大的安全不变量,因为它确保任何这样的模板的渲染都不会导致 XSS 漏洞,无论模板被喂入什么(潜在恶意的)数据。

同时,从开发者的角度来看,使用上下文自动转义的 HTML 模板系统就像使用常规的 HTML 模板一样-您提供数据,模板系统将其插入到 HTML 标记中的占位符中-只是您不再需要担心添加适当的转义或验证指令。

示例:安全的加密 API 和 Tink 加密框架

加密代码特别容易出现微妙的错误。许多加密原语(如密码和哈希算法)具有灾难性的故障模式,非专家很难识别。例如,在某些情况下,加密与身份验证不正确地结合在一起(或根本没有使用身份验证),只能观察服务请求是否失败或被接受的攻击者仍然可以利用服务作为所谓的“解密神谕”并恢复加密消息的明文。(21)一个不熟悉攻击技术的非专家几乎没有注意到这个缺陷的机会:加密数据看起来完全不可读,而代码使用的是标准的、推荐的、安全的密码,如 AES。然而,由于名义上安全密码的微妙错误用法,加密方案是不安全的。

根据我们的经验,涉及密码原语的代码如果不是由经验丰富的密码学家开发和审查的,通常会存在严重的缺陷。正确使用密码学确实非常困难。

我们在许多安全审查项目中的经验促使谷歌开发了 Tink:一个使工程师能够在其应用程序中安全使用密码学的库。Tink 源于我们与谷歌产品团队合作的丰富经验,修复密码实现中的漏洞,并提供了简单的 API,使没有密码学背景的工程师可以安全使用。

Tink 减少了常见的加密陷阱,并提供了易于正确使用且难以滥用的安全 API。以下原则指导了 Tink 的设计和开发:

默认安全

该库提供了一个难以被滥用的 API。例如,API 不允许在 Galois 计数器模式中重用 nonce,这是一个相当常见但微妙的错误,在RFC 5288中特别指出,因为它允许导致 AES-GCM 模式的完整认证失败的认证密钥恢复。多亏了Project Wycheproof,Tink 重用了经过验证和经过充分测试的库。

易用性

该库具有简单易用的 API,因此软件工程师可以专注于所需的功能,例如实现块和流的带关联数据的认证加密(AEAD)原语。

可读性和可审计性

功能在代码中清晰可读,Tink 保持对使用的加密方案的控制。

可扩展性

可以很容易地通过密钥管理器的注册表添加新功能、方案和格式。

敏捷性

Tink 内置了密钥轮换,并支持淘汰过时/损坏的方案。

互操作性

Tink 在许多语言和平台上都可用。

Tink 还提供了一个密钥管理的解决方案,与Cloud Key Management Service (KMS)AWS Key Management ServiceAndroid Keystore集成。许多密码库都可以轻松地将私钥存储在磁盘上,并且更容易地将私钥添加到您的源代码中——这是一种强烈不建议的做法。即使您运行“keyhunt”和“password hunt”活动来查找和清除代码库和存储系统中的秘密,也很难完全消除与密钥管理相关的事件。相比之下,Tink 的 API 不接受原始密钥材料。相反,API 鼓励使用密钥管理服务。

谷歌使用 Tink 来保护许多产品的数据,现在它是谷歌内部和与第三方通信时保护数据的推荐库。通过提供具有良好理解属性(如“经过身份验证的加密”)的抽象,支持良好设计的实现,它允许安全工程师专注于加密代码的更高级别方面,而不必担心底层加密原语的攻击。

然而,需要注意的是,Tink 无法防止加密代码中的高级设计错误。例如,没有足够密码学背景的软件开发人员可能会选择通过散列来保护敏感数据。如果问题数据来自一个(在密码学术语中)相对较小的集合,比如信用卡或社会安全号码,这是不安全的。在这种情况下使用密码散列,而不是经过身份验证的加密,是一个设计级错误,它在 Tink 的 API 之上表现出来。安全审查人员不能因为代码使用 Tink 而不是其他加密库就得出这样的结论,即这样的错误在应用程序中不存在。

软件开发人员和审查人员必须注意理解库或框架保证和不保证的安全性和可靠性属性。Tink 可以防止许多可能导致低级加密漏洞的错误,但不能防止基于使用错误的加密 API(或根本不使用加密)的错误。同样,一个安全构建的 Web 框架可以防止 XSS 漏洞,但不能防止应用程序业务逻辑中的安全漏洞。

结论

可靠性和安全性从根本上和紧密地与可理解系统相互关联。

尽管“可靠性”有时被视为“可用性”的同义词,但这个属性实际上意味着维护系统的所有关键设计保证——包括可用性、耐久性和安全不变量等。

我们构建可理解系统的主要指导是使用具有清晰和受限目的的组件。其中一些组件可能构成其受信任的计算基础,因此集中负责解决安全风险。

我们还讨论了强制执行理想属性的策略,例如安全不变量、架构弹性和数据耐久性,以及这些组件之间的关系。这些策略包括以下内容:

  • 窄、一致、类型化的接口

  • 一致和谨慎地实现身份验证、授权和会计策略

  • 将身份清晰地分配给活动实体,无论它们是软件组件还是人类管理员

  • 应用程序框架库和数据类型封装安全不变量,以确保组件始终遵循最佳实践

当您最关键的系统行为发生故障时,系统的可理解性可能是短暂事件和长期灾难之间的区别。SRE 必须了解系统的安全不变量才能完成他们的工作。在极端情况下,他们可能不得不在安全事件期间使服务脱机,以牺牲可用性换取安全性。

¹ 自动模糊测试,特别是如果结合仪器和覆盖指导,有时可以探索更大比例的可能行为。这在第十三章中有详细讨论。

² 例如SANS, MITRE, 和OWASP发布的内容。

³ 参见 Murray, Toby, and Paul van Oorschot. 2018. “BP: Formal Proofs, the Fine Print and Side Effects.” Proceedings of the 2018 IEEE Cybersecurity Development Conference: 1–10. doi:10.1109/SecDev.2018.00009.

⁴ 参见 Klein, Gerwin 等人。2014 年。《Comprehensive Formal Verification of an OS Microkernel》。ACM 计算机系统交易 32(1):1-70。doi:10.1145/2560537。

⁵ 例如,参见 Erbsen, Andres 等人。2019 年。《Simple High-Level Code for Cryptographic Arithmetic—With Proofs, Without Compromises》。2019 年 IEEE 安全与隐私研讨会论文集:73-90。doi:10.1109/SP.2019.00005。另一个例子,请参见 Chudnov, Andrey 等人。2018 年。《Continuous Formal Verification of Amazon s2n》。第 30 届国际计算机辅助验证会议论文集:430-446。doi:10.1007/978-3-319-96142-2_26。

⁶ 参见 Denning, Peter J. 1968 年。《Thrashing: Its Causes and Prevention》。1968 年秋季联合计算机会议论文集:915-922。doi:10.1145/1476589.1476705。

⁷ 有关更多信息,请参见SRE workbook 中的第十一章

⁸ 通常情况下,系统中有多个身份子系统。例如,系统可能有一个用于内部微服务的身份子系统,另一个用于人类管理员的身份子系统。

⁹ 有关 Borg 的更多信息,请参见 Verma, Abhishek 等人。2015 年。《Large-Scale Cluster Management at Google with Borg》。欧洲计算机系统会议论文集(EuroSys)https://oreil.ly/zgKsd

¹⁰ Anderson, Ross J. 2008 年。《Security Engineering: A Guide to Building Dependable Distributed Systems》。霍博肯,新泽西州:Wiley。

¹¹ 这是真的,除非用户 A 是根用户,以及其他一些特定条件,例如涉及共享内存,或者像 Linux 能力这样的机制赋予根用户特定的权限。

¹² 为了使示例简单化,图 6-2 没有显示与外部服务提供商的连接。

¹³ 在实际设计中,您可能会使用一个单独的数据库,其中有多组表,工作负载身份已被授予适当的访问权限。这样可以实现对数据的访问分离,同时允许数据库确保所有表之间的数据一致性属性,例如购物车内容和目录商品之间的外键约束。

¹⁴ Zalewski, Michał。2012 年。《The Tangled Web: A Guide to Securing Modern Web Applications》。旧金山,加利福尼亚州:No Starch Press。

¹⁵ 参见 Zalewski,《The Tangled Web》。

¹⁶ 我们需要配置我们的 Web 服务器,以便支付前端可以在例如https://widgets.example.com/checkout上访问。

¹⁷ 或者更一般地说,一个满足特定相关属性的 URL,比如具有特定方案。

¹⁸ 参见 Kern, Christoph。2014 年。《Securing the Tangled Web》。ACM 通信 57(9):38-47。doi:10.1145/2643134。

¹⁹ 参见 Samuel, Mike, Prateek Saxena 和 Dawn Song。2011 年。《Context-Sensitive Auto-Sanitization in Web Templating Languages Using Type Qualifiers》。第 18 届 ACM 计算机与通信安全会议论文集:587-600。doi:10.1145/2046707.2046775。

²⁰ 正如前面所指出的,这种断言仅在假设应用程序的整个代码库都是非恶意的情况下才成立。换句话说,类型系统依赖于在代码库的其他地方发生非恶意错误的情况下维护不变量,但不能抵御积极恶意的代码,例如使用语言的反射 API 修改类型的私有字段。您可以通过额外的安全机制,如代码审查、访问控制和源代码存储库级别的审计跟踪来解决后者。

²¹ 参见 Rizzo, Juliano, and Thai Duong. 2010. “Practical Padding Oracle Attacks.” Proceedings of the 4th USENIX Conference on Offensive Technologies: 1–8. https://oreil.ly/y-OYm

第七章:面向不断变化的环境的设计

原文:7. Design for a Changing Landscape

译者:飞龙

协议:CC BY-NC-SA 4.0

Maya Kaczorowski、John Lunney 和 Deniz Pecel

与 Jen Barnason、Peter Duff 和 Emily Stark 合作

“变化是唯一不变的”是一个至理名言,对于软件来说绝对是如此:随着我们每年使用的设备数量(和种类)的增加,图书馆和应用程序的漏洞数量也在增加。任何设备或应用程序都有可能受到远程利用、数据泄露、僵尸网络接管或其他引人注目的情景的影响。

与此同时,用户和监管机构对安全和隐私的期望不断提高,要求实现更严格的控制,如企业特定的访问限制和认证系统。

为了应对这种不断变化的漏洞、期望和风险,您需要能够频繁快速地改变您的基础设施,同时保持高度可靠的服务——这并不容易。实现这种平衡通常归结为决定何时以及多快地推出变更。

安全变更类型

您可能会做出许多种改变,以改善您的安全姿态或安全基础设施的弹性,例如:

  • 对安全事件做出的变更(见第十八章)

  • 对新发现的漏洞做出的变更

  • 产品或功能变更

  • 出于内部动机改善您的安全姿态的变更

  • 外部动机的变更,如新的监管要求

一些类型的出于安全考虑的变更需要额外的考虑。如果你正在推出一个可选功能作为迈向强制性的第一步,你需要收集足够的早期采用者反馈,并彻底测试你的初始工具。

如果您正在考虑对依赖项进行更改,例如供应商或第三方代码依赖项,您需要确保新解决方案符合您的安全要求。

设计您的变更

安全变更受到与任何其他软件变更相同的基本可靠性要求和发布工程原则的约束;有关更多信息,请参阅本书中的第四章和 SRE 书中的第八章](https://landing.google.com/sre/sre-book/chapters/release-engineering/)。推出安全变更的时间表可能有所不同(请参阅[“不同的变更:不同的速度,不同的时间表”),但整体流程应遵循相同的最佳实践。

所有变更都应具有以下特征:

渐进式

进行尽可能小而独立的变更。避免将变更与不相关的改进(如重构代码)联系在一起的诱惑。

记录

描述您的变更的“如何”和“为什么”,以便他人能够理解变更和推出的相对紧急性。您的文档可能包括以下任何或所有内容:

  • 要求

  • 受影响的系统和团队

  • 从概念验证中学到的经验

  • 决策的基础(以防需要重新评估计划)

  • 所有涉及的团队的联系点

测试

使用单元测试和(在可能的情况下)集成测试来测试您的安全变更(有关测试的更多信息,请参阅第十三章)。完成同行评审,以获得变更在生产环境中能够正常工作的信心。

隔离

使用功能标志来隔离彼此的变更,并避免发布不兼容性;有关更多信息,请参阅SRE 工作手册中的第十六章。当关闭功能时,底层二进制应该不会表现出任何行为上的变化。

合格的

使用您正常的二进制发布流程推出您的变更,在接收生产或用户流量之前,通过资格的各个阶段。

分阶段

逐步推出你的变更,并进行金丝雀测试的仪器化。你应该能够在变更前后看到行为上的差异。

这些做法建议采取“缓慢而稳定”的推出方式。根据我们的经验,速度和安全之间的有意的权衡是值得的。你不希望通过推出一个有问题的变更来冒险制造一个更大的问题,比如广泛的停机或数据丢失。

使变更更容易的架构决策

如何设计你的基础架构和流程,以应对你将面临的不可避免的变化?在这里,我们讨论了一些策略,使你能够灵活地调整系统并推出变化,同时也有助于建立安全可靠的文化(在第二十一章中讨论)。

保持依赖项最新并频繁重新构建

确保你的代码指向代码依赖的最新版本有助于使你的系统不太容易受到新的漏洞的影响。保持对依赖项的引用最新对于经常更改的开源项目尤为重要,比如 OpenSSL 或 Linux 内核。许多大型开源项目都有建立良好的安全漏洞响应和修复计划,澄清了新发布是否包含关键安全补丁,并将修复补丁迁移到受支持的版本。如果你的依赖项是最新的,你很可能可以直接应用关键补丁,而不需要合并一大堆变更或应用多个补丁。

新发布及其安全补丁在你的环境中不会生效,直到你重新构建。频繁重新构建和部署你的环境意味着当你需要时你将准备好推出新版本,并且紧急推出可以获取最新的变更。

使用自动化测试频繁发布

基本的 SRE 原则建议定期切割和推出发布,以促进紧急变更。通过将一个大的发布分割成许多较小的发布,你确保每个发布包含的变更更少,因此更不太可能需要回滚。有关这个主题的更深入探讨,请参见 SRE workbook 中的“善行循环”(https://landing.google.com/sre/workbook/chapters/canarying-releases/#the-virtuous-cycle-of-ci-cd)。

当每个发布包含更少的代码变更时,更容易理解发生了什么变化并找出潜在问题。当你需要推出安全变更时,你可以更加自信地预期结果。

为了充分利用频繁的发布,自动化它们的测试和验证。这样可以自动推送良好的发布,同时防止不足的发布进入生产环境。自动化测试还可以在需要推出修复以防止关键漏洞时给你额外的信心。

同样,通过使用容器²和微服务³,你可以减少需要修补的表面积,建立定期发布流程,并简化你对系统漏洞的理解。

使用容器

容器将应用程序所需的二进制文件和库与底层主机操作系统解耦。因为每个应用程序都打包了自己的依赖和库,所以主机操作系统不需要包含它们,因此可以更小。因此,应用程序更具可移植性,你可以独立地对其进行安全保护。例如,你可以在主机操作系统中修补内核漏洞,而无需更改应用程序容器。

容器的设计是不可变的,这意味着它们在部署后不会改变——而不是通过 SSH 进入机器,你重新构建和部署整个镜像。因为容器的寿命很短,它们经常被重新构建和部署。

与其对活动容器进行修补,不如对容器注册表中的镜像进行修补。这意味着您可以将一个完全打了补丁的容器镜像作为一个单元进行部署,使补丁的部署过程与您(非常频繁的)代码部署过程相同,包括监控、金丝雀发布和测试。因此,您可以更频繁地进行补丁。

随着这些变化应用到每个任务中,系统会无缝地将服务流量转移到另一个实例;参见《SRE 书》第七章中的“案例研究 4:在共享平台上运行数百个微服务”。您可以通过蓝/绿部署实现类似的结果,并在修补时避免停机;参见《SRE 工作手册》第十六章

您还可以使用容器来检测和修补新发现的漏洞。由于容器是不可变的,它们提供内容可寻址性。换句话说,您实际上知道在您的环境中运行什么,例如,您部署了哪些镜像。如果您之前部署了一个完全打了补丁的镜像,恰好容易受到新漏洞的影响,您可以使用您的注册表来识别易受影响的版本并应用补丁,而不是直接扫描您的生产集群。

为了减少这种临时修补的需求,您应该监视生产环境中运行的容器的年龄,并定期重新部署,以确保旧容器不再运行。同样,为了避免重新部署较旧的未打补丁的镜像,您应该强制执行只有最近构建的容器才能在生产环境中部署。

使用微服务

理想的系统架构易于扩展,提供对系统性能的可见性,并允许您管理基础架构中服务之间的每个潜在瓶颈。使用微服务架构,您可以将工作负载分割为更小、更易管理的单元,以便进行维护和发现。因此,您可以独立扩展、负载平衡和在每个微服务中执行部署,这意味着您可以更灵活地进行基础架构更改。由于每个服务都单独处理请求,您可以独立和顺序地使用多种防御措施,提供深度防御(见“深度防御”)。

微服务还自然地促进了有限或零信任的网络,这意味着您的系统并不会因为一个服务位于同一网络中就本能地信任它(见第六章)。与使用基于边界的安全模型不同,该模型将不受信任的外部流量与受信任的内部流量进行区分,微服务使用更多元化的信任概念在边界内部:内部流量可能具有不同级别的信任。当前的趋势是朝着越来越分段化的网络发展。随着对单一网络边界(如防火墙)的依赖被消除,网络可以通过服务进一步分段。在极端情况下,网络可以实现微服务级别的分段,服务之间没有固有的信任关系。

使用微服务的一个次要后果是安全工具的收敛,因此一些流程、工具和依赖关系可以在多个团队之间重复使用。随着架构的扩展,整合您的努力以解决共享的安全需求可能是有意义的,例如,使用常见的加密库或常见的监控和警报基础设施。这样,您可以将关键的安全服务拆分为单独的微服务,由少数负责方更新和管理。重要的是要注意,实现微服务架构的安全优势需要克制,以确保服务尽可能简单,同时仍保持所需的安全属性。

使用微服务架构和开发流程允许团队在开发和部署生命周期的早期阶段解决安全问题——在进行更改成本较低的时候——以标准化的方式。因此,开发人员可以在花费较少时间处理安全性的同时实现安全的结果。

示例:谷歌的前端设计

谷歌的前端设计使用微服务提供弹性和深度防御。将前端和后端分开成不同的层有许多优势:Google Front End(GFE)作为大多数谷歌服务的前端层,实现为微服务,因此这些服务不直接暴露在互联网上。GFE 还终止了传入 HTTP(S)、TCP 和 TLS 代理的流量;提供了 DDoS 攻击对策;并将流量路由和负载平衡到谷歌云服务。

GFE 允许独立分区前端和后端服务,这在可伸缩性、可靠性、灵活性和安全性方面都有好处:

  • 全局负载平衡有助于在 GFE 和后端之间移动流量。例如,我们可以在数据中心故障期间重定向流量,减少缓解时间。

  • 后端和前端层内部可以有几个层。因为每个层都是一个微服务,我们可以对每个层进行负载平衡。因此,相对容易增加容量,进行一般性更改或对每个微服务应用快速更改。

  • 如果服务过载,GFE 可以作为缓解点,在负载达到后端之前丢弃或吸收连接。这意味着微服务架构中并非每个层都需要自己的负载保护。

  • 新协议和安全要求的采用相对简单。即使一些后端还没有准备好,GFE 也可以处理 IPv6 连接。GFE 还通过作为各种常见服务的终止点来简化证书管理,比如 SSL。例如,当发现 SSL 重新协商的实现中存在漏洞时,GFE 的控制限制了这些重新协商,保护了其后面的所有服务。快速的应用层传输安全加密也说明了微服务架构如何促进变化的采用:谷歌的安全团队将 ALTS 库集成到其 RPC 库中,以处理服务凭据,从而实现了广泛的采用,而对个别开发团队的负担不大。

在今天的云世界中,您可以通过使用微服务架构、构建安全控制层和使用服务网格来实现类似于此处描述的好处。例如,您可以将请求处理与管理请求处理的配置分开。行业将这种有意的分离称为数据平面(请求)和控制平面(配置)。在这种模型中,数据平面提供系统中的实际数据处理,通常处理负载平衡、安全性和可观察性。控制平面为数据平面服务提供策略和配置,从而提供可管理和可扩展的控制表面。

不同的更改:不同的速度,不同的时间轴

并非所有更改都在相同的时间轴上或以相同的速度发生。有几个因素影响您可能希望进行更改的速度:

严重性

每天都会发现漏洞,但并非所有漏洞都是关键的、正在被积极利用的,或者适用于您特定的基础设施。当您遇到这种情况时,您可能希望尽快发布补丁。加速的时间表会造成混乱,并更有可能破坏系统。有时速度是必要的,但一般来说,变化发生得越慢越安全,这样您就可以确保足够的产品安全性和可靠性。(理想情况下,您可以独立应用关键安全补丁,这样您就可以快速应用补丁,而不会不必要地加速其他正在进行的推出。)

依赖系统和团队

一些系统变化可能取决于其他团队,他们需要在推出之前实现新政策或启用特定功能。您的变化也可能取决于外部方面,例如,如果您需要从供应商那里接收补丁,或者如果客户需要在您的服务器之前打补丁。

敏感性

您的变化的敏感性可能会影响您何时可以将其部署到生产环境中。改进组织整体安全状况的非必要变化并不一定像关键补丁那样紧急。您可以更逐渐地推出这种非必要的变化,例如逐个团队。根据其他因素,进行变化可能不值得冒风险——例如,您可能不希望在关键的生产时间窗口内推出非紧急变化,比如假日购物活动期间,那里的变化通常是受严格控制的。

截止日期

一些变化有一个有限的截止日期。例如,监管变化可能有指定的合规日期,或者您可能需要在披露漏洞之前(请参见下面的侧边栏)应用补丁。

确定特定变化速度没有硬性规定——一个组织可能需要快速进行配置更改和推出,而另一个组织可能需要数月时间。虽然单个团队可能能够按照特定时间表进行特定变化,但您的组织可能需要很长时间才能完全采纳这种变化。

在接下来的几节中,我们将讨论变化的三种不同时间范围,并举例说明谷歌的情况:

  • 对新安全漏洞的短期变化反应

  • 中期变化,新产品采用可能会逐渐发生

  • 出于监管原因的长期变化,谷歌必须构建新系统以实现变化

短期变化:零日漏洞

新发现的漏洞通常需要短期行动。零日漏洞是已知至少部分攻击者知道的漏洞,但尚未公开披露或被针对的基础设施提供者发现。通常,补丁要么尚未可用,要么尚未广泛应用。

有多种方法可以了解可能影响您环境的新漏洞,包括定期代码审查、内部代码扫描(参见“清理您的代码”)、模糊测试(参见“模糊测试”)、外部扫描如渗透测试和基础设施扫描,以及漏洞赏金计划。

在短期变化的背景下,我们将重点放在谷歌在零日得知漏洞的情况。尽管谷歌通常参与封锁漏洞响应,例如在开发补丁时,对于零日漏洞的短期变化是行业中大多数组织的常见行为。

注意

尽管零日漏洞受到了很多关注(无论是外部还是内部),但它们不一定是攻击者最经常利用的漏洞。在处理当天零日漏洞之前,请确保您已经为近年来的“热门”漏洞打了补丁。

当你发现一个新的漏洞时,要对其进行分类,以确定其严重程度和影响。例如,允许远程代码执行的漏洞可能被认为是关键的。但对你的组织的影响可能非常难以确定:哪些系统使用这个特定的二进制文件?受影响的版本是否部署在生产环境中?在可能的情况下,你还需要建立持续的监控和警报,以确定漏洞是否正在被积极利用。

要采取行动,你需要获取一个补丁——一个应用了修复的受影响软件包或库的新版本。首先要验证补丁是否真的解决了漏洞。使用一个有效的利用工具来做这件事可能是有用的。但要注意,即使你无法使用利用工具触发漏洞,你的系统可能仍然是有漏洞的(请记住,没有证据并不意味着没有证据)。例如,你应用的补丁可能只解决了一个更大类别的漏洞中的一个可能的利用。

一旦验证了你的补丁,就将其推出——最好是在测试环境中。即使在加速的时间表上,补丁也应该像任何其他生产变更一样逐渐推出——使用相同的测试、金丝雀发布和其他工具——大约需要几个小时或几天的时间。逐步推出可以让你及早发现潜在问题,因为补丁可能会对你的应用产生意想不到的影响。例如,使用你不知道的 API 的应用可能会影响性能特征或引起其他错误。

有时你无法直接修复漏洞。在这种情况下,最好的做法是通过限制或限制对易受攻击的组件的访问来减轻风险。这种减轻可能是临时的,直到补丁可用,或者是永久的,如果你无法将补丁应用到你的系统上——例如,因为性能要求。如果已经有适当的减轻措施来保护你的环境,你甚至可能不需要采取进一步的行动。

有关事件响应的更多细节,请参见第十七章。

示例:Shellshock

2014 年 9 月 24 日早上,谷歌安全团队得知了一个公开披露的、可以远程利用的漏洞,这个漏洞在shell上轻松地允许在受影响的系统上执行代码。漏洞披露很快就被野外利用所跟随,从同一天开始。

原始报告有一些混乱的技术细节,并没有清楚地说明对讨论这个问题的禁运的状态。这份报告,再加上几个类似漏洞的迅速发现,导致了对攻击的性质和可利用性的困惑。谷歌的事件响应团队启动了其黑天鹅协议,以应对一个特殊的漏洞,并协调了大规模的响应来做以下事情:⁸

  • 确定范围内的系统和每个系统的相对风险级别

  • 内部向所有可能受到影响的团队通报

  • 尽快修补所有有漏洞的系统

  • 外部向合作伙伴和客户通报我们的行动,包括补救计划

我们在公开披露之前不知道这个问题,所以我们将其视为需要紧急减轻的零日漏洞。在这种情况下,bash 的修补版本已经可用。

团队评估了不同系统的风险,并相应地采取了行动。

  • 我们认为大量的谷歌生产服务器风险较低。这些服务器很容易通过自动化发布进行修补。一旦服务器经过足够的验证和测试,我们会比通常更快地对其进行修补,而不是长时间分阶段进行。

  • 我们认为大量的谷歌工作站风险更高。幸运的是,这些工作站很容易快速修补。

  • 少量非标准服务器和继承基础设施被认为存在高风险,需要手动干预。我们向每个团队发送通知,详细说明他们需要采取的后续行动,这使我们能够将努力扩展到多个团队,并轻松跟踪进展。

与此同时,团队开发了软件来检测谷歌网络范围内的易受攻击系统。我们使用这个软件完成了剩余的补丁工作,并将这个功能添加到了谷歌的标准安全监控中。

我们在这次应对工作中做得好(和不好)提供了许多其他团队和组织的教训:

  • 尽可能标准化软件分发,这样补丁就成了简单的选择。这还要求服务所有者理解和接受选择非标准、不受支持分发的风险。服务所有者应负责维护和打补丁替代选项。

  • 使用公共分发标准——理想情况下,你需要推出的补丁已经是正确的格式。这样,你的团队可以开始快速验证和测试补丁,而不需要重新制定补丁以解决你特定的环境。

  • 确保你可以加速推动紧急变更的机制,比如零日漏洞。这个机制应该允许在向受影响系统全面推出之前进行比平时更快的验证。我们不一定建议你跳过验证你的环境仍然正常运行的步骤——你必须在需要减轻利用的需求之间平衡这一步骤。

  • 确保你有监控来跟踪你的推出进度,识别未打补丁的系统,以及识别你仍然存在漏洞的地方。如果你已经有工具来识别漏洞是否正在被利用,它可能会帮助你决定根据你当前的风险放缓或加速。

  • 尽早准备外部沟通,尽可能在应对工作的早期。当媒体要求回应时,你不希望在内部公关批准中被拖延。

  • 提前起草可重复使用的事件或漏洞响应计划(见第十七章),包括外部沟通的语言。如果你不确定你需要什么,可以从以前事件的事后总结开始。

  • 了解哪些系统是非标准的或需要特别关注。通过跟踪离群值,你将知道哪些系统可能需要积极通知和补丁协助。(如果你按照我们在第一条建议的标准化软件分发,离群值应该是有限的。)

中期变化:改善安全状况

安全团队经常实现改变以提高环境的整体安全状况并减少风险。这些积极的改变是由内部和外部的要求和截止日期驱动的,很少需要突然推出。

在规划安全状况变更时,你需要弄清楚哪些团队和系统受到影响,并确定最佳的开始地点。遵循“设计你的变更”中概述的 SRE 原则,制定逐步推出的行动计划。每个阶段都应包括必须在进入下一个阶段之前满足的成功标准。

受安全变更影响的系统或团队不能必然表示为推出的百分比。相反,你可以根据受影响的人和需要进行的变更来分阶段推出。

在受影响的人员方面,逐组推出您的变更,其中一组可能是一个开发团队、系统或一组最终用户。例如,您可以开始向经常在外出的用户(如销售团队)推出设备策略的变更。这样可以让您快速测试最常见的情况,并获得真实的反馈。在推出人员方面有两种相互竞争的哲学:

  • 从最容易的用例开始,这样你将获得最大的动力并证明价值。

  • 从最困难的用例开始,这样你将找到最多的错误和边缘情况。

当您仍在寻求组织的支持时,从最容易的用例开始是有意义的。如果您在一开始就得到了领导支持和投资,那么尽早发现实现错误和痛点就更有价值。除了组织上的考虑,您还应考虑哪种策略将在短期和长期内带来最大的风险降低。在所有情况下,成功的概念验证有助于确定如何最好地前进。进行变更的团队还必须经历这一过程,“吃自己的狗粮”,以便他们了解用户体验。

您可能还可以逐步推出变更本身。例如,您可以逐渐实现更严格的要求,或者变更最初可以选择加入,而不是强制性的。在可能的情况下,您还应考虑在警报或审计模式下进行变更的试运行,然后再切换到执行模式——这样,用户可以在变更强制执行之前体验到他们将受到的影响。这样可以帮助您找到错误地确定为受影响范围内的用户或系统,以及对于他们来说,达到合规性将特别困难的用户或系统。

示例:使用 FIDO 安全密钥进行强大的双因素认证

钓鱼是 Google 的一个重大安全问题。尽管我们已广泛实现了使用一次性密码(OTPs)的双因素认证,但 OTP 仍然容易在钓鱼攻击中被截取。我们假设即使是最复杂的用户也是出于善意并且做好了应对钓鱼的准备,但仍然容易受到由于混乱的用户界面或用户错误而导致的账户接管。为了解决这一风险,从 2011 年开始,我们调查和测试了几种更强大的双因素认证(2FA)方法。我们最终选择了通用双因素(U2F)硬件安全令牌,因为它们具有安全性和可用性属性。为了为 Google 庞大的全球分布的员工群体实现安全密钥,需要构建定制集成并协调大规模注册。

评估潜在解决方案和我们最终的选择是变更过程的一部分。一开始,我们定义了安全、隐私和可用性要求。然后,我们与用户一起验证潜在的解决方案,以了解正在发生的变化,获得真实的反馈,并衡量变化对日常工作流程的影响。

除了安全和隐私要求,潜在的解决方案还必须满足可用性要求,以促进无缝采用,这对于建立安全和可靠性文化至关重要(见第二十一章)。双因素认证需要简单易用——快速和“无脑”到足以使错误或不安全地使用它变得困难。这一要求对于 SREs 尤为重要——在发生故障时,双因素认证不能减慢响应流程。此外,内部开发人员需要能够通过简单的 API 轻松地将双因素认证解决方案集成到他们的网站中。在理想的可用性要求方面,我们希望找到一个高效的解决方案,适用于跨多个帐户的用户,并且不需要额外的硬件,而且在物理上轻松、易学、易于找回。

在评估了几种选择后,我们共同设计了FIDO 安全密钥。尽管这些密钥并不符合我们所有的理想要求,但在最初的试点中,安全密钥减少了总认证时间,并且认证失败率可以忽略不计。

一旦我们有了解决方案,就必须向所有用户推出安全密钥,并在谷歌全面停用 OTP 支持。我们从 2013 年开始推出安全密钥。为了确保广泛采用,注册是自助的:

  • 最初,许多用户自愿选择安全密钥,因为这些密钥比现有的 OTP 工具更简单易用——他们不必从手机输入代码或使用物理 OTP 设备。用户获得了可以留在笔记本电脑的 USB 驱动器中的“纳米”安全密钥。

  • 要获得安全密钥,用户可以前往任何办公室的任何 TechStop 位置。 (向全球办公室分发设备很复杂,需要法律团队进行出口合规和海关进口要求。)

  • 用户通过自助注册网站注册他们的安全密钥。TechStops 为第一批采用者和需要额外帮助的人提供了帮助。用户需要在首次认证时使用现有的 OTP 系统,因此密钥在首次使用时是可信的(TOFU)。

  • 用户可以注册多个安全密钥,这样他们就不必担心丢失密钥。这种方法增加了整体成本,但与我们的目标强烈一致,即不让用户携带额外的硬件。

团队在推出过程中确实遇到了一些问题,比如固件过时。在可能的情况下,我们以自助方式解决这些问题,例如允许用户自行更新安全密钥固件。

使安全密钥对用户可访问只是问题的一半。使用 OTP 的系统也需要转换为使用安全密钥。2013 年,许多应用程序并不原生支持这一最近开发的技术。团队首先专注于支持谷歌员工日常使用的应用程序,如内部代码审查工具和仪表板。在不支持安全密钥的情况下(例如某些硬件设备证书管理和第三方 Web 应用程序),谷歌直接与供应商合作请求并添加支持。然后我们必须处理长尾应用程序。由于所有 OTP 都是集中生成的,我们可以通过跟踪发出 OTP 请求的客户端来确定下一个目标应用程序。

2015 年,团队专注于完成推出并停用 OTP 服务。当用户使用 OTP 而不是安全密钥时,我们会发送提醒,并最终通过 OTP 阻止访问。尽管我们已经处理了大部分 OTP 应用需求,但仍然存在一些例外情况,比如移动设备设置。对于这些情况,我们为特殊情况创建了基于 Web 的 OTP 生成器。用户需要使用他们的安全密钥验证身份,这是一个合理的故障模式,但时间负担略高。我们成功地在 2015 年完成了全公司范围内的安全密钥推出。

这一经验提供了一些普遍适用的教训,对于建立安全和可靠性文化很有相关性(参见第二十一章):

确保所选择的解决方案适用于所有用户。

至关重要的是,2FA 解决方案必须是可访问的,以便视力受损的用户不会被排除在外。

使变更易于学习,并尽可能轻松。

如果解决方案比初始情况更用户友好,这一点尤为重要!这对于您期望用户频繁执行的操作或更改尤为重要,稍微的摩擦可能会导致用户负担重大。

使变更成为自助服务,以减轻中央 IT 团队的负担。

对于影响所有用户日常活动的广泛变化,重要的是他们能够轻松地注册、注销和解决问题。

向用户提供解决方案有效并符合他们最佳利益的有形证据。

清楚地解释变化对安全和风险减少的影响,并提供他们提供反馈的机会。

尽快使政策不合规的反馈循环。

这种反馈循环可以是身份验证失败、系统错误或电子邮件提醒。让用户在几分钟或几小时内知道他们的操作与期望的政策不一致,使他们能够采取措施解决问题。

跟踪进展并确定如何解决长尾问题。

通过检查应用程序的 OTP 请求,我们可以确定下一个要关注的应用程序。使用仪表板跟踪进展,并确定是否具有类似安全属性的替代解决方案可以适用于使用情况的长尾。

长期变化:外部需求

在某些情况下,您可能需要更多时间来推出变化,例如,需要进行重大架构或系统变更的内部驱动变化,或者更广泛的行业监管变化。这些变化可能受到外部截止日期或要求的激励或限制,并可能需要数年时间来实现。

在进行大规模、多年的努力时,您需要清晰地定义并衡量与目标的进展。文档尤为关键,既可以确保您考虑了必要的设计考虑因素(参见“设计您的变化”),也可以保持连续性。今天致力于变革的个人可能会离开公司并需要移交他们的工作。保持文档与最新计划和状态的更新对于维持持续的领导支持至关重要。

为了衡量持续的进展,建立适当的仪器和仪表板。理想情况下,配置检查或测试可以自动测量变化,无需人为干预。就像您为基础设施中的代码努力实现重要的测试覆盖率一样,您应该为受变化影响的系统的合规性检查覆盖率而努力。为了有效扩展这种覆盖范围,这种仪器应该是自助的,允许团队实现变化和仪器。透明地跟踪这些结果有助于激励用户,并简化沟通和内部报告。您还应该使用这个单一的真相来源进行高管沟通,而不是重复工作。

在保持持续领导支持的情况下,在组织中进行任何大规模、长期的变化是困难的。为了持续推动动力,进行这些变化的个人需要保持积极性。建立有限的目标,跟踪进展,并展示重大影响的实例可以帮助团队完成马拉松。实现总是会有长尾,因此要找出对你的情况最有意义的策略。如果变化不是必需的(根据法规或其他原因),达到 80%或 90%的采纳率可以对降低安全风险产生可衡量的影响,因此应被视为成功。

示例:增加 HTTPS 使用率

在过去的十年中,网络上的 HTTPS 采用率大幅增加,这得益于谷歌 Chrome 团队、Let’s Encrypt和其他组织的共同努力。HTTPS 为用户和网站提供了重要的保密性和完整性保证,对于网络生态系统的成功至关重要——它现在作为 HTTP/2 的一部分是必需的。

为了推动网络上的 HTTPS 使用,我们进行了广泛的研究以制定战略,通过各种外联渠道联系网站所有者,并为他们设置了强大的激励措施来进行迁移。长期而言,整个生态系统的变化需要深思熟虑的战略和重要的规划。我们采用了数据驱动的方法来确定最佳的接触每个利益相关者群体的方式:

  • 我们收集了全球当前 HTTPS 使用情况的数据,以选择目标地区。

  • 我们调查最终用户,以了解他们如何在浏览器中看待 HTTPS UI。

  • 我们测量网站行为,以确定可以限制为 HTTPS 的网络平台功能,以保护用户隐私。

  • 我们使用案例研究来了解开发人员对 HTTPS 的担忧以及我们可以构建的工具类型。

这不是一次性的努力:我们在多年的时间里继续监测指标并收集数据,必要时调整我们的战略。例如,随着我们逐渐在几年的时间内推出 Chrome 对不安全页面的警告,我们监测用户行为遥测,以确保 UI 变化不会导致意外的负面影响(例如,留存率或与网络的互动下降)。

过度沟通是成功的关键。在每次变更之前,我们利用所有可用的外联渠道:博客、开发人员邮件列表、新闻、Chrome 帮助论坛和与合作伙伴的关系。这种方法最大化了我们的影响力,使网站所有者不会感到惊讶,因为他们被推动迁移到 HTTPS。我们还根据地区情况进行了定制化的外联工作——例如,当我们意识到日本的 HTTPS 采用率因顶级网站的采用速度缓慢而滞后时,我们特别关注了日本。

最终,我们专注于创建和强调激励措施,以提供迁移到 HTTPS 的商业原因。即使是注重安全的开发人员也很难说服他们的组织,除非他们能将迁移与商业案例联系起来。例如,启用 HTTPS 允许网站使用 Web 平台功能,如Service Worker,这是一种使离线访问、推送通知和定期后台同步成为可能的后台脚本。这些功能,限制在 HTTPS 网站上,可以提高性能和可用性,并可能直接影响企业的底线。只有当组织感到迁移到 HTTPS 与他们的业务利益一致时,他们才更愿意投入资源。

如图 7-1 所示,跨平台的 Chrome 用户现在超过 90%的时间花在 HTTPS 网站上,而以前,Windows 上的 Chrome 用户这一数字低至 70%,Android 上的 Chrome 用户为 37%。来自许多组织的无数人——网页浏览器供应商、证书颁发机构和网页发布者——都为此增长做出了贡献。这些组织通过标准机构、研究会议和对每个面临的挑战和成功的公开沟通进行协调。Chrome 在这一转变中的角色产生了关于为生态系统范围的变化做出贡献的重要经验教训:

在承诺战略之前了解生态系统。

我们的战略基于定量和定性研究,重点关注包括不同国家和不同设备上的网页开发人员和最终用户在内的广泛利益相关者。

通过过度沟通来最大化影响力。

我们使用各种外联渠道来触达最广泛的利益相关者。

将安全变化与商业激励联系起来。

组织领导者在能够看到业务原因时更愿意迁移到 HTTPS。

建立行业共识。

多个浏览器供应商和组织同时支持网络迁移到 HTTPS;开发人员将 HTTPS 视为行业趋势。

Chrome 平台上的 HTTPS 浏览时间百分比

图 7-1:Chrome 平台上的 HTTPS 浏览时间百分比

复杂性:当计划改变时

安全和 SRE 的最佳计划经常会出现问题。有许多原因可能需要加快变化或放慢变化的速度。

通常,您需要根据外部因素加快变化的速度,通常是由于正在积极利用的漏洞。在这种情况下,您可能希望加快推出以尽快修补系统。要谨慎:加快速度并破坏系统并不一定对系统的安全性和可靠性更好。考虑是否可以改变推出顺序,以更快地覆盖某些更高风险的系统,或以其他方式通过限制操作的速率或将特别关键的系统下线来阻止攻击者的访问。

您也可以决定放慢变化的速度。这种方法通常是由于补丁存在问题,比如高于预期的错误率或其他推出失败。如果放慢变化的速度不能解决问题,或者在不对系统或用户造成负面影响的情况下无法完全推出,那么回滚、调试问题,然后再次推出是一种痛苦但谨慎的方法。您也可以根据更新的业务需求放慢变化的速度,例如内部截止日期的变更或行业标准的延迟。(你是什么意思,TLS 1.0 仍在使用?!)

在最理想的情况下,您在开始实现之前就改变了计划。作为回应,您只需要制定新计划!以下是更改计划的一些潜在原因,以及相应的策略:

您可能需要根据外部因素推迟变化。

如果你不能在禁令解除后立即开始打补丁(参见[“不同的变化:不同的速度,不同的时间表”](#different_changes_different_speedscomma)),与漏洞团队合作,看看是否有其他系统处于您的情况,并且是否可能改变时间表。无论如何,确保您已准备好与受影响的用户通信的计划。

你可能需要根据公开公告加快变化的速度。

对于一个受禁令的漏洞,您可能需要等待公开公告才能打补丁。如果公告泄露,如果有人公开利用漏洞,或者漏洞在野外被利用,您的时间表可能会改变。在这种情况下,您应该尽早开始打补丁。您应该在每个阶段都有一个行动计划,以应对禁令被打破时该怎么办。

你可能不会受到严重影响。

如果一个漏洞或变化主要影响公共面向网络的服务,并且您的组织拥有非常有限数量的此类服务,那么您可能不需要急于修补整个基础设施。修补受影响的部分,并减缓您对系统其他区域应用补丁的速度。

你可能依赖外部方。

大多数组织依赖第三方分发修补程序包和漏洞图像,或依赖软件和硬件交付作为基础设施变更的一部分。如果修补的操作系统不可用,或者您需要的硬件已经订购完毕,那么您可能无能为力。您可能不得不比最初计划的时间晚开始变化。

示例:范围扩大——心脏出血

2011 年 12 月,SSL/TLS 的心跳功能被添加到 OpenSSL 中,同时还出现了一个未被认识的错误,允许服务器或客户端访问另一个服务器或客户端的 64 KB 私有内存。2014 年 4 月,这个错误被谷歌员工尼尔·梅塔和 Codenomicon(一家网络安全公司)的一名工程师共同发现,并向 OpenSSL 项目报告。OpenSSL 团队提交了代码更改以修复错误,并制定了公开披露计划。出人意料的是,Codenomicon 发布了公告,并启动了解释性网站heartbleed.com。这是第一次使用巧妙的名称和标志,引起了意外的大规模媒体关注。

通过提前获得补丁(受到禁运限制)并在计划的公开披露之前,谷歌基础设施团队已经悄悄地修补了一小部分直接处理 TLS 流量的外部系统。然而,没有其他内部团队知道这个问题。

一旦漏洞公开,就会迅速在诸如Metasploit之类的框架中开发利用程序。面对加速的时间表,许多谷歌团队现在需要匆忙修补他们的系统。谷歌的安全团队使用自动扫描来发现其他脆弱的系统,并通知受影响的团队进行补丁,并跟踪他们的进展。内存泄露意味着私钥可能会泄露,这意味着许多服务需要密钥轮换。安全团队通知受影响的团队并在中央电子表格中跟踪他们的进展。

Heartbleed 阐明了许多重要的教训:

为禁运被突破或提前解除的最坏情况做准备。

负责任的披霆露面是理想的,但意外事件(以及激发媒体兴趣的可爱标志)可能会发生。尽可能多地进行预先工作,并迅速修补最脆弱的系统,而不管披露协议(这需要对禁运信息进行内部保密)如何。如果您能提前获得补丁,您可能可以在公开宣布之前部署它。当这不可能时,您仍应该能够测试和验证您的补丁,以确保平稳的推出过程。

为大规模快速部署做好准备。

使用持续构建来确保您可以随时重新编译,并使用金丝雀策略进行验证而不会造成破坏。

定期轮换您的加密密钥和其他机密信息。

密钥轮换是限制密钥泄露潜在影响范围的最佳实践。定期进行此操作,并确认您的系统仍然按预期工作;有关详细信息,请参阅SRE 工作手册中的第九章。通过这样做,您确保更换任何受损密钥不会是一个艰巨的工作。

确保您与最终用户(内部和外部)有沟通渠道。

当更改失败或导致意外行为时,您需要能够快速提供更新。

结论

区分不同类型的安全更改至关重要,以便受影响的团队知道他们应该做什么,以及您可以提供多少支持。

下次您被要求在您的基础设施中进行安全更改时,深呼吸并制定计划。从小处着手,或找到愿意测试更改的志愿者。建立反馈循环,了解用户遇到的问题,并使更改成为自助服务。如果计划发生变化,不要惊慌,但也不要感到惊讶。

设计策略,如频繁的推出、容器化和微服务,使得积极的改进和紧急的缓解更加容易,而分层方法则使外部表面积少且易于管理。深思熟虑的设计和持续的文档记录,都要考虑到变化,保持系统的健康,使团队的工作负担更加可管理,并且正如你将在下一章中看到的那样,这将导致更大的韧性。

¹ 广泛归因于以弗所的赫拉克利特

² 有关容器的更多信息,请参阅 Dan Lorenc 和 Maya Kaczorowski 的博客文章“探索容器安全性”。另请参阅 Burns, Brendan 等人 2016 年的文章“Borg, Omega, and Kubernetes: Lessons Learned from Three Container-Management Systems Over a Decade.” ACM Queue 14(1)。https://oreil.ly/tDKBJ

³ 有关微服务的更多信息,请参阅《SRE 书》第七章中的“案例研究 4:在共享平台上运行数百个微服务”。

⁴ 有关容器的更多信息,请参阅《SRE 书》第二章。

⁵ 有关更多信息,请参阅Google Cloud 中的传输加密白皮书

⁶ 同前。

⁷ 请参阅《SRE 书》第 27 章中有关渐进和分阶段推出的讨论。

⁸ 另请参阅有关此事件的小组讨论的YouTube 视频

⁹ 请参阅 Lang, Juan 等人 2016 年的文章“Security Keys: Practical Cryptographic Second Factors for the Modern Web.” 2016 年国际金融密码学和数据安全会议论文集:422–440。https://oreil.ly/S2ZMU

¹⁰ TechStops 是 Google 的 IT 帮助台,由 Jesus Lugo 和 Lisa Mauck 在一篇博客文章中描述。

第八章:面向弹性的设计

原文: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 章

第三部分:实现系统

原文:Part III. Implementing Systems

译者:飞龙

协议:CC BY-NC-SA 4.0

一旦您分析并设计了您的系统,就该是实现计划的时候了。在某些情况下,实现可能意味着购买现成的解决方案。第十一章提供了谷歌在决定构建定制软件解决方案时的思考过程的一个例子。

本书的第三部分着重于在软件开发生命周期的实现阶段集成安全性和可靠性。第十二章重申了框架将简化您的系统的观念。在测试过程中添加静态分析和模糊测试,正如第十三章所描述的那样,将加固代码。第十四章讨论了为什么您还应该投资于可验证的构建和进一步的控制——如果对手能够绕过它们达到您的生产环境,那么围绕编码、构建和测试的保障措施的效果将受到限制。

即使您的整个软件供应链对安全性和可靠性故障具有弹性,当问题出现时,您仍然需要分析您的系统。第十五章讨论了您必须在授予适当的调试访问权限和存储和访问日志的安全要求之间保持谨慎平衡。

第十一章:案例研究:设计、实现和维护公信 CA

原文:11. Case Study: Designing, Implementing, and Maintaining a Publicly Trusted CA

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Andy Warner、James Kasten、Rob Smits、Piotr Kucharski 和 Sergey Simakov

公信证书颁发机构的背景

公信证书颁发机构通过为传输层安全性(TLS)、S/MIME 等常见的分布式信任场景颁发证书,充当互联网传输层的信任锚点。它们是浏览器、操作系统和设备默认信任的 CA 集合。因此,编写和维护一个公信 CA 引发了许多安全性和可靠性考虑。

要成为公信并保持这一地位,CA 必须通过跨不同平台和用例的一系列标准。至少,公信 CA 必须接受诸如WebTrust欧洲电信标准化协会(ETSI)等组织设定的标准的审计。公信 CA 还必须满足CA/Browser 论坛基线要求的目标。这些评估评估逻辑和物理安全控制、程序和实践,一个典型的公信 CA 每年至少花费四分之一的时间进行这些审计。此外,大多数浏览器和操作系统都有自己独特的要求,CA 必须在被默认信任之前满足这些要求。随着要求的变化,CA 需要适应并愿意进行流程或基础设施的变更。

你的组织很可能永远不需要构建一个公信 CA——大多数组织依赖第三方获取公共 TLS 证书、代码签名证书和其他需要用户广泛信任的证书。考虑到这一点,本案例研究的目标不是向您展示如何构建一个公信 CA,而是强调我们的一些发现可能与您环境中的项目产生共鸣。主要的收获包括以下内容:

  • 我们选择的编程语言以及在处理第三方生成的数据时使用分段或容器使整体环境更加安全。

  • 严格测试和加固代码——无论是我们自己生成的代码还是第三方代码——对于解决基本的可靠性和安全性问题至关重要。

  • 当我们在设计中减少复杂性并用自动化替换手动步骤时,我们的基础设施变得更安全、更可靠。

  • 了解我们的威胁模型使我们能够构建验证和恢复机制,使我们能够更好地提前为灾难做准备。

我们为什么需要一个公信 CA?

随着时间的推移,我们对公信 CA 的业务需求发生了变化。在谷歌早期,我们从第三方 CA 购买了所有的公共证书。这种方法存在三个固有问题,我们希望解决:

依赖第三方

业务需求需要高度的信任——例如,向客户提供云服务——这意味着我们需要对证书的发放和处理进行强有力的验证和控制。即使我们在 CA 生态系统中进行了强制性的审计,我们仍然不确定第三方是否能够达到高标准的安全性。公信 CA 中的显著安全漏洞巩固了我们对安全性的看法。

自动化的需求

谷歌拥有成千上万的公司拥有的域名,为全球用户提供服务。作为我们普遍的 TLS 工作的一部分(参见“示例:增加 HTTPS 使用率”),我们希望保护我们拥有的每个域,并经常更换证书。我们还希望为客户提供获取 TLS 证书的简便方法。自动获取新证书很困难,因为第三方公信 CA 通常没有可扩展的 API,或者提供的 SLA 低于我们的需求。因此,这些证书的请求过程很大程度上涉及容易出错的手动方法。

成本

考虑到谷歌想要为自己的网络属性和客户代表使用数百万个 TLS 证书,成本分析显示,与继续从第三方根 CA 获取证书相比,设计、实现和维护我们自己的 CA 将更具成本效益。

建设或购买决策

一旦谷歌决定要运营一个公信 CA,我们就必须决定是购买商业软件来运营 CA,还是编写我们自己的软件。最终,我们决定自己开发 CA 的核心部分,并在必要时集成开源和商业解决方案。在许多决定因素中,这个决定背后有一些主要的动机:

透明度和验证

CA 的商业解决方案通常没有我们对代码或供应链的审计能力,这是我们对于如此关键的基础设施所需的。尽管它与开源库集成并使用了一些第三方专有代码,但编写和测试我们自己的 CA 软件使我们对正在构建的系统更加有信心。

集成能力

我们希望通过与谷歌的安全关键基础设施集成,简化 CA 的实现和维护。例如,我们可以在Spanner中的配置文件中设置定期备份。

灵活性

更广泛的互联网社区正在开发新的倡议,以提高生态系统的安全性。证书透明度——一种监视和审计证书的方式——以及使用 DNS、HTTP 和其他方法进行域验证⁵是两个典型的例子。我们希望成为这类倡议的早期采用者,自定义 CA 是我们能够迅速增加这种灵活性的最佳选择。

设计、实现和维护考虑

为了保护我们的 CA,我们创建了一个三层分层架构,其中每一层负责发行过程的不同部分:证书请求解析、注册机构功能(路由和逻辑)和证书签发。每一层都由具有明确定义责任的微服务组成。我们还设计了一个双信任区域架构,其中不受信任的输入在不同的环境中处理关键操作。这种分割创建了精心定义的边界,促进了可理解性和审查的便利。该架构还使得发动攻击更加困难:由于组件的功能受到限制,攻击者如果获得对特定组件的访问权限,也将受到功能受限的限制。要获得额外的访问权限,攻击者必须绕过额外的审计点。

每个微服务都是以简单性作为关键原则进行设计和实现的。在 CA 的整个生命周期中,我们不断地以简单性为考量对每个组件进行重构。我们对代码(包括内部开发和第三方)和数据进行严格的测试和验证。当需要提高安全性时,我们还会对代码进行容器化。本节详细描述了我们通过良好的设计和实现选择来解决安全性和可靠性的方法。

编程语言选择

对于接受任意不受信任输入的系统部分的编程语言选择是设计的一个重要方面。最终,我们决定用 Go 和 C++的混合编写 CA,并根据其目的选择使用哪种语言来处理每个子组件。Go 和 C++都可以与经过充分测试的加密库进行互操作,表现出优异的性能,并拥有强大的生态系统框架和工具来实现常见任务。

由于 Go 是内存安全的,它在处理 CA 处理任意输入时具有一些额外的安全优势。例如,证书签名请求(CSR)代表 CA 的不受信任输入。CSR 可能来自我们内部系统中的一个,这可能相对安全,也可能来自互联网用户(甚至是恶意行为者)。代码中解析 DER(用于证书的编码格式)的代码存在与内存相关的漏洞的悠久历史,因此我们希望使用一种提供额外安全性的内存安全语言。Go符合要求。

C++不是内存安全的,但对于系统的关键子组件具有良好的互操作性,特别是对于谷歌核心基础设施的某些组件。为了保护这些代码,我们在安全区域中运行它,并在数据到达该区域之前验证所有数据。例如,对于 CSR 处理,我们在 Go 中解析请求,然后将其传递给 C++子系统进行相同的操作,然后比较结果。如果存在差异,则不进行处理。

此外,我们在所有 C++代码的提交前强制执行良好的安全实践和可读性,谷歌的集中式工具链实现了各种编译时和运行时缓解措施。这些包括以下内容:

W^X

通过复制 shellcode 并跳转到该内存来破坏mmapPROT_EXEC的常见利用技巧。这种缓解措施不会造成 CPU 或内存性能损失。

Scudo Allocator

用户模式安全堆分配器。

SafeStack

一种安全缓解技术,可防范基于堆栈缓冲区溢出的攻击。

复杂性与可理解性

作为一种防御措施,我们明确选择实现 CA 的功能有限,与标准中提供的完整选项相比(参见“设计可理解的系统”)。我们的主要用例是为具有常用属性和扩展的标准 Web 服务颁发证书。我们对商业和开源 CA 软件选项的评估显示,它们试图适应我们不需要的奇特属性和扩展导致了系统的复杂性,使软件难以验证且更容易出错。因此,我们选择编写具有有限功能和更好可理解性的 CA,以便更容易审计预期的输入和输出。

我们不断努力简化 CA 的架构,使其更易于理解和维护。有一次,我们意识到我们的架构创建了太多不同的微服务,导致维护成本增加。虽然我们希望获得模块化服务和明确定义的边界的好处,但我们发现将系统的某些部分合并更简单。在另一种情况下,我们意识到我们对 RPC 调用的 ACL 检查是在每个实例中手动实现的,这为开发人员和审阅人员出现错误提供了机会。我们重构了代码库,以集中 ACL 检查并消除添加新的 RPC 而不使用 ACL 的可能性。

保护第三方和开源组件

我们的自定义 CA 依赖于第三方代码,即开源库和商业模块。我们需要验证、加固和容器化这些代码。作为第一步,我们专注于 CA 使用的几个众所周知且广泛使用的开源软件包。即使是在安全环境中广泛使用的开源软件包,也可能来自具有强大安全背景的个人或组织,也容易受到漏洞的影响。我们对每个软件包进行了深入的安全审查,并提交了修补程序以解决我们发现的问题。在可能的情况下,我们还将所有第三方和开源组件都提交到下一节详细介绍的测试制度中。

我们使用两个安全区域——一个用于处理不受信任的数据,另一个用于处理敏感操作——也为我们提供了一些分层保护,以防止错误或恶意插入到代码中。前面提到的 CSR 解析器依赖于开源 X.509 库,并作为 Borg 容器中不受信任区域的微服务运行。这为这段代码提供了额外的保护层。

我们还必须保护专有的第三方闭源代码。运行一个公开可信的 CA 需要使用硬件安全模块(HSM)——由商业供应商提供的专用加密处理器——作为保护 CA 密钥的保险库。我们希望为与 HSM 交互的供应商提供的代码提供额外的验证层。与许多供应商提供的解决方案一样,我们可以进行的测试种类有限。为了保护系统免受内存泄漏等问题的影响,我们采取了以下步骤:

  • 我们构建了必须与 HSM 库进行交互的 CA 的部分,采取了防御性措施,因为我们知道输入或输出可能存在风险。

  • 我们在nsjail中运行第三方代码,这是一个轻量级的进程隔离机制。

  • 我们向供应商报告了我们发现的问题。

测试

为了保持项目的卫生,我们编写单元测试和集成测试(见第十三章)来覆盖各种场景。团队成员应在开发过程中编写这些测试,而同行评审则确保遵守这一做法。除了测试预期行为外,我们还测试负面情况。每隔几分钟,我们生成符合良好标准的测试证书签发条件,以及包含严重错误的条件。例如,我们明确测试了当未经授权的人员进行签发时,准确的错误消息是否会触发警报。拥有正面和负面测试条件的存储库使我们能够非常快速地对所有新的 CA 软件部署进行高可信度的端到端测试。

通过使用谷歌的集中式软件开发工具链,我们还获得了在提交前和构建后的集成自动代码测试的好处。正如在“将静态分析集成到开发人员工作流程中”中讨论的那样,谷歌所有的代码更改都经过 Tricorder,我们的静态分析平台的检查。我们还对 CA 的代码进行各种消毒剂的检查,如 AddressSanitizer(ASAN)和 ThreadSanitizer,以识别常见错误(见“动态程序分析”)。此外,我们对 CA 代码进行有针对性的模糊测试(见“模糊测试”)。

CA 密钥材料的弹性

CA 面临的最严重风险是 CA 密钥材料的盗窃或滥用。公开可信的 CA 的大多数强制性安全控制措施都是针对可能导致这种滥用的常见问题,包括使用 HSM 和严格的访问控制等标准建议。

我们将 CA 的根密钥材料离线保存,并用多层物理保护来保护它,每个访问层都需要两方授权。对于日常证书签发,我们使用在线可用的中间密钥,这是行业标准做法。由于让公众信任的 CA 被广泛纳入生态系统(即浏览器、电视和手机)的过程可能需要多年时间,因此在遭受损害后旋转密钥(参见“旋转签名密钥”)作为恢复工作的一部分并不是一个简单或及时的过程。因此,密钥材料的丢失或被盗可能会造成重大中断。为了防范这种情况,我们在生态系统中成熟了其他根密钥材料(通过将材料分发给使用加密连接的浏览器和其他客户端),这样我们就可以在必要时替换备用材料。

数据验证

除了密钥材料的丢失,签发错误是 CA 可能犯的最严重的错误。我们努力设计我们的系统,以确保人为判断不能影响验证或签发,这意味着我们可以将注意力集中在 CA 代码和基础设施的正确性和健壮性上。

持续验证(见“持续验证”)确保系统的行为符合预期。为了在 Google 的公众信任 CA 中实现这一概念,我们自动在签发过程的多个阶段通过 linter 运行证书。linter 检查错误模式,例如确保证书具有有效的生命周期或者subject:commonName具有有效的长度。一旦证书经过验证,我们将其输入到证书透明日志中,这允许公众进行持续验证。为了防范恶意签发,我们还使用多个独立的日志系统,通过逐个比较两个系统的条目来确保一致性。这些日志在到达日志存储库之前会被签名,以提供进一步的安全性和以备需要时进行后续验证。

结论

证书颁发机构是对安全性和可靠性有严格要求的基础设施的一个例子。使用本书中概述的最佳实践来实现基础设施可以带来长期的安全性和可靠性。这些原则应该是设计的一部分,但您也应该在系统成熟时使用它们来改进系统。

¹ TLS 的最新版本在RFC 8446中有描述。

² 安全/多用途互联网邮件扩展是一种加密电子邮件内容的常见方法。

³ 我们意识到许多组织确实构建和运营私有 CA,使用诸如微软的 AD 证书服务等常见解决方案。这些通常仅供内部使用。

⁴ DigiNotar 在遭受攻击者的侵害和滥用其 CA 后破产了

⁵ 域验证指南的良好参考是CA/Browser Forum 基线要求

Mitre CVE 数据库中包含了各种 DER 处理程序发现的数百个漏洞。

⁷ Borg 容器在 Verma, Abhishek 等人的 2015 年的论文“Large-Scale Cluster Management at Google with Borg.”中有描述。Proceedings of the 10th European Conference on Computer Systems: 1–17. doi:10.1145/2741948.2741964。

⁸ 例如,ZLint是一个用 Go 编写的 linter,用于验证证书的内容是否符合 RFC 5280 和 CA/Browser Forum 的要求。

第十二章:编写代码

原文:12. Writing Code

译者:飞龙

协议:CC BY-NC-SA 4.0

由 Michał Czapiński 和 Julian Bangert 撰写

与 Thomas Maufer 和 Kavita Guliani 合作

安全性和可靠性不能轻易地加入软件中,因此在软件设计的早期阶段就考虑它们是很重要的。在发布后添加这些功能是痛苦且不太有效的,可能需要您改变代码库的其他基本假设(有关此主题的更深入讨论,请参见第四章)。

减少安全性和可靠性问题的第一步,也是最重要的一步是教育开发人员。然而,即使是训练有素的工程师也会犯错——安全专家可能会编写不安全的代码,SRE 可能会忽略可靠性问题。在同时考虑构建安全和可靠系统所涉及的许多考虑因素和权衡是困难的,尤其是如果你还负责生产软件的话。

与其完全依赖开发人员审查代码的安全性和可靠性,不如让 SRE 和安全专家审查代码和软件设计。这种方法也是不完美的——手动代码审查不会发现每个问题,也不会每个安全问题都能被审查人员发现。审查人员也可能会受到自己的经验或兴趣的影响。例如,他们可能自然而然地倾向于寻找新的攻击类型、高级设计问题或加密协议中的有趣缺陷;相比之下,审查数百个 HTML 模板以查找跨站脚本(XSS)漏洞,或者检查应用程序中每个 RPC 的错误处理逻辑可能会被视为不那么令人兴奋。

虽然代码审查可能不会发现每个漏洞,但它们确实有其他好处。良好的审查文化鼓励开发人员以便于审查安全性和可靠性属性的方式构建他们的代码。本章讨论了使审查人员能够明显看到这些属性的策略,并将自动化整合到开发过程中。这些策略可以释放团队的带宽,让他们专注于其他问题,并建立安全和可靠性的文化(参见第二十一章)。

强制执行安全性和可靠性的框架

如第六章所讨论的,应用程序的安全性和可靠性依赖于特定领域的不变量。例如,如果应用程序的所有数据库查询仅由开发人员控制的代码组成,并通过查询参数绑定提供外部输入,那么该应用程序就可以防止 SQL 注入攻击。如果所有插入 HTML 表单的用户输入都经过适当转义或清理以删除任何可执行代码,Web 应用程序就可以防止 XSS 攻击。

理论上,您可以通过仔细编写维护这些不变量的应用程序代码来创建安全可靠的软件。然而,随着所需属性的数量和代码库的规模增长,这种方法几乎变得不可能。不合理地期望任何开发人员都是所有这些主题的专家,或者在编写或审查代码时始终保持警惕是不合理的。

如果需要人工审查每个更改,那么这些人将很难维护全局不变量,因为审查人员无法始终跟踪全局上下文。如果审查人员需要知道哪些函数参数是由调用者传递的用户输入,哪些参数只包含开发人员控制的可信值,他们还必须熟悉函数的所有传递调用者。审查人员不太可能能够长期保持这种状态。

更好的方法是在通用框架、语言和库中处理安全和可靠性。理想情况下,库只公开一个接口,使得使用常见安全漏洞类的代码编写变得不可能。多个应用程序可以使用每个库或框架。当领域专家修复问题时,他们会从框架支持的所有应用程序中删除它,从而使这种工程方法更好地扩展。与手动审查相比,使用集中的强化框架还可以减少未来漏洞的可能性。当然,没有框架可以防止所有安全漏洞,攻击者仍然有可能发现未预料到的攻击类别或发现框架实现中的错误。但是,如果您发现了新的漏洞,您可以在一个地方(或几个地方)解决它,而不是在整个代码库中。

举一个具体的例子:SQL 注入(SQLI)在常见安全漏洞的 OWASP](https://oreil.ly/TnBaK)和SANS列表中占据首要位置。根据我们的经验,当您使用像TrustedSqlString这样的强化数据库时(参见[“SQL 注入漏洞:TrustedSqlString”),这类漏洞就不再是问题。类型使这些假设变得明确,并且由编译器自动执行。

使用框架的好处

大多数应用程序都具有类似的安全构建块(身份验证和授权、日志记录、数据加密)和可靠性构建块(速率限制、负载平衡、重试逻辑)。为每个服务从头开始开发和维护这些构建块是昂贵的,并且会导致每个服务中不同错误的拼接。

框架实现了代码重用:开发人员只需定制特定的构建块,而不需要考虑影响给定功能或特性的所有安全和可靠性方面。例如,开发人员可以指定传入请求凭据中哪些信息对授权很重要,而无需担心这些信息的可信度——框架会验证可信度。同样,开发人员可以指定需要记录哪些数据,而无需担心存储或复制。框架还使传播更新更容易,因为您只需要在一个位置应用更新。

使用框架可以提高组织中所有开发人员的生产力,有助于建立安全和可靠文化(参见第二十一章)。对于一个团队的领域专家来说,设计和开发框架构建块要比每个团队单独实现安全和可靠特性更有效率。例如,如果安全团队处理加密,其他所有团队都会从他们的知识中受益。使用框架的开发人员无需担心其内部细节,而可以专注于应用程序的业务逻辑。

框架通过提供易于集成的工具进一步提高了生产力。例如,框架可以提供自动导出基本操作指标的工具,比如总请求数、按错误类型分解的失败请求数量,或者每个处理阶段的延迟。您可以使用这些数据生成自动化监控仪表板和服务的警报。框架还使与负载均衡基础设施集成更容易,因此服务可以自动将流量重定向到超载实例之外,或者在负载较重时启动新的服务实例。因此,基于框架构建的服务表现出更高的可靠性。

使用框架还可以通过清晰地将业务逻辑与常见功能分离,使对代码的推理变得容易。这使开发人员可以更有信心地对服务的安全性或可靠性做出断言。总的来说,框架可以降低复杂性——当跨多个服务的代码更加统一时,遵循常见的良好实践就更容易了。

开发自己的框架并不总是有意义。在许多情况下,最好的策略是重用现有的解决方案。例如,几乎任何安全专业人士都会建议您不要设计和实现自己的加密框架,而是可以使用像 Tink 这样的成熟和广泛使用的框架(在“示例:安全加密 API 和 Tink 加密框架”中讨论)。

在决定采用任何特定框架之前,评估其安全姿态是很重要的。我们还建议使用积极维护的框架,并不断更新您的代码依赖项,以纳入对您的代码依赖的任何代码的最新安全修复。

以下案例是一个实际示例,演示了框架的好处:在这种情况下,是用于创建 RPC 后端的框架。

示例:RPC 后端框架

大多数 RPC 后端遵循类似的结构。它们处理特定于请求的逻辑,并通常还执行以下操作:

  • 日志记录

  • 认证

  • 授权

  • 限流(速率限制)

我们建议使用一个可以隐藏这些构建块的实现细节的框架,而不是为每个单独的 RPC 后端重新实现这些功能。然后开发人员只需要定制每个步骤以适应其服务的需求。

图 12-1 展示了一个基于预定义拦截器的可能框架架构,这些拦截器负责前面提到的每个步骤。您还可以使用拦截器来执行自定义步骤。每个拦截器定义了在实际 RPC 逻辑执行之前之后要执行的操作。每个阶段都可以报告错误条件,这会阻止进一步执行拦截器。但是,当发生这种情况时,已经调用的每个拦截器的之后步骤会以相反的顺序执行。拦截器之间的框架可以透明地执行其他操作,例如导出错误率或性能指标。这种架构导致了在每个阶段执行的逻辑的清晰分离,从而增加了简单性和可靠性。

RPC 后端潜在框架中的控制流;典型步骤封装在预定义的拦截器中,授权作为示例突出显示

图 12-1:RPC 后端潜在框架中的控制流:典型步骤封装在预定义的拦截器中,授权作为示例突出显示

在这个例子中,日志拦截器的之前阶段可以记录调用,之后阶段可以记录操作的状态。现在,如果请求未经授权,RPC 逻辑不会执行,但是“权限被拒绝”的错误会被正确记录。之后,系统调用认证和日志拦截器的之后阶段(即使它们是空的),然后才将错误发送给客户端。

拦截器通过它们传递给彼此的上下文对象共享状态。例如,认证拦截器的之前阶段可以处理与证书处理相关的所有加密操作(注意从重用专门的加密库而不是重新实现一个来提高安全性)。然后系统将提取和验证的关于调用者的信息包装在一个方便的对象中,并将其添加到上下文中。随后的拦截器可以轻松访问此对象。

然后框架可以使用上下文对象来跟踪请求执行时间。如果在任何阶段明显地请求不会在截止日期之前完成,系统可以自动取消请求。通过快速通知客户端,还可以提高服务的可靠性,这也节省了资源。

一个好的框架还应该使您能够处理 RPC 后端的依赖关系,例如负责存储日志的另一个后端。您可以将这些注册为软依赖或硬依赖,框架可以不断监视它们的可用性。当它检测到硬依赖不可用时,框架可以停止服务,报告自身不可用,并自动将流量重定向到其他实例。

迟早,过载、网络问题或其他问题将导致依赖不可用。在许多情况下,重试请求是合理的,但要小心实现重试,以避免级联故障(类似于多米诺骨牌的倒塌)。¹最常见的解决方案是使用指数退避。²一个好的框架应该提供对这样的逻辑的支持,而不是要求开发人员为每个 RPC 调用实现逻辑。

一个优雅处理不可用依赖并重定向流量以避免过载服务或其依赖的框架自然地提高了服务本身和整个生态系统的可靠性。这些改进需要开发人员的最少参与。

示例代码片段

示例 12-1 到 12-3 演示了 RPC 后端开发人员与安全或可靠性框架合作的视角。这些示例使用 Go 并使用Google Protocol Buffers

12-1 示例。初始类型定义(拦截器的前阶段可以修改上下文;例如,身份验证拦截器可以添加有关调用者的验证信息)
type Request struct {  
  Payload proto.Message  
} 
type Response struct {  
  Err error
  Payload proto.Message  
} 
type Interceptor interface {  
  Before(context.Context, *Request) (context.Context, error)  
  After(context.Context, *Response) error 
} 
type CallInfo struct {  
  User string  
  Host string  
  ...  
}
12-2 示例。示例授权拦截器,只允许来自白名单用户的请求
type authzInterceptor struct {
 allowedRoles map[string]bool
}

func (ai *authzInterceptor) Before(ctx context.Context, req *Request) (context.Context, error) {
  // callInfo was populated by the framework.
  callInfo, err := FromContext(ctx)
  if err != nil { return ctx, err }

  if ai.allowedRoles[callInfo.User] { return ctx, nil }
  return ctx, fmt.Errorf("Unauthorized request from %q", callInfo.User)
}

func (*authzInterceptor) After(ctx context.Context, resp *Response) error {
  return nil  // Nothing left to do here after the RPC is handled.
}
12-3 示例。示例日志拦截器,记录每个传入请求(阶段前)然后记录所有失败的请求及其状态(阶段后);WithAttemptCount 是一个由框架提供的 RPC 调用选项,实现指数退避
type logInterceptor struct {
  logger *LoggingBackendStub
}

func (*logInterceptor) Before(ctx context.Context,
                              req *Request) (context.Context, error) {
  // callInfo was populated by the framework.
  callInfo, err := FromContext(ctx)
  if err != nil { return ctx, err }
  logReq := &pb.LogRequest{
    timestamp: time.Now().Unix(),
    user: callInfo.User,
    request: req.Payload,
  }
  resp, err := logger.Log(ctx, logReq, WithAttemptCount(3))
  return ctx, err
}

func (*logInterceptor) After(ctx context.Context, resp *Response) error {
  if resp.Err == nil { return nil }

  logErrorReq := &pb.LogErrorRequest{
    timestamp: time.Now().Unix(),
    error: resp.Err.Error(),
  }
  resp, err := logger.LogError(ctx, logErrorReq, WithAttemptCount(3))
  return err
}

常见安全漏洞

在大型代码库中,少数类占据了大部分安全漏洞,尽管不断努力教育开发人员并引入代码审查。OWASP 和 SANS 发布了常见漏洞类别的列表。表 12-1 列出了根据OWASP列出的前 10 个最常见的漏洞风险,以及在框架级别上缓解每个漏洞的一些潜在方法。

表 12-1。根据 OWASP 列出的前 10 个最常见的漏洞风险

OWASP 前 10 大漏洞 框架加固措施
[SQL]注入 TrustedSQLString(请参阅下一节)。
损坏的身份验证 在将请求路由到应用程序之前,要求使用像 OAuth 这样经过充分测试的机制进行身份验证。(参见“示例:RPC 后端框架”。)
敏感数据泄露 使用不同的类型(而不是字符串)来存储和处理信用卡号等敏感数据。这种方法可以限制序列化以防止泄漏并强制适当的加密。框架还可以强制执行透明的传输保护,如使用 LetsEncrypt 的 HTTPS。加密 API,如Tink,可以鼓励适当的秘密存储,例如从云密钥管理系统加载密钥,而不是从配置文件加载。
XML 外部实体(XXE) 使用未启用 XXE 的 XML 解析器;确保支持它的库中禁用这个风险特性。ᵃ
破损的访问控制 这是一个棘手的问题,因为它通常是特定于应用程序的。使用一个要求每个请求处理程序或 RPC 具有明确定义的访问控制限制的框架。如果可能的话,将最终用户凭据传递到后端,并在后端强制执行访问控制策略。
安全配置错误 使用默认提供安全配置并限制或不允许风险配置选项的技术堆栈。例如,使用一个在生产中不打印错误信息的 Web 框架。使用一个标志来启用所有调试功能,并设置部署和监控基础设施以确保这个标志不对公共用户启用。Rails 中的environment标志就是这种方法的一个例子。
跨站脚本(XSS) 使用 XSS 强化的模板系统(参见“预防 XSS:SafeHtml”)。
不安全的反序列化 使用专为处理不受信任输入而构建的反序列化库,例如Protocol Buffers
使用已知漏洞的组件 选择受欢迎且积极维护的库。不要选择有未修复或缓慢修复安全问题历史的组件。另请参见“评估和构建框架的教训”。
日志记录和监控不足 不要依赖于临时日志记录,适当地记录和监控请求和其他事件在低级库中。有关示例,请参见前一节中描述的日志拦截器。
ᵃ有关更多信息,请参见XXE 预防备忘单

SQL 注入漏洞:TrustedSqlString

SQL 注入是一种常见的安全漏洞类别。当不可信的字符串片段被插入到 SQL 查询中时,攻击者可能会注入数据库命令。以下是一个简单的密码重置网页表单:

db.query("UPDATE users SET pw_hash = '" + request["pw_hash"] 
         + "' WHERE reset_token = '" + request.params["reset_token"] + "'")

在这种情况下,用户的请求被定向到一个具有与其帐户特定的不可猜测的reset_token的后端。然而,由于字符串连接,恶意用户可以制作一个带有额外 SQL 命令(例如' or username='admin)的自定义reset_token并将其注入到后端。结果可能会重置不同用户的密码哈希—在这种情况下是管理员帐户。

在更复杂的代码库中,SQL 注入漏洞可能更难以发现。数据库引擎可以通过提供绑定参数和预编译语句来帮助您防止 SQL 注入漏洞:

Query q = db.createQuery(
  "UPDATE users SET pw_hash = @hash WHERE token = @token");
q.setParameter("hash", request.params["hash"]);
q.setParameter("token", request.params["token"]);
db.query(q);

然而,仅仅建立一个使用预编译语句的准则并不能导致可扩展的安全流程。您需要教育每个开发人员遵守这个规则,并且安全审查人员需要审查所有应用程序代码,以确保一致使用预编译语句。相反,您可以设计数据库 API,使用户输入和 SQL 的混合在设计上变得不可能。例如,您可以创建一个名为TrustedSqlString的单独类型,并通过构造强制执行所有 SQL 查询字符串都是由开发人员控制的输入创建的。在 Go 中,您可以实现该类型如下:

struct Query {
  sql strings.Builder;
} 
type stringLiteral string;  
*// Only call this function with string literal parameters.*
func (q *Query) AppendLiteral(literal stringLiteral) {
  q.sql.writeString(literal);
}
*// q.AppendLiteral("foo") will work, q.AppendLiteral(foo) will not*

该实现通过构造保证了q.sql的内容完全是从您的源代码中存在的字符串字面量连接而成的,用户无法提供字符串字面量。为了在规模上强制执行这个合同,您可以使用一种特定于语言的机制,确保AppendLiteral只能与字符串字面量一起调用。例如:

在 Go

使用包私有类型别名(stringLiteral)。包外的代码不能引用此别名;但是,字符串字面量会被隐式转换为这种类型。

在 Java

使用Error Prone代码检查器,它为参数提供了@CompileTimeConstant注释。

在 C++中

使用依赖于字符串中每个字符值的模板构造函数。

您可以在其他语言中找到类似的机制。

您无法仅使用编译时常量构建某些功能,比如设计为由拥有数据的用户提供任意 SQL 查询的数据分析应用程序。为了处理复杂的用例,在 Google,我们允许通过安全工程师的批准绕过类型限制的方法。例如,我们的数据库 API 有一个单独的包unsafequery,它导出一个独特的unsafequery.String类型,可以从任意字符串构造并附加到 SQL 查询中。只有很小一部分查询使用了未经检查的 API。对于数百到数千名活跃开发人员,审核不安全的 SQL 查询的新用途和其他受限 API 模式的负担由一名(轮换的)工程师兼职处理。参见“评估和构建框架的教训”以了解审核豁免的其他好处。

防止 XSS:SafeHtml

我们在前一节中描述的基于类型的安全方法并不特定于 SQL 注入。Google 使用更复杂的相同设计的版本来减少 Web 应用程序中的跨站脚本漏洞。³

在核心上,XSS 漏洞发生在 Web 应用程序在没有适当清理的情况下呈现不可信的输入时。例如,一个应用程序可能会将一个受攻击者控制的$address值插入到 HTML 片段中,如<div>$address</div>,然后显示给另一个用户。攻击者可以将$address设置为<script>exfiltrate_user_data();</script>并在另一个用户页面的上下文中执行任意代码。

HTML 没有绑定查询参数的等价物。相反,不可信的值必须在插入到 HTML 页面之前得到适当的清理或转义。此外,不同的 HTML 属性和元素具有不同的语义,因此应用程序开发人员必须根据它们出现的上下文来不同对待值。例如,攻击者控制的 URL 可以使用javascript:方案来执行代码。

类型系统可以通过为不同上下文中的值引入不同的类型来捕获这些要求,例如,SafeHtml用于表示 HTML 元素的内容,SafeUrl用于安全导航到的 URL。每种类型都是一个(不可变的)字符串包装器;构造函数负责维护每种类型的合同。构造函数构成了负责确保应用程序安全属性的受信任代码库。

Google 为不同的用例创建了不同的构建器库。可以使用构建器方法构造单个 HTML 元素,该方法要求每个属性值都具有正确的类型,并且对于元素内容使用SafeHtml。具有严格上下文转义的模板系统可以保证更复杂的 HTML 的SafeHtml合同。该系统执行以下操作:

  1. 解析模板中的部分 HTML

  2. 确定每个替换点的上下文

  3. 要么要求程序传递正确类型的值,要么正确转义或清理不受信任的字符串值

例如,如果您有以下 Closure 模板:

{template .foo kind="html"}<script src="{$url}"></script>{/template}

尝试使用$url的字符串值将失败:

templateRendered.setMapData(ImmutableMap.of("url", some_variable));

相反,开发人员必须提供TrustedResourceUrl值,例如:

templateRenderer.setMapData(
     ImmutableMap.of("x", TrustedResourceUrl.fromConstant("/script.js"))
 ).render();

如果 HTML 来自不受信任的来源,您不希望将其嵌入到应用程序的 Web UI 中,因为这样做会导致易受攻击的 XSS 漏洞。相反,您可以使用 HTML 清理器解析 HTML 并执行运行时检查,以确定每个值是否符合其合同。清理器会删除不符合其合同的元素,或者无法在运行时检查合同的元素。您还可以使用清理器与不使用安全类型的其他系统进行交互,因为许多 HTML 片段在清理过程中保持不变。

不同的 HTML 构建库针对不同的开发人员生产力和代码可读性权衡。但是,它们都强制执行相同的合同,并且应该同样值得信赖(除了它们受信任的实现中的任何错误)。实际上,为了减少谷歌的维护负担,我们从声明性配置文件中为各种语言代码生成构建器函数。该文件列出了 HTML 元素和每个属性值的所需合同。我们的一些 HTML 清理器和模板系统使用相同的配置文件。

Closure Templates中提供了成熟的开源安全类型实现,目前正在进行引入基于类型的安全性的工作,作为 Web 标准。

评估和构建框架的教训

前几节讨论了如何构建库以建立安全性和可靠性属性。但是,并非所有这样的属性都可以通过 API 设计优雅地表达,有些情况下甚至无法轻松更改 API,例如与 Web 浏览器公开的标准化 DOM API 交互时。

相反,您可以引入编译时检查,以防止开发人员使用风险 API。流行编译器的插件,如 Java 的Error Prone和 TypeScript 的Tsetse,可以禁止危险的代码模式。

我们的经验表明,编译器错误提供了即时和可操作的反馈。在代码审查时运行的工具(如 linter)或在代码审查时提供的反馈要晚得多。到代码发送进行审查时,开发人员通常已经有了一个完成的、可工作的代码单元。在开发过程的这么晚阶段得知需要进行一些重新架构才能使用严格类型的 API 可能会令人沮丧。

向开发人员提供编译器错误或更快的反馈机制(如 IDE 插件,可以标出有问题的代码)要容易得多。通常,开发人员会快速解决编译问题,并且已经必须修复其他编译器诊断,如拼写错误和语法错误。因为开发人员已经在处理受影响的特定代码行,他们有完整的上下文,所以进行更改更容易,例如将字符串的类型更改为SafeHtml

通过建议自动修复,甚至可以进一步改善开发人员的体验,这些自动修复可以作为安全解决方案的起点。例如,当检测到对 SQL 查询函数的调用时,可以自动插入对TrustedSqlBuilder.fromConstant的调用,其中包含查询参数。即使生成的代码不能完全编译(也许是因为查询是字符串变量而不是常量),开发人员也知道该怎么做,无需通过找到正确的函数、添加正确的导入声明等来烦恼 API 的机械细节。

根据我们的经验,只要反馈周期快,修复每个模式相对容易,开发人员就会更愿意接受固有安全的 API,即使我们无法证明他们的代码是不安全的,或者他们在使用不安全的 API 编写安全代码时做得很好。我们的经验与现有的研究文献形成对比,后者侧重于降低误报和漏报率。(4)

我们发现,专注于这些速率通常会导致复杂的检查器,需要更长时间才能发现问题。例如,一个检查可能需要分析复杂应用程序中的整个程序数据流。通常很难解释如何从静态分析检测到的问题中删除问题,因为检查器的工作方式比简单的语法属性要难得多。理解一个发现需要和在 GDB(GNU 调试器)中追踪错误一样多的工作。另一方面,在编写新代码时在编译时修复类型安全错误通常并不比修复微不足道的类型错误困难得多。

简单、安全、可靠的常见任务库

构建一个安全的库,涵盖所有可能的用例并可靠地处理每个用例可能非常具有挑战性。例如,一个在 HTML 模板系统上工作的应用程序开发人员可能会编写以下模板:

<a onclick="showUserProfile('{{username}}');">Show profile</a>">

为了防止 XSS 攻击,如果“用户名”受攻击者控制,模板系统必须嵌套三种不同的上下文层:单引号字符串,JavaScript 内部,HTML 元素属性内部。创建一个可以处理所有可能的边缘情况组合的模板系统是复杂的,并且使用该系统不会简单。在其他领域,这个问题可能变得更加复杂。例如,业务需求可能会规定谁可以执行动作,谁不能。除非您的授权库像通用编程语言一样具有表达力(并且难以分析),您可能无法满足所有开发人员的需求。

相反,您可以从一个简单的、小型的库开始,只涵盖常见用例,但更容易正确使用。简单的库更容易解释、文档化和使用。这些特性减少了开发人员的摩擦,并可能帮助您说服其他开发人员采用安全设计的库。在某些情况下,提供针对不同用例进行优化的不同库可能是有意义的。例如,您可能既有用于复杂页面的 HTML 模板系统,也有用于短片段的构建器库。

您可以通过专家审查的方式满足其他用例,访问一个不受限制的、风险的库,绕过安全保证。如果您看到类似的重复请求,您可以在固有安全的库中支持该功能。正如我们在“SQL 注入漏洞:TrustedSqlString”中观察到的,审查负载通常是可以管理的。

由于审查请求的数量相对较少,安全审查人员可以深入查看代码并提出广泛的改进建议——审查往往是独特的用例,这保持了审查人员的积极性,并防止了由于重复和疲劳而导致的错误。豁免也作为一个反馈机制:如果开发人员反复需要豁免某个用例,库作者应该考虑为该用例构建一个库。

推出策略

我们的经验表明,对安全属性使用类型对于新代码非常有用。事实上,使用安全类型的谷歌内部广泛使用的一个 Web 框架创建的应用程序,报告的 XSS 漏洞要少得多(少两个数量级)比没有使用安全类型编写的应用程序,尽管进行了仔细的代码审查。少数报告的漏洞是由于应用程序的组件没有使用安全类型造成的。

将现有代码调整为使用安全类型更具挑战性。即使您从头开始创建一个全新的代码库,您也需要一个迁移遗留代码的策略——您可能会发现您想要保护的新的安全性和可靠性问题类别,或者您可能需要完善现有的合同。

我们已经尝试了几种重构现有代码的策略;我们在下面的小节中讨论了我们最成功的两种方法。这些策略要求您能够访问和修改应用程序的整个源代码。Google 的大部分源代码都存储在一个单一的存储库中⁵,并具有用于制作、构建、测试和提交更改的集中式流程。代码审查人员还会强制执行常见的可读性和代码组织标准,这减少了改变陌生代码库的复杂性。在其他环境中,大规模的重构可能更具挑战性。获得广泛的一致意见有助于每个代码所有者都愿意接受对他们源代码的更改,这有助于建立一个安全可靠的文化。

注意

Google 公司的全公司风格指南融入了语言可读性的概念:工程师理解给定语言的 Google 最佳实践和编码风格的认证。可读性确保了代码质量的基线。工程师必须在他们正在使用的语言中具有可读性,或者从具有可读性的人那里获得代码审查。对于特别复杂或至关重要的代码,面对面的代码审查可能是改进代码库质量最有效和高效的方式。

增量推出

一次性修复整个代码库通常是不可行的。不同的组件可能在不同的存储库中,对多个应用程序进行更改的编写、审查、测试和提交通常是脆弱且容易出错的。相反,在 Google,我们最初豁免遗留代码的执行,并逐个解决现有不安全 API 的使用者。

例如,如果您已经有一个带有doQuery(String sql)函数的数据库 API,您可以引入一个重载,doQuery(TrustedSqlString sql),并将不安全版本限制为现有的调用者。使用 Error Prone 框架,您可以添加一个@RestrictedApi(whitelistAnnotation={LegacyUnsafeStringQueryAllowed.class})注解,并将@LegacyUnsafeStringQueryAllowed注解添加到所有现有的调用者。

然后,通过引入Git hooks来分析每个提交,您可以防止新代码使用基于字符串的过载。或者,您可以限制不安全 API 的可见性——例如,Bazel 可见性白名单将允许用户仅在安全团队成员批准拉取请求(PR)时调用 API。如果您的代码库正在积极开发,它将自然地向安全 API 迁移。在达到只有少部分调用者使用已弃用的基于字符串的 API 的时候,您可以手动清理剩余部分。在那时,您的代码将因设计而免疫 SQL 注入。

遗留转换

将所有的豁免机制整合到一个在源代码中明显的函数中通常也是值得的。例如,您可以创建一个函数,它接受任意字符串并返回一个安全类型。您可以使用这个函数来替换所有对字符串类型的 API 的调用,使其更精确地调用。通常,类型会比使用它们的函数少得多。与限制和监控许多遗留 API 的移除(例如,每个消耗 URL 的 DOM API)不同,您只需要删除每种类型的一个遗留转换函数。

简单导致安全可靠的代码

在实际情况下,尽量保持代码简洁和简单。关于这个主题有很多出版物,⁶所以这里我们专注于两个轻量级的故事,这些故事发表在Google 测试博客上。这两个故事都强调了避免快速增加代码库复杂性的策略。

避免多级嵌套

多层嵌套是一种常见的反模式,可能导致简单的错误。如果错误出现在最常见的代码路径中,它很可能会被单元测试捕获。但是,单元测试并不总是检查多层嵌套代码中的错误处理路径。错误可能导致可靠性降低(例如,如果服务在错误处理不当时崩溃)或安全漏洞(如错误处理授权检查错误)。

您能在图 12-2 的代码中发现错误吗?这两个版本是等价的。⁷

在多层嵌套代码中,错误通常更难发现

图 12-2:在多层嵌套代码中,错误通常更难发现

“错误编码”和“未经授权”的错误被交换了。在重构版本中更容易看到这个错误,因为检查发生在处理错误时。

消除 YAGNI 气味

有时,开发人员通过添加可能在将来有用的功能“以防万一”来过度设计解决方案。这违反了YAGNI(你不会需要它)原则,该原则建议仅实现您需要的代码。YAGNI 代码会增加不必要的复杂性,因为它需要进行文档化、测试和维护。考虑以下示例:⁸

class Mammal { ...
  virtual Status Sleep(bool hibernate) = 0;
};
class Human : public Mammal { ...
  virtual Status Sleep(bool hibernate) {
    age += hibernate ? kSevenMonths : kSevenHours;
    return OK;
  }
};

Human::Sleep代码必须处理hibernatetrue的情况,即使所有调用者应始终传递false。此外,调用者必须处理返回的状态,即使该状态应始终为OK。因此,在您需要除Human之外的其他类之前,此代码可以简化为以下内容:

class Human { ...
  void Sleep() { age += kSevenHours; }
};

如果开发人员对未来功能的可能需求做出的假设实际上是正确的,他们可以通过遵循增量开发和设计原则轻松地稍后添加该功能。在我们的例子中,基于几个现有类进行概括时,将更容易创建具有更好公共 API 的Mammal接口。

总之,避免 YAGNI 代码会提高可靠性,简化代码会减少安全漏洞,减少出错的机会,并减少开发人员维护未使用代码的时间。

偿还技术债务

开发人员通常会使用 TODO 或 FIXME 注释标记需要进一步关注的地方。短期内,这种习惯可以加快最关键功能的交付速度,并允许团队满足早期的截止日期,但也会产生技术债务。不过,只要您有清晰的流程(并分配时间)来偿还这样的债务,这并不一定是一种坏习惯。

技术债务可能包括对异常情况的错误处理以及将不必要的复杂逻辑引入代码(通常编写以解决其他技术债务领域的问题)。任何一种行为都可能引入安全漏洞和可靠性问题,这些问题在测试期间很少被检测到(因为罕见情况的覆盖不足),因此成为生产环境的一部分。

您可以以许多方式处理技术债务。例如:

  • 保持具有代码健康度指标的仪表板。这些可以是简单的仪表板,显示测试覆盖率或 TODO 的数量和平均年龄,也可以是包括圈复杂度可维护性指数等指标的更复杂的仪表板。

  • 使用诸如 linter 之类的分析工具来检测常见的代码缺陷,例如死代码、不必要的依赖关系或特定于语言的陷阱。通常,这些工具还可以自动修复您的代码。

  • 当代码健康度指标下降到预定义阈值以下或自动检测到的问题数量过高时创建通知。

此外,重要的是要保持一个拥抱并专注于良好代码健康的团队文化。领导可以通过多种方式支持这种文化。例如,您可以安排定期的修复周,在这些周内,开发人员专注于改善代码健康和修复未解决的错误,而不是添加新功能。您还可以通过奖金或其他形式的认可来支持团队内对代码健康的持续贡献。

重构

重构是保持代码库清洁和简单的最有效方法。即使健康的代码库偶尔也需要在扩展现有功能集、更改后端等情况下进行重构。

重构在处理旧的、继承的代码库时特别有用。重构的第一步是测量代码覆盖率,并将覆盖率提高到足够的水平。⁹一般来说,覆盖率越高,对重构的安全性的信心就越高。不幸的是,即使测试覆盖率达到 100%,也不能保证成功,因为测试可能没有意义。您可以通过其他类型的测试来解决这个问题,例如fuzzing,这在第十三章中有介绍。

注意

无论重构背后的原因是什么,您都应始终遵循一个黄金法则:永远不要在单个提交到代码存储库中混合重构和功能更改。重构更改通常很重要,可能难以理解。如果提交还包括功能更改,那么作者或审阅者可能会忽略错误的风险更高。

重构技术的完整概述超出了本书的范围。有关此主题的更多信息,请参阅 Martin Fowler 的优秀著作¹⁰以及 Wright 等人提供的自动化大规模重构工具的讨论(2013 年),¹¹ Wasserman(2013 年),¹²和 Potvin 和 Levenberg(2016 年)。

默认安全性和可靠性

除了使用具有强大保证的框架外,您还可以使用其他几种技术来自动改善应用程序的安全性和可靠性姿态,以及团队文化的姿态,您将在第二十一章中了解更多。

选择正确的工具

选择语言、框架和库是一项复杂的任务,通常受多种因素的影响,例如:

  • 与现有代码库的集成

  • 库的可用性

  • 开发团队的技能或偏好

要意识到语言选择对项目安全性和可靠性的巨大影响。

使用内存安全语言

在 2019 年 2 月的以色列 BlueHat 大会上,微软的 Matt Miller 声称,大约 70%的安全漏洞都是由内存安全问题引起的。¹³这个统计数据在过去至少 12 年中一直保持一致。

在 2016 年的一次演讲中,来自 Google 的 Nick Kralevich 报告称,Android 中 85%的所有错误(包括内核和其他组件中的错误)都是由内存管理错误引起的(第 54 页)。¹⁴ Kralevich 得出结论:“我们需要转向内存安全语言。”通过使用具有更高级内存管理的任何语言(如 Java 或 Go)而不是具有更多内存分配困难的语言(如 C/C++),您可以默认避免这整类安全(和可靠性)漏洞。或者,您可以使用代码消毒剂来检测大多数内存管理陷阱(请参阅“消毒您的代码”)。

使用强类型和静态类型检查

强类型语言中,“每当对象从调用函数传递到被调用函数时,其类型必须与被调用函数中声明的类型兼容。”没有这个要求的语言被称为弱类型松散类型。您可以在编译期间(静态类型检查)或运行时(动态类型检查)执行类型检查。

强类型和静态类型检查的好处在于在大型代码库中与多个开发人员合作时特别明显,因为您可以在编译时强制执行不变量,并消除各种错误,而不是在运行时。这导致在生产环境中更可靠的系统,更少的安全问题和更高性能的代码。

相比之下,在使用动态类型检查时(例如在 Python 中),除非代码具有 100%的测试覆盖率,否则您几乎无法推断代码的任何信息——这在原则上很好,但在实践中很少见。在弱类型语言中,推理代码变得更加困难,通常会导致意外行为。例如,在 JavaScript 中,每个文字默认都被视为字符串:[9, 8, 10].sort() -> [10, 8, 9]。在这两种情况下,由于不变量在编译时未被强制执行,您只能在测试期间捕获错误。因此,您更容易在生产环境中而不是在开发过程中检测到可靠性和安全性问题,特别是在较少频繁使用的代码路径中。

如果要使用默认具有动态类型检查或弱类型的语言,我们建议使用以下扩展来提高代码的可靠性。这些扩展提供了对更严格类型检查的支持,您可以逐步将它们添加到现有的代码库中:

使用强类型

使用未类型化的基元(例如字符串或整数)可能会导致以下问题:

  • 向函数传递概念上无效的参数

  • 不需要的隐式类型转换

  • 难以理解的类型层次结构

  • 令人困惑的测量单位

第一种情况——向函数传递概念上无效的参数——发生在函数参数的原始类型没有足够的上下文,并且在调用时变得令人困惑。例如:

  • 对于函数AddUserToGroup(string, string),不清楚组名是作为第一个参数还是第二个参数提供的。

  • Rectangle(3.14, 5.67)构造函数调用中,高度和宽度的顺序是什么?

  • Circle(double)是否期望半径还是直径?

文档可以纠正歧义,但开发人员仍然可能犯错。如果我们尽了责任,单元测试可以捕捉到大多数这些错误,但有些错误可能只在运行时出现。

使用强类型时,您可以在编译时捕捉到这些错误。回到我们之前的例子,所需的调用将如下所示:

  • Add(User("alice"), Group("root-users"))

  • Rectangle(Width(3.14), Height(5.67))

  • Circle(Radius(1.23))

其中UserGroupWidthHeightRadius是围绕字符串或双精度基元的强类型包装器。这种方法不太容易出错,并使代码更具自我说明性——在这种情况下,在第一个示例中,只需调用函数Add即可。

在第二种情况下,隐式类型转换可能导致以下情况:

  • 从较大的整数类型转换为较小的整数类型时的截断

  • 从较大的浮点类型转换为较小的浮点类型时的精度损失

  • 意外对象创建

在某些情况下,编译器将报告前两个问题(例如,在 C++中使用{}直接初始化语法时),但许多实例可能会被忽视。使用强类型可以保护您的代码免受编译器无法捕获的此类错误。

现在让我们考虑难以理解的类型层次结构的情况:

class Bar {  public:  Bar(bool is_safe) {...}  };  void Foo(const Bar& bar) {...}  Foo(false);  *// Likely OK, but is the developer aware a Bar object was created?*  Foo(5);      *// Will create Bar(is_safe := true), but likely by accident.*  Foo(NULL);   *// Will create Bar(is_safe := false), again likely by accident.*

这里的三个调用将编译并执行,但操作的结果是否符合开发人员的期望呢?默认情况下,C++编译器会尝试隐式转换(强制)参数以匹配函数参数类型。在这种情况下,编译器将尝试匹配类型Bar,它恰好有一个接受bool类型参数的单值构造函数。大多数 C++类型会隐式转换为bool

构造函数中的隐式转换有时是有意的(例如,将浮点值转换为std::complex类时),但在大多数情况下可能是危险的。为了防止危险的结果,至少要使单值构造函数显式——例如,explicit Bar(bool is_safe)。请注意,最后一次调用将导致编译错误,因为使用nullptr而不是NULL,因为没有到bool的隐式转换。

最后,单位混淆是错误的无尽源泉。这些错误可能被描述如下:

无害的

例如,设置一个 30 秒的定时器而不是 30 分钟,因为程序员不知道Timer(30)使用的单位。

危险的

例如,加拿大航空的“吉姆利滑翔机”飞机在地勤人员计算所需的燃料时使用的是磅而不是千克,导致它只有所需燃料的一半。

昂贵的

例如,科学家们失去了价值 1.25 亿美元的火星气候轨道飞行器,因为两个独立的工程团队使用了不同的测量单位(英制与公制)。

与以前一样,强类型是解决此问题的一种方法:它们可以封装单位,并且只表示抽象概念,如时间戳、持续时间或重量。这些类型通常实现以下功能:

明智的操作

例如,添加两个时间戳通常不是一个有用的操作,但是减去它们会返回一个对许多用例有用的持续时间。类似地,添加两个持续时间或重量也是有用的。

单位转换

例如,Timestamp::ToUnix, Duration::ToHours, Weight::ToKilograms

一些语言本身提供这样的抽象:例如 Go 中的time和即将到来的 C++20 标准中的chrono。其他语言可能需要专门的实现。

Fluent C++博客对 C++中强类型的应用和示例实现进行了更多讨论。

净化您的代码

自动验证代码是否遇到任何典型的内存管理或并发陷阱非常有用。您可以将这些检查作为每个更改列表的预提交操作运行,也可以作为持续构建和测试自动化工具的一部分运行。要检查的陷阱列表取决于语言。本节介绍了 C++和 Go 的一些解决方案。

C++:Valgrind 或 Google Sanitizers

C++允许进行低级内存管理。正如我们之前提到的,内存管理错误是安全问题的主要原因,并且可能导致以下故障场景:

  • 读取未分配的内存(new之前或delete之后)

  • 读取超出分配内存范围的内容(缓冲区溢出攻击场景)

  • 读取未初始化的内存

  • 当系统丢失已分配内存的地址或不及时释放未使用的内存时,会发生内存泄漏

Valgrind是一个流行的框架,允许开发人员捕捉这些类型的错误,即使单元测试没有捕捉到它们。Valgrind 的好处在于提供了一个解释用户二进制的虚拟机,因此用户无需重新编译代码即可使用它。Valgrind 工具Helgrind还可以检测常见的同步错误,例如:

  • 对 POSIX pthreads API 的误用(例如,解锁未锁定的互斥锁,或者由另一个线程持有的互斥锁)

  • 由于锁定顺序问题而产生的潜在死锁

  • 由于访问内存而未进行充分锁定或同步而引起的数据竞争

或者,Google Sanitizers 套件提供了各种组件,可以检测 Valgrind 的 Callgrind(缓存和分支预测分析器)可以检测到的所有相同问题:

  • AddressSanitizer (ASan)检测内存错误(缓冲区溢出,释放后使用,不正确的初始化顺序)。

  • LeakSanitizer (LSan)检测内存泄漏。

  • MemorySanitizer (MSan)检测系统是否正在读取未初始化的内存。

  • ThreadSanitizer (TSan)检测数据竞争和死锁。

  • UndefinedBehaviorSanitizer (UBSan)检测具有未定义行为的情况(使用未对齐的指针;有符号整数溢出;转换到、从或在浮点类型之间溢出目标)。

Google Sanitizers 套件的主要优势是速度:它比 Valgrind 快高达 10 倍。像CLion这样的流行 IDE 还提供了与 Google Sanitizers 的一流集成。下一章将更详细地介绍 sanitizers 和其他动态程序分析工具。

Go:竞争检测器

虽然 Go 旨在禁止 C++典型的内存损坏问题,但仍可能受到数据竞争条件的影响。Go 竞争检测器可以检测这些条件。

结论

本章介绍了几个指导开发人员设计和实现更安全可靠代码的原则。特别是,我们建议使用框架作为一种强大的策略,因为它们重用了已被证明对于代码的敏感区域(身份验证、授权、日志记录、速率限制和分布式系统中的通信)的建设块:框架还倾向于提高开发人员的生产力,无论是编写框架的人还是使用框架的人,并使对代码的推理变得更加容易。编写安全可靠代码的其他策略包括追求简单性,选择合适的工具,使用强类型而不是原始类型,并持续对代码进行消毒。

在编写软件时,额外投入精力改善安全性和可靠性将在长期内得到回报,并减少您在部署应用程序后需要花费的审查应用程序或修复问题的工作量。

¹ 有关级联故障的更多信息,请参见SRE 书的第 22 章

² 也在SRE 书的第 22 章中有描述。

³ 有关该系统的更多详细信息,请参见 Kern, Christoph. 2014. “保护纠缠不清的网络。” ACM 通讯 57(9): 38–47. https://oreil.ly/drZss.

⁴ 请参见 Bessey, Al 等人。2010. “数十亿行代码之后:使用静态分析在现实世界中查找错误。” ACM 通讯 53(2): 66–75. doi:10.1145/1646353.1646374.

⁵ Potvin, Rachel, and Josh Levenberg. 2016. “为什么 Google 将数十亿行代码存储在单个存储库中。” ACM 通讯 59(7): 78–87. doi:10.1145/2854146.

⁶ 例如,Ousterhout, John. 2018. 软件设计哲学. Palo Alto, CA: Yaknyam Press.

⁷ 来源:Karpilovsky, Elliott. 2017. “代码健康:减少嵌套,减少复杂性。” https://oreil.ly/PO1QR.

⁸ 来源:Eaddy, Marc. 2017. “代码健康:消除 YAGNI 气味。” https://oreil.ly/NYr7y.

⁹ 有很多代码覆盖工具可供选择。有关概述,请参阅Stackify 上的列表

¹⁰ Fowler, Martin. 2019 年。 重构:改善现有代码的设计。马萨诸塞州波士顿:Addison-Wesley。

¹¹ Wright, Hyrum 等。 2013 年。 “使用 Clang 进行大规模自动重构。” 软件维护国际会议第 29 届论文集:548–551。 doi:10.1109/ICSM.2013.93。

¹² Wasserman, Louis. 2013 年。 “可扩展的基于示例的重构与 Refaster。” 2013 年重构工具 ACM 研讨会论文集:25–28 doi:10.1145/2541348.2541355。

¹³ Miller, Matt. 2019 年。 “软件漏洞缓解领域的趋势、挑战和战略转变。” BlueHat IL。 https://goo.gl/vKM7uQ

¹⁴ Kralevich, Nick. 2016 年。 “防御的艺术:漏洞如何塑造 Android 中的安全功能和缓解措施。” BlackHat。 https://oreil.ly/16rCq

¹⁵ Liskov, Barbara 和 Stephen Zilles。 1974 年。 “使用抽象数据类型进行编程。” ACM SIGPLAN 非常高级语言研讨会论文集:50–59。 doi:10.1145/800233.807045。

¹⁶ 有关 JavaScript 和 Ruby 中更多的惊喜,请参见Gary Bernhardt 在 CodeMash 2012 的闪电演讲

¹⁷ 参见 Eaddy, Mark. 2017 年。 “代码健康:对基元着迷?” https://oreil.ly/0DvJI

第十三章:测试代码

原文:13. Testing Code

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Phil Ames 和 Franjo Ivančić

作者:Vera Haas 和 Jen Barnason

无论开发软件的工程师多么小心,都不可避免地会出现一些错误和被忽视的边缘情况。意外的输入组合可能会触发数据损坏或导致像 SRE 书的第 22 章中的“死亡查询”示例中的可用性问题。编码错误可能会导致安全问题,如缓冲区溢出和跨站脚本漏洞。简而言之,在现实世界中,软件容易出现许多故障。

本章讨论的技术在软件开发的不同阶段和环境中具有各种成本效益概况。例如,模糊测试——向系统发送随机请求——可以帮助您在安全性和可靠性方面加固系统。这种技术可能有助于捕捉信息泄漏,并通过暴露服务于大量边缘情况来减少服务错误。要识别无法轻松快速修补的系统中的潜在错误,您可能需要进行彻底的前期测试。

单元测试

单元测试可以通过在发布之前找出个别软件组件中的各种错误来提高系统安全性和可靠性。这种技术涉及将软件组件分解为没有外部依赖关系的更小、自包含的“单元”,然后对每个单元进行测试。单元测试由编写测试的工程师选择的不同输入来执行给定单元的代码组成。许多语言都有流行的单元测试框架;基于xUnit架构的系统非常常见。

遵循 xUnit 范例的框架允许通用的设置和拆卸代码与每个单独的测试方法一起执行。这些框架还定义了各个测试框架组件的角色和职责,有助于标准化测试结果格式。这样,其他系统就可以详细了解到底出了什么问题。流行的例子包括 Java 的 JUnit,C++的 GoogleTest,Golang 的 go2xunit,以及 Python 中内置的unittest模块。

示例 13-1 是使用 GoogleTest 框架编写的简单单元测试

示例 13-1。使用 GoogleTest 框架编写检查提供的参数是否为质数的函数的单元测试
TEST(IsPrimeTest, Trivial) {
  EXPECT_FALSE(IsPrime(0));
  EXPECT_FALSE(IsPrime(1));
  EXPECT_TRUE(IsPrime(2));
  EXPECT_TRUE(IsPrime(3));
}

单元测试通常作为工程工作流程的一部分在本地运行,以便在开发人员提交更改到代码库之前为他们提供快速反馈。在持续集成/持续交付(CI/CD)流水线中,单元测试通常在提交合并到存储库的主干分支之前运行。这种做法旨在防止破坏其他团队依赖的行为的代码更改。

编写有效的单元测试

单元测试的质量和全面性可以显著影响软件的健壮性。单元测试应该快速可靠,以便工程师立即得到反馈,了解更改是否破坏了预期的行为。通过编写和维护单元测试,您可以确保工程师在添加新功能和代码时不会破坏相关测试覆盖的现有行为。如第九章所讨论的,您的测试还应该是隔离的——如果测试无法在隔离的环境中重复产生相同的结果,您就不能完全依赖测试结果。

考虑一个管理团队在给定数据中心或区域可以使用的存储字节数的系统。假设该系统允许团队在数据中心有可用的未分配字节时请求额外的配额。一个简单的单元测试可能涉及验证在由虚构团队部分占用的虚构集群中请求配额的情况,拒绝超出可用存储容量的请求。以安全为重点的单元测试可能检查涉及负字节数的请求是如何处理的,或者代码如何处理容量溢出,例如导致接近用于表示它们的变量类型的限制的大型转移。另一个单元测试可能检查系统在发送恶意或格式不正确的输入时是否返回适当的错误消息。

经常有用的是使用不同的参数或环境数据对相同的代码进行测试,例如我们示例中的初始起始配额使用情况。为了最小化重复的代码量,单元测试框架或语言通常提供一种以不同参数调用相同测试的方法。这种方法有助于减少重复的样板代码,从而使重构工作变得不那么乏味。

何时编写单元测试

一个常见的策略是在编写代码后不久编写测试,使用测试来验证代码的预期性能。这些测试通常与新代码一起提交,并且通常包括工程师手动检查的情况。例如,我们的示例存储管理应用程序可能要求“只有拥有服务的组的计费管理员才能请求更多的配额。”您可以将这种要求转化为几个单元测试。

在进行代码审查的组织中,同行审查者可以再次检查测试,以确保它们足够健壮,以维护代码库的质量。例如,审阅者可能会注意到,尽管新的测试伴随着变化,但即使删除或停用新代码,测试也可能通过。如果审阅者可以在新代码中用if (false)if (true)替换类似if (condition_1 || condition_2)的语句,并且没有新的测试失败,那么测试可能已经忽略了重要的测试用例。有关 Google 自动化这种突变测试的经验的更多信息,请参见 Petrović和 Ivanković(2018)。^([2](ch13.html#ch13fn2))

测试驱动开发(TDD)方法鼓励工程师根据已建立的需求和预期行为在编写代码之前编写单元测试,而不是在编写代码之后编写测试。在测试新功能或错误修复时,测试将在行为完全实现之前失败。一旦功能实现并且测试通过,工程师就会进入下一个功能,然后该过程重复。

对于没有使用 TDD 模型构建的现有项目,通常会根据错误报告或积极努力增加对系统的信心来慢慢整合和改进测试覆盖率。但即使您实现了全面覆盖,您的项目也不一定是无错误的。未知的边缘情况或稀疏实现的错误处理仍可能导致不正确的行为。

您还可以根据内部手动测试或代码审查工作编写单元测试。您可能会在标准开发和审查实践中编写这些测试,或者在像发布前的安全审查这样的里程碑期间编写。新的单元测试可以验证建议的错误修复是否按预期工作,并且以后的重构不会重新引入相同的错误。如果代码难以理解并且潜在的错误会影响安全性,例如在编写具有复杂权限模型的系统中的访问控制检查时,这种类型的测试尤为重要。

注意

为了尽可能涵盖多种情景,你通常会花费更多时间编写测试而不是编写被测试的代码,特别是在处理非平凡系统时。这额外的时间从长远来看是值得的,因为早期测试会产生质量更高的代码库,减少需要调试的边缘情况。

单元测试如何影响代码

为了改进测试的全面性,你可能需要设计新的代码来包含测试规定,或者重构旧代码使其更易于测试。通常,重构涉及提供拦截对外部系统的调用的方法。利用这种内省能力,你可以以各种方式测试代码,例如验证代码调用拦截器的次数是否正确,或者使用正确的参数。

考虑一下如何测试一段代码,当满足某些条件时在远程问题跟踪器中打开票证。每次单元测试运行时创建一个真实的票证会产生不必要的噪音。更糟糕的是,如果问题跟踪系统不可用,这种测试策略可能会随机失败,违反了快速、可靠测试结果的目标。

要重构这段代码,你可以删除对问题跟踪服务的直接调用,并用一个抽象来替换这些调用,例如一个IssueTrackerService对象的接口。用于测试的实现可以在接收到“创建问题”等调用时记录数据,测试可以检查元数据以做出通过或失败的结论。相比之下,生产实现将连接到远程系统并调用公开的 API 方法。

这种重构大大减少了依赖于现实世界系统的测试的“不稳定性”。因为它们依赖于不能保证的行为,比如外部依赖性,或者从某些容器类型中检索项目时元素的顺序,不稳定的测试通常更像是一种麻烦而不是帮助。尽量在出现不稳定的测试时进行修复;否则,开发人员可能会养成在提交更改时忽略测试结果的习惯。

注意

这些抽象及其相应的实现被称为模拟存根伪装。工程师有时会将这些词用法混淆,尽管这些概念在实现复杂性和功能上有所不同,因此确保你的组织中的每个人都使用一致的词汇是很重要的。如果你进行代码审查或使用风格指南,你可以通过提供团队可以对齐的定义来帮助减少混淆。

很容易陷入过度抽象的陷阱,测试断言关于函数调用顺序或它们的参数的机械事实。过度抽象的测试通常并不提供太多价值,因为它们往往“测试”语言的控制流实现,而不是你关心的系统的行为。

如果每次方法更改时都必须完全重写测试,你可能需要重新考虑测试,甚至是系统本身的架构。为了避免不断重写测试,你可以考虑要求熟悉服务的工程师为任何非平凡的测试需求提供合适的虚拟实现。这种解决方案对于负责系统的团队和测试代码的工程师都是有利的:拥有抽象的团队可以确保它跟踪服务的功能集随着其发展的变化,而使用抽象的团队现在有了一个更真实的组件用于测试。

集成测试

集成测试超越了单个单元和抽象,用真实的实现替换了抽象的假或存根实现,如数据库或网络服务。因此,集成测试涵盖了更完整的代码路径。由于你必须初始化和配置这些其他依赖项,集成测试可能比单元测试更慢、更不稳定——执行测试时,这种方法会将网络延迟等真实世界变量纳入其中,因为服务端到端地进行通信。当你从测试代码的单个低级单元转移到测试它们在组合在一起时的交互方式时,最终结果是对系统行为符合预期的更高程度的信心。

集成测试采用不同的形式,这取决于它们所涉及的依赖项的复杂性。当集成测试需要的依赖相对简单时,集成测试可能看起来像一个设置了一些共享依赖项(例如,处于预配置状态的数据库)的基类,其他测试从中继承。随着服务复杂性的增加,集成测试可能变得更加复杂,需要监督系统来编排依赖项的初始化或设置,以支持测试。谷歌有专门致力于基础设施的团队,为常见的基础设施服务提供标准化的集成测试设置。对于使用像Jenkins这样的持续构建和交付系统的组织,集成测试可以根据代码库的大小和项目中可用测试的数量,与单元测试一起运行,或者单独运行。

注意

在构建集成测试时,请牢记第五章中讨论的原则:确保测试的数据和系统访问要求不会引入安全风险。诱人的做法是将实际数据库镜像到测试环境中,因为数据库提供了丰富的真实数据,但你应该避免这种反模式,因为它们可能包含敏感数据,将对使用这些数据库运行测试的任何人都可用。这种实现与最小特权原则不一致,可能会带来安全风险。相反,你可以使用非敏感的测试数据来填充这些系统。这种方法还可以轻松地将测试环境清除到已知的干净状态,减少集成测试不稳定性的可能性。

编写有效的集成测试

与单元测试一样,集成测试可能受到代码中设计选择的影响。继续我们之前关于问题跟踪器的例子,一个单元测试模拟可能只是断言该方法被调用以向远程服务提交一个问题。而集成测试更可能使用一个真实的客户端库。与其在生产中创建虚假的错误,集成测试会与 QA 端点进行通信。测试用例将使用触发对 QA 实例的调用的输入来执行应用逻辑。监督逻辑随后可以查询 QA 实例,以验证从端到端的角度成功地进行了外部可见的操作。

了解为什么集成测试失败,而所有单元测试都通过可能需要大量的时间和精力。在集成测试的关键逻辑交汇处进行良好的日志记录可以帮助你调试和理解故障发生的位置。还要记住,因为集成测试超越了单个单元,检查组件之间的交互,它们只能告诉你有关这些单元在其他场景中是否符合你的期望的有限信息。这是在开发生命周期中使用每种类型的测试的许多原因之一,因为一种测试通常不能替代另一种。

深入探讨:动态程序分析

程序分析允许用户执行许多有用的操作,例如性能分析、检查与安全相关的正确性、代码覆盖报告和死代码消除。正如本章后面讨论的那样,您可以静态地执行程序分析来研究软件而不执行它。在这里,我们关注动态方法。动态程序分析通过运行程序来分析软件,可能在虚拟化或模拟环境中,用于除了测试之外的目的。

性能分析器(用于发现程序中的性能问题)和代码覆盖报告生成器是最常见的动态分析类型。上一章介绍了动态程序分析工具Valgrind,它提供了一个虚拟机和各种工具来解释二进制代码,并检查执行是否存在各种常见的错误。本节重点介绍依赖于编译器支持(通常称为instrumentation)来检测与内存相关的错误的动态分析方法。

编译器和动态程序分析工具允许您配置仪器化,以收集编译器生成的二进制文件的运行时统计信息,例如性能分析信息、代码覆盖信息和基于配置的优化。当二进制文件执行时,编译器插入额外的指令和回调到后端运行时库,以显示和收集相关信息。在这里,我们关注 C/C++程序的安全相关内存误用错误。

Google Sanitizers 套件提供了基于编译的动态分析工具。它们最初作为LLVM编译器基础设施的一部分开发,用于捕获常见的编程错误,并且现在也得到了 GCC 和其他编译器的支持。例如,AddressSanitizer (ASan)可以在 C/C++程序中找到许多常见的与内存相关的错误,比如越界内存访问。其他流行的 sanitizers 包括以下内容:

UndefinedBehaviorSanitizer

执行未定义行为的运行时标记

ThreadSanitizer

检测竞争条件

MemorySanitizer

检测未初始化内存的读取

LeakSanitizer

检测内存泄漏和其他类型的泄漏

随着新的硬件功能允许对内存地址进行标记,有提案利用这些新功能进一步提高 ASan 的性能。

ASan 通过构建程序分析的自定义仪器化二进制文件来提供快速性能。在编译过程中,ASan 添加了某些指令,以便调用提供的 sanitizer 运行时。运行时维护有关程序执行的元数据,例如哪些内存地址是有效的访问。ASan 使用影子内存来记录给定字节对程序访问是否安全,并使用编译器插入的指令在程序尝试读取或写入该字节时检查影子内存。它还提供自定义内存分配和释放(mallocfree)实现。例如,malloc函数在返回请求的内存区域之前立即分配额外的内存。这创建了一个缓冲内存区域,使 ASan 能够轻松报告关于溢出和下溢的精确信息。为此,ASan 将这些区域(也称为red zones)标记为poisoned。同样,ASan 将已释放的内存标记为poisoned,使您能够轻松捕获使用后释放的错误。

以下示例说明了使用 Clang 编译器运行 ASan 的简单过程。shell 命令对具有使用后释放错误的特定输入文件进行插装和运行。当读取先前释放的内存区域的内存地址时,将发生使用后释放错误。安全漏洞可以利用这种类型的访问作为构建块。选项-fsanitize=address打开 ASan 插装:

$ cat -n use-after-free.c
 1  #include <stdlib.h>
 2  int main() {
 3    char *x = (char*)calloc(10, sizeof(char));
 4    free(x);
 5    return x[5];
 6  }

$ clang -fsanitize=address -O1 -fno-omit-frame-pointer -g use-after-free.c

编译完成后,当执行生成的二进制文件时,我们可以看到 ASan 生成的错误报告。(为了简洁起见,我们省略了完整的 ASan 错误消息。)请注意,ASan 允许错误报告指示源文件信息,例如行号,使用 LLVM 符号化程序,如 Clang 文档中的“符号化报告”部分所述。正如您在输出报告中所看到的,ASan 发现了一个 1 字节的使用后释放读取访问(已强调)。错误消息包括原始分配、释放和随后的非法使用的信息:

% ./a.out
=================================================================
==142161==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000015 
at pc 0x00000050b550 bp 0x7ffc5a603f70 sp 0x7ffc5a603f68
READ of size 1 at 0x602000000015 thread T0
    #0 0x50b54f in main use-after-free.c:5:10
    #1 0x7f89ddd6452a in __libc_start_main 
    #2 0x41c049 in _start 

0x602000000015 is located 5 bytes inside of 10-byte region [0x602000000010,0x60200000001a)
freed by thread T0 here:
    #0 0x4d14e8 in free 
    #1 0x50b51f in main use-after-free.c:4:3
    #2 0x7f89ddd6452a in __libc_start_main 

previously allocated by thread T0 here:
    #0 0x4d18a8 in calloc 
    #1 0x50b514 in main use-after-free.c:3:20
    #2 0x7f89ddd6452a in __libc_start_main 

SUMMARY: AddressSanitizer: heap-use-after-free use-after-free.c:5:10 in main
[...]
==142161==ABORTING

深入探讨:模糊测试

模糊测试(通常称为模糊测试)是一种补充先前提到的测试策略的技术。模糊测试涉及使用模糊引擎(或模糊器)生成大量候选输入,然后通过模糊驱动程序传递给模糊目标(处理输入的代码)。然后,模糊器分析系统如何处理输入。各种软件处理的复杂输入都是模糊测试的热门目标,例如文件解析器、压缩算法实现、网络协议实现和音频编解码器。

您还可以使用模糊测试来评估相同功能的不同实现。例如,如果您正在考虑从库 A 迁移到库 B,模糊器可以生成输入,将其传递给每个库进行处理,并比较结果。模糊器可以将任何不匹配的结果报告为“崩溃”,这有助于工程师确定可能导致微妙行为变化的原因。这种在不同输出上崩溃的操作通常作为模糊驱动程序的一部分实现,如在 OpenSSL 的BigNum 模糊器中所见。⁵

由于模糊测试可能会无限期地执行,因此不太可能阻止每次提交都要进行扩展测试的结果。这意味着当模糊器发现错误时,该错误可能已经被检入。理想情况下,其他测试或分析策略将首先防止错误发生,因此模糊测试通过生成工程师可能没有考虑到的测试用例来作为补充。作为额外的好处,另一个单元测试可以使用在模糊目标中识别错误的生成输入样本,以确保后续更改不会使修复退化。

模糊引擎的工作原理

模糊引擎的复杂性和精密度可以有所不同。在光谱的低端,一种通常称为愚蠢模糊的技术简单地从随机数生成器中读取字节,并将它们传递给模糊目标,以寻找错误。通过与编译器工具链的集成,模糊引擎变得越来越智能。它们现在可以利用先前讨论的编译器插装功能生成更有趣和有意义的样本。在工业实践中,使用尽可能多的模糊引擎集成到构建工具链中,并监视代码覆盖的百分比等指标被认为是一种良好的做法。如果代码覆盖在某个点停滞不前,通常值得调查为什么模糊器无法到达其他区域。

一些模糊引擎接受来自规范或语法的有趣关键字字典,这些规范或语法来自规范良好的协议、语言和格式(如 HTTP、SQL 和 JSON)。 模糊引擎可以生成可能被测试程序接受的输入,因为如果输入包含非法关键字,则生成的解析器代码可能会简单地拒绝输入。 提供字典可以增加通过模糊测试达到实际想要测试的代码的可能性。 否则,您可能最终会执行基于无效标记拒绝输入的代码,并且永远找不到任何有趣的错误。

Peach Fuzzer这样的模糊引擎允许模糊驱动程序作者以编程方式定义输入的格式和字段之间的预期关系,因此模糊引擎可以生成违反这些关系的测试用例。 模糊引擎通常还接受一组示例输入文件,称为种子语料库,这些文件代表了被模糊化的代码所期望的内容。 然后,模糊引擎会改变这些种子输入,以及执行任何其他支持的输入生成策略。 一些软件包包含示例文件(例如音频库的 MP3 文件或图像处理的 JPEG 文件)作为其现有测试套件的一部分 - 这些示例文件非常适合作为种子语料库的候选文件。 否则,您可以从真实世界或手动生成的文件中策划种子语料库。 安全研究人员还会发布流行文件格式的种子语料库,例如以下提供的种子语料库:

近年来,编译器工具链的改进已经在使更智能的模糊引擎方面取得了重大进展。 对于 C/C++,例如 LLVM Clang 等编译器可以对代码进行仪器化(如前所述),以允许模糊引擎观察处理特定样本输入时执行的代码。 当模糊引擎找到新的代码路径时,它会保留触发代码路径的样本,并使用它们来生成未来的样本。 其他语言或模糊引擎可能需要特定的编译器 - 例如AFL的 afl-gcc 或go-fuzz 引擎的 go-fuzz-build,以正确跟踪执行路径以增加代码覆盖率。

当模糊引擎生成触发经过消毒处理的代码路径中的崩溃的输入时,它会记录输入以及从程序中提取的元数据,这些元数据包括诸如堆栈跟踪(指示触发崩溃的代码行)或进程在那个时间的内存布局等信息。 此信息为工程师提供了有关崩溃原因的详细信息,这有助于他们了解其性质、准备修复或优先处理错误。 例如,当组织考虑如何为不同类型的问题设置优先级时,内存读取访问违规可能被认为比写入访问违规不太重要。 这种优先级有助于建立安全和可靠的文化(参见第二十一章)。

当模糊引擎触发潜在错误时,程序对其做出反应的方式取决于各种各样的情况。 如果遇到错误会触发一致和明确定义的事件,例如接收信号或在发生内存损坏或未定义行为时执行特定函数,那么模糊引擎最有效地检测错误。 这些函数可以在系统达到特定错误状态时明确地向模糊引擎发出信号。 之前提到的许多消毒剂都是这样工作的。

一些模糊引擎还允许您为处理特定生成的输入设置一个上限时间。例如,如果死锁或无限循环导致输入超过时间限制,模糊器将把样本归类为“崩溃”。它还保存该样本以供进一步调查,以便开发团队可以防止可能导致服务不可用的 DoS 问题。

通过使用正确的模糊驱动程序和消毒剂,可以相对快速地识别导致 Web 服务器泄漏内存(包括包含 TLS 证书或 Cookie 的内存)的 Heartbleed 漏洞(CVE-2014-0160)。Google 的fuzzer-test-suite GitHub 存储库包含一个演示成功识别该漏洞的 Dockerfile 示例。以下是 Heartbleed 漏洞的 ASan 报告摘录,由消毒剂编译器插件插入的__asan_memcpy函数调用触发(重点添加):

==19==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x629000009748 at pc 
0x0000004e59c9 bp 0x7ffe3a541360 sp 0x7ffe3a540b10
READ of size 65535 at 0x629000009748 thread T0
    #0 0x4e59c8 in __asan_memcpy /tmp/final/llvm.src/projects/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cc:23:3
    #1 0x522e88 in tls1_process_heartbeat /root/heartbleed/BUILD/ssl/t1_lib.c:2586:3
    #2 0x58f94d in ssl3_read_bytes /root/heartbleed/BUILD/ssl/s3_pkt.c:1092:4
    #3 0x59418a in ssl3_get_message /root/heartbleed/BUILD/ssl/s3_both.c:457:7
    #4 0x55f3c7 in ssl3_get_client_hello /root/heartbleed/BUILD/ssl/s3_srvr.c:941:4
    #5 0x55b429 in ssl3_accept /root/heartbleed/BUILD/ssl/s3_srvr.c:357:9
    #6 0x51664d in LLVMFuzzerTestOneInput /root/FTS/openssl-1.0.1f/target.cc:34:3
[...]

0x629000009748 is located 0 bytes to the right of 17736-byte region [0x629000005200,
0x629000009748)
allocated by thread T0 here:
    #0 0x4e68e3 in __interceptor_malloc /tmp/final/llvm.src/projects/compiler-rt/lib/asan/asan_malloc_linux.cc:88:3
    #1 0x5c42cb in CRYPTO_malloc /root/heartbleed/BUILD/crypto/mem.c:308:8
    #2 0x5956c9 in freelist_extract /root/heartbleed/BUILD/ssl/s3_both.c:708:12
    #3 0x5956c9 in ssl3_setup_read_buffer /root/heartbleed/BUILD/ssl/s3_both.c:770
    #4 0x595cac in ssl3_setup_buffers /root/heartbleed/BUILD/ssl/s3_both.c:827:7
    #5 0x55bff4 in ssl3_accept /root/heartbleed/BUILD/ssl/s3_srvr.c:292:9
    #6 0x51664d in LLVMFuzzerTestOneInput /root/FTS/openssl-1.0.1f/target.cc:34:3
[...]

输出的第一部分描述了问题的类型(在本例中是“堆缓冲区溢出”——具体来说是读取访问违规)和一个易于阅读的符号化堆栈跟踪,指向读取超出分配的缓冲区大小的代码行。第二部分包含了有关附近内存区域的元数据,以及如何分配这些元数据,以帮助工程师分析问题并了解进程如何达到无效状态。

编译器和消毒剂仪器使这种分析成为可能。然而,这种仪器有限:当软件的某些部分是手写汇编以提高性能时,使用消毒剂进行模糊处理效果不佳。编译器无法对汇编代码进行仪器化,因为消毒剂插件在更高层操作。因此,未被仪器化的手写汇编代码可能会导致误报或未检测到的错误。

完全不使用消毒剂进行模糊处理是可能的,但会降低您检测无效程序状态和分析崩溃时可用的元数据的能力。例如,如果您不使用消毒剂,为了使模糊处理产生任何有用的信息,程序必须遇到“未定义行为”场景,然后将此错误状态通知外部模糊引擎(通常是通过崩溃或退出)。否则,未定义的行为将继续未被检测。同样,如果您不使用 ASan 或类似的仪器,您的模糊器可能无法识别内存已被损坏但未被使用以导致操作系统终止进程的状态。

如果您正在使用仅以二进制形式提供的库,编译器仪器化就不是一个选择。一些模糊引擎,如 American Fuzzy Lop,还与处理器模拟器(如 QEMU)集成,以在 CPU 级别仪器化有趣的指令。这种集成可能是一个吸引人的选择,用于模糊化需要模糊化的仅以二进制形式提供的库,但会降低速度。这种方法允许模糊引擎了解生成的输入可能触发的代码路径,但与使用编译器添加的消毒剂指令构建的源代码构建一样,它不提供太多的错误检测帮助。

许多现代模糊引擎,如libFuzzerAFLHonggfuzz,使用先前描述的技术的某种组合,或这些技术的变体。可以构建一个单一的模糊驱动程序,可以与多个模糊引擎一起使用。在使用多个模糊引擎时,最好确保定期将每个引擎生成的有趣输入样本移回其他模糊引擎配置为使用的种子语料库中。一个引擎可能成功地接受另一个引擎生成的输入,对其进行变异,并触发崩溃。

编写有效的模糊驱动程序

为了使这些模糊概念更具体,我们将更详细地介绍使用 LLVM 的 libFuzzer 引擎提供的框架编写模糊驱动程序的步骤,该引擎包含在 Clang 编译器中。这个特定的框架很方便,因为其他模糊引擎(如 Honggfuzz 和 AFL)也可以使用 libFuzzer 入口点。作为模糊器作者,使用这个框架意味着您只需要编写一个实现函数原型的驱动程序:

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);

随后的模糊引擎将生成字节序列并调用您的驱动程序,该驱动程序可以将输入传递给您想要测试的代码。

模糊引擎的目标是通过驱动尽快执行模糊目标,并生成尽可能多的独特和有趣的输入。为了实现可重现的崩溃和快速模糊,请尽量避免在模糊驱动程序中出现以下情况:

  • 非确定性行为,比如依赖随机数生成器或特定的多线程行为。

  • 慢操作,如控制台日志记录或磁盘 I/O。相反,考虑创建“模糊器友好”的构建,禁用这些慢操作,或者使用基于内存的文件系统。

  • 故意崩溃。模糊测试的理念是找到你没有意图发生的崩溃。模糊引擎无法区分故意的崩溃。

这些属性对于本章描述的其他类型的测试也可能是理想的。

您还应该避免任何对手可以在生成的输入样本中“修复”的专门完整性检查(如 CRC32 或消息摘要)。模糊引擎不太可能生成有效的校验和,并在没有专门逻辑的情况下通过完整性检查。一个常见的约定是使用编译器预处理器标志,如-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION,以启用这种模糊器友好的行为,并帮助通过模糊测试识别出的崩溃。

一个示例模糊器

本节遵循编写一个名为Knusperli的简单开源 C++库的模糊器的步骤。Knusperli 是一个 JPEG 解码器,如果它对用户上传的内容进行编码或处理来自网络的图像(包括潜在的恶意图像),可能会看到各种各样的输入。

Knusperli 还为我们提供了一个方便的接口来进行模糊测试:一个接受字节序列(JPEG)和大小参数的函数,以及一个控制解析图像的哪些部分的参数。对于不提供这样直接接口的软件,您可以使用辅助库,如FuzzedDataProvider,来帮助将字节序列转换为目标接口的有用值。我们的示例模糊驱动程序针对这个函数

bool ReadJpeg(const uint8_t* data, const size_t len, JpegReadMode mode, 
              JPEGData* jpg);

Knusperli 使用Bazel 构建系统。通过修改.bazelrc文件,您可以创建一个方便的快捷方式来使用各种消毒剂构建目标,并直接构建基于 libFuzzer 的模糊器。以下是 ASan 的示例:

$ cat ~/.bazelrc
build:asan --copt -fsanitize=address --copt -O1 --copt -g -c dbg
build:asan --linkopt -fsanitize=address --copt -O1 --copt -g -c dbg
build:asan --copt -fno-omit-frame-pointer --copt -O1 --copt -g -c dbg

在这一点上,您应该能够构建启用了 ASan 的工具版本:

$ CC=clang-6.0 CXX=clang++-6.0 bazel build --config=asan :knusperli

您还可以为我们即将编写的模糊器在BUILD文件中添加规则:

cc_binary(
    name = "fuzzer",
    srcs = [
        "jpeg_decoder_fuzzer.cc",
    ],
    deps = [
        ":jpeg_data_decoder",
        ":jpeg_data_reader",
    ],
    linkopts = ["-fsanitize=address,fuzzer"],
)

示例 13-2 展示了模糊驱动程序的简单尝试可能是什么样子。

示例 13-2. jpeg_decoder_fuzzer.cc
 1  #include <cstddef>
 2  #include <cstdint>
 3  #include "jpeg_data_decoder.h"
 4  #include "jpeg_data_reader.h"
 5  
 6  extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t sz) {
 7    knusperli::JPEGData jpg;
 8    knusperli::ReadJpeg(data, sz, knusperli::JPEG_READ_HEADER, &jpg);
 9      return 0;
10  }

我们可以使用以下命令构建和运行模糊驱动程序:

$ CC=clang-6.0 CXX=clang++-6.0 bazel build --config=asan :fuzzer
$ mkdir synthetic_corpus
$ ASAN_SYMBOLIZER_PATH=/usr/lib/llvm-6.0/bin/llvm-symbolizer bazel-bin/fuzzer \
 -max_total_time 300 -print_final_stats synthetic_corpus/

上述命令在使用空输入语料库的情况下运行模糊器五分钟。LibFuzzer 将有趣的生成样本放在synthetic_corpus/目录中,以便在未来的模糊会话中使用。您将收到以下结果:

[...]
INFO:        0 files found in synthetic_corpus/
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 
4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2      INITED cov: 110 ft: 111 corp: 1/1b exec/s: 0 rss: 36Mb
[...]
#3138182       DONE   cov: 151 ft: 418 corp: 30/4340b exec/s: 10425 rss: 463Mb
[...]
Done 3138182 runs in 301 second(s)
stat::number_of_executed_units: 3138182
stat::average_exec_per_sec:     10425
stat::new_units_added:          608
stat::slowest_unit_time_sec:    0
stat::peak_rss_mb:              463

添加 JPEG 文件(例如,在广播电视上看到的彩条图案)到种子语料库中也会带来改进。这个单一的种子输入带来了执行的代码块的>10%的改进(cov指标):

#2      INITED cov: 169 ft: 170 corp: 1/8632b exec/s: 0 rss: 37Mb

为了进一步达到更多的代码,我们可以使用不同的值来设置JpegReadMode参数。有效值如下:

enum JpegReadMode {
  JPEG_READ_HEADER,   *// only basic headers*
  JPEG_READ_TABLES,   *// headers and tables (quant, Huffman, ...)*
  JPEG_READ_ALL,      *// everything*
};

与编写三种不同的模糊器不同,我们可以对输入的子集进行哈希处理,并使用该结果在单个模糊器中执行不同组合的库特性。要小心使用足够的输入来创建多样化的哈希输出。如果文件格式要求输入的前N个字节都看起来相同,那么在决定哪些字节会影响设置哪些选项时,请至少使用比N多一个的字节。

其他方法包括使用先前提到的FuzzedDataProvider来分割输入,或者将输入的前几个字节专门用于设置库参数。然后,剩余的字节作为输入传递给模糊目标。与对输入进行哈希处理可能会导致不同的配置,如果单个输入位发生变化,那么将输入拆分的替代方法允许模糊引擎更好地跟踪所选选项与代码行为方式之间的关系。要注意这些不同方法如何影响潜在现有种子输入的可用性。在这种情况下,想象一下,通过决定依赖前几个输入字节来设置库的选项,您可以创建一个新的伪格式。结果,除非您首先对文件进行预处理以添加初始参数,否则您将不再能够轻松地使用世界上所有现有的 JPEG 文件作为可能的种子输入。

为了探索将库配置为生成输入样本的函数的想法,我们将使用输入的前 64 个字节中设置的位数来选择JpegReadMode,如示例 13-3 所示。

示例 13-3。通过拆分输入进行模糊
  #include <cstddef>
  #include <cstdint>
  #include "jpeg_data_decoder.h"
  #include "jpeg_data_reader.h"

  const unsigned int kInspectBytes = 64;
  const unsigned int kInspectBlocks = kInspectBytes / sizeof(unsigned int);

  extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t sz) {
   knusperli::JPEGData jpg;
   knusperli::JpegReadMode rm;
   unsigned int bits = 0;

   if (sz <= kInspectBytes) {  *// Bail on too-small inputs.*
     return 0;
   }

   for (unsigned int block = 0; block < kInspectBlocks; block++) {
     bits += 
       __builtin_popcount(reinterpret_cast<const unsigned int *>(data)[block]);
   }

   rm = static_cast<knusperli::JpegReadMode>(bits % 
                                             (knusperli::JPEG_READ_ALL + 1));

   knusperli::ReadJpeg(data, sz, rm, &jpg);

   return 0;
 }

当将彩色条作为唯一的输入语料库使用五分钟时,这个模糊器给出了以下结果:

#851071 DONE   cov: 196 ft: 559 corp: 51/29Kb exec/s: 2827 rss: 812Mb
[...]
Done 851071 runs in 301 second(s)
stat::number_of_executed_units: 851071
stat::average_exec_per_sec:     2827
stat::new_units_added:          1120
stat::slowest_unit_time_sec:    0
stat::peak_rss_mb:              812

每秒执行次数下降了,因为更改使库的更多特性生效,导致这个模糊驱动器达到了更多代码(由上升的cov指标表示)。如果您在没有任何超时限制的情况下运行模糊器,它将继续无限期地生成输入,直到代码触发了一个消毒器错误条件。在那时,您将看到一个类似于之前显示的 Heartbleed 漏洞的报告。然后,您可以进行代码更改,重新构建,并运行您使用保存的工件构建的模糊器二进制文件,以重现崩溃或验证代码更改是否会修复问题。

持续模糊

一旦您编写了一些模糊器,定期在代码库上运行它们可以为工程师提供宝贵的反馈循环。持续构建管道可以在您的代码库中生成每日构建的模糊器,以供运行模糊器、收集崩溃信息并在问题跟踪器中提交错误。工程团队可以利用结果来专注于识别漏洞或消除导致服务未达到 SLO 的根本原因。

示例:ClusterFuzz 和 OSSFuzz

ClusterFuzz是由 Google 发布的可扩展模糊基础设施的开源实现。它管理运行模糊任务的虚拟机池,并提供一个 Web 界面来查看有关模糊器的信息。ClusterFuzz 不构建模糊器,而是期望持续构建/集成管道将模糊器推送到 Google Cloud Storage 存储桶。它还提供诸如语料库管理、崩溃去重和崩溃的生命周期管理等服务。ClusterFuzz 用于崩溃去重的启发式是基于崩溃时程序的状态。通过保留导致崩溃的样本,ClusterFuzz 还可以定期重新测试这些问题,以确定它们是否仍然重现,并在最新版本的模糊器不再在有问题的样本上崩溃时自动关闭问题。

ClusterFuzz 网络界面显示了您可以使用的指标,以了解给定模糊器的性能如何。可用的指标取决于构建流水线中集成的模糊引擎导出的内容(截至 2020 年初,ClusterFuzz 支持 libFuzzer 和 AFL)。ClusterFuzz 文档提供了从使用 Clang 代码覆盖支持构建的模糊器中提取代码覆盖信息的说明,然后将该信息转换为可以存储在 Google Cloud Storage 存储桶中并在前端显示的格式。使用此功能来探索上一节中编写的模糊器覆盖的代码将是确定输入语料库或模糊驱动程序的其他改进的下一个良好步骤。

OSS-Fuzz现代模糊技术与托管在谷歌云平台上的可扩展分布式 ClusterFuzz 执行相结合。它发现安全漏洞和稳定性问题,并直接向开发人员报告——自 2016 年 12 月推出以来的五个月内,OSS-Fuzz 已经发现了一千多个错误,自那时以来,它已经发现了成千上万个错误。

一旦项目与 OSS-Fuzz 集成,该工具就会使用持续和自动化测试,在修改的代码引入上游存储库后的几小时内发现问题,而不会影响任何用户。在谷歌,通过统一和自动化我们的模糊工具,我们已经将我们的流程整合到基于 OSS-Fuzz 的单个工作流程中。这些集成的 OSS 项目还受益于谷歌内部工具和外部模糊工具的审查。我们的集成方法增加了代码覆盖率,并更快地发现错误,改善了谷歌项目和开源生态系统的安全状况。

深入探讨:静态程序分析

静态分析是一种分析和理解计算机程序的方法,它通过检查其源代码而不执行或运行它们。静态分析器解析源代码并构建适合自动化分析的程序的内部表示。这种方法可以在源代码中发现潜在的错误,最好是在代码被检入或部署到生产环境之前。许多工具可用于各种语言,以及用于跨语言分析的工具。

静态分析工具在分析深度与分析源代码成本之间做出不同的权衡。例如,最浅的分析器执行简单的文本或基于抽象语法树(AST)的模式匹配。其他技术依赖于对程序的基于状态的语义构造进行推理,并基于程序的控制流和数据流进行推理。

工具还针对分析误报(错误警告)和漏报(遗漏警告)之间的不同分析权衡。权衡是不可避免的,部分原因是静态分析的基本限制:静态验证任何程序都是一个不可判定的问题——也就是说,不可能开发出一个能够确定任何给定程序是否会执行而不违反任何给定属性的算法。

鉴于这一约束,工具提供商专注于在开发的各个阶段为开发人员生成有用的信号。根据静态分析引擎的集成点,对于分析速度和预期分析反馈的不同权衡是可以接受的。例如,集成在代码审查系统中的静态分析工具可能只针对新开发的源代码,并会发出专注于非常可能的问题的精确警告。另一方面,正在进行最终的预部署发布分析的源代码(例如,用于航空电子软件或具有潜在政府认证要求的医疗设备软件等领域)可能需要更正式和更严格的分析。⁶

以下部分介绍了针对开发过程不同阶段的各种需求而调整的静态分析技术。我们重点介绍了自动化代码检查工具、基于抽象解释的工具(有时这个过程被称为深度静态分析)以及更消耗资源的方法,比如形式化方法。我们还讨论了如何将静态分析器集成到开发人员的工作流程中。

自动化代码检查工具

自动化代码检查工具针对语言特性和使用规则对源代码进行了句法分析。这些工具通常被称为linters,通常不对程序的复杂行为进行建模,比如过程间数据流。由于它们执行的分析相对较浅,这些工具很容易扩展到任意大小的代码——它们通常可以在大约相同的时间内完成源代码分析,就像编译代码一样。代码检查工具也很容易扩展——您可以简单地添加涵盖许多类型错误的新规则,特别是与语言特性相关的错误。

在过去几年中,代码检查工具已经专注于风格和可读性的改变,因为这些代码改进建议被开发人员高度接受。许多组织默认强制执行风格和格式检查,以便在大型开发团队中维护一个更易管理的统一代码库。这些组织还定期运行检查,以揭示潜在的代码异味和高度可能的错误。

以下示例侧重于执行一种特定类型的分析的工具——AST 模式匹配。 AST是程序源代码的树形表示,基于编程语言的句法结构。编译器通常将给定的源代码输入文件解析为这样的表示,并在编译过程中操纵该表示。例如,AST 可能包含一个表示if-then-else结构的节点,该节点有三个子节点:一个节点用于if语句的条件,一个节点表示then分支的子树,另一个节点表示else分支的子树。

Error Prone用于 Java,Clang-Tidy用于 C/C++在 Google 的项目中被广泛使用。这两种分析器都允许工程师添加自定义检查。例如,截至 2018 年初,已有 162 位作者提交了 733 个 Error Prone 检查。对于某些类型的错误,Error Prone 和 Clang-Tidy 都可以提出建议的修复方案。一些编译器(如 Clang 和 MSVC)还支持社区开发的C++核心指南。借助指南支持库(GSL)的帮助,这些指南可以防止 C++程序中的许多常见错误。

AST 模式匹配工具允许用户通过编写对解析 AST 的规则来添加新的检查。例如,考虑absl-string-find-startsWith Clang-Tidy 警告。该工具试图改进使用 C++ string::find API检查字符串前缀匹配的代码的可读性和性能:Clang-Tidy 建议改用ABSL提供的StartsWith API。为了执行其分析,该工具创建了一个 AST 子树模式,比较了 C++ string::find API 的输出与整数值 0。Clang-Tidy 基础设施提供了在被分析程序的 AST 表示中找到 AST 子树模式的工具。

考虑以下代码片段:

std::string s = "...";
if (s.find("Hello World") == 0) { /* do something */ }

absl-string-find-startsWith Clang-Tidy 警告标记了这段代码,并建议以以下方式更改代码:

std::string s = "...";
if (absl::StartsWith(s, "Hello World")) { /* do something */ }

为了提出修复建议,Clang-Tidy(从概念上讲)提供了根据模式转换 AST 子树的能力。图 13-1 的左侧显示了 AST 模式匹配。(为了清晰起见,AST 子树进行了简化。)如果工具在源代码的解析 AST 树中找到匹配的 AST 子树,它会通知开发人员。AST 节点还包含行和列信息,这使得 AST 模式匹配器能够向开发人员报告特定的警告。

AST 模式匹配和替换建议

图 13-1. AST 模式匹配和替换建议

除了性能和可读性检查外,Clang-Tidy 还提供了许多常见的错误模式检查。考虑在以下输入文件上运行 Clang-Tidy:⁷

$ cat -n sizeof.c
 1  #include <string.h>
 2  const char* kMessage = "Hello World!";
 3  int main() {
 4    char buf[128];
 5    memcpy(buf, kMessage, sizeof(kMessage));
 6    return 0;
 7  }

$ clang-tidy sizeof.c
[...]
Running without flags.
1 warning generated.
sizeof.c:5:32: warning: 'memcpy' call operates on objects of type 'const char' 
while the size is based on a different type 'const char *' 
[clang-diagnostic-sizeof-pointer-memaccess]
  memcpy(buf, kMessage, sizeof(kMessage));
                               ^
sizeof.c:5:32: note: did you mean to provide an explicit length?
  memcpy(buf, kMessage, sizeof(kMessage));

$ cat -n sizeof2.c
 1  #include <string.h>
 2  const char kMessage[] = "Hello World!";
 3  int main() {
 4    char buf[128];
 5    memcpy(buf, kMessage, sizeof(kMessage));
 6    return 0;
 7  }

$ clang-tidy sizeof2.c
[...]
Running without flags.

这两个输入文件只在kMessage的类型声明上有所不同。当kMessage被定义为指向初始化内存的指针时,sizeof(kMessage)返回指针类型的大小。因此,Clang-Tidy 产生了clang-diagnostic-sizeof-pointer-memaccess警告。另一方面,当kMessage的类型为const char[]时,sizeof(kMessage)操作返回适当的预期长度,Clang-Tidy 不会产生警告。

对于某些模式检查,除了报告警告外,Clang-Tidy 还可以建议代码修复。前面提到的absl-string-find-startsWith Clang-Tidy 警告建议就是这样一个例子。图 13-1 的右侧显示了适当的 AST 级别替换。当这样的建议可用时,您可以告诉 Clang-Tidy 自动将它们应用到输入文件中,使用--fix命令行选项。

您还可以使用自动应用的建议来使用 Clang-Tidy 的modernize修复更新代码库。考虑以下命令序列,其中展示了modernize-use-nullptr模式。该序列查找了用于指针赋值或比较的零常量的实例,并将它们更改为使用nullptr。为了运行所有modernize检查,我们使用 Clang-Tidy 选项--checks=modernize-*;然后--fix将建议应用到输入文件。在命令序列的末尾,我们通过打印转换后的文件来突出显示这四个更改(已强调):

$ cat -n nullptr.cc
 1  #define NULL 0x0
 2
 3  int *ret_ptr() {
 4    return 0;
 5  }
 6
 7  int main() {
 8    char *a = NULL;
 9    char *b = 0;
10    char c = 0;
11    int *d = ret_ptr();
12    return d == NULL ? 0 : 1;
13  }

$ clang-tidy nullptr.cc -checks=modernize-* --fix
[...]
Running without flags.
4 warnings generated.
nullptr.cc:4:10: warning: use nullptr [modernize-use-nullptr]
  return 0;
         ^
         nullptr
nullptr.cc:4:10: note: FIX-IT applied suggested code changes
  return 0;
         ^
nullptr.cc:8:13: warning: use nullptr [modernize-use-nullptr]
  char *a = NULL;
            ^
            nullptr
nullptr.cc:8:13: note: FIX-IT applied suggested code changes
  char *a = NULL;
            ^
nullptr.cc:9:13: warning: use nullptr [modernize-use-nullptr]
  char *b = 0;
            ^
            nullptr
nullptr.cc:9:13: note: FIX-IT applied suggested code changes
  char *b = 0;
            ^
nullptr.cc:12:15: warning: use nullptr [modernize-use-nullptr]
  return d == NULL ? 0 : 1;
              ^
              nullptr
nullptr.cc:12:15: note: FIX-IT applied suggested code changes
  return d == NULL ? 0 : 1;
              ^
clang-tidy applied 4 of 4 suggested fixes.

$ cat -n nullptr.cc
 1  #define NULL 0x0
 2
 3  int *ret_ptr() {
 4    return nullptr;
 5  }
 6
 7  int main() {
 8    char *a = nullptr;
 9    char *b = nullptr;
10    char c = 0;
11    int *d = ret_ptr();
12    return d == nullptr ? 0 : 1;
13  }

其他语言也有类似的自动化代码检查工具。例如,GoVet分析 Go 源代码中常见的可疑结构,Pylint分析 Python 代码,Error Prone 为 Java 程序提供分析和自动修复能力。以下示例简要演示了通过 Bazel 构建规则运行 Error Prone(已强调)。在 Java 中,对Short类型的变量i进行减法操作i-1会返回int类型的值。因此,remove操作不可能成功:

$ cat -n ShortSet.java
 1  import java.util.Set;
 2  import java.util.HashSet;
 3
 4  public class ShortSet {
 5    public static void main (String[] args) {
 6      Set<Short> s = new HashSet<>();
 7      for (short i = 0; i < 100; i++) {
 8        s.add(i);
 9        s.remove(i - 1);
10      }
11      System.out.println(s.size());
12    }
13  }

$ bazel build :hello
ERROR: example/myproject/BUILD:29:1: Java compilation in rule '//example/myproject:hello'
ShortSet.java:9: error: [CollectionIncompatibleType] Argument 'i - 1' should not be 
passed to this method;
its type int is not compatible with its collection's type argument Short
      s.remove(i - 1);
              ^
    (see http://errorprone.info/bugpattern/CollectionIncompatibleType)
1 error

将静态分析集成到开发者工作流程中

尽早在开发周期中运行相对快速的静态分析工具被认为是良好的行业实践。早期发现错误很重要,因为如果将它们推送到源代码存储库或部署给用户,修复它们的成本会大大增加。

将静态分析工具集成到 CI/CD 流水线中的门槛很低,对工程师的生产力可能会产生很高的积极影响。例如,开发人员可以收到有关如何修复空指针解除引用的错误和建议。如果他们无法推送他们的代码,他们就不会忘记修复问题并意外地导致系统崩溃或暴露信息,这有助于建立安全和可靠的文化(见第二十一章)。

为此,谷歌开发了 Tricorder 程序分析平台⁸和 Tricorder 的开源版本Shipshape。Tricorder 每天对大约 50,000 个代码审查更改进行静态分析。该平台运行许多类型的程序分析工具,并在代码审查期间向开发人员显示警告,当时他们习惯于评估建议。这些工具旨在提供易于理解和易于修复的代码发现,用户感知的误报率低(最多为 10%)。

Tricorder 旨在允许用户运行许多不同的程序分析工具。截至 2018 年初,该平台包括 146 个分析器,涵盖 30 多种源语言。其中大多数分析器是由谷歌开发人员贡献的。一般来说,通常可用的静态分析工具并不是非常复杂。Tricorder 运行的大多数检查器都是自动化的代码检查工具。这些工具针对各种语言,检查是否符合编码风格指南,并查找错误。如前所述,Error Prone 和 Clang-Tidy 在某些情况下可以提供建议的修复方法。代码作者随后可以通过点击按钮应用修复。

图 13-2 显示了给定 Java 输入文件的 Tricorder 分析结果的屏幕截图,呈现给代码审查人员。结果显示了两个警告,一个来自 Java linter,一个来自 Error Prone。Tricorder 通过允许代码审查人员通过“无用”链接对出现的警告提供反馈来衡量用户感知的误报率。Tricorder 团队使用这些信号来禁用个别检查。代码审查人员还可以向代码作者发送“请修复”个别警告的请求。

通过 Tricorder 提供的代码审查期间的静态分析结果的屏幕截图

图 13-2:通过 Tricorder 提供的代码审查期间的静态分析结果的屏幕截图

图 13-3 显示了在代码审查期间由 Error Prone 建议的自动应用的代码更改。

来自的 Error Prone 警告的预览修复视图的屏幕截图

图 13-3:来自图 13-2 的 Error Prone 警告的预览修复视图的屏幕截图

抽象解释

基于抽象解释的工具静态地执行程序行为的语义分析。这种技术已成功用于验证关键安全软件,如飞行控制软件。考虑一个简单的例子,一个程序生成 10 个最小的正偶数。在正常执行期间,程序生成整数值 2、4、6、8、10、12、14、16、18 和 20。为了允许对这样一个程序进行高效的静态分析,我们希望使用一个紧凑的表示来总结所有可能的值,覆盖所有观察到的值。使用所谓的区间或范围域,我们可以代表所有观察到的值,使用抽象区间值[2, 20]。区间域允许静态分析器通过简单地记住最低和最高可能的值来高效地推理所有程序执行。

为了确保我们捕捉到所有可能的程序行为,重要的是用抽象表示覆盖所有观察到的值。然而,这种方法也引入了可能导致不精确或错误警告的近似。例如,如果我们想要保证实际程序永远不会产生值 11,使用整数域的分析将导致一个错误的阳性。

利用抽象解释的静态分析器通常为每个程序点计算一个抽象值。为此,它们依赖于程序的控制流图(CFG)表示。CFG 在编译器优化和静态分析程序中通常被使用。CFG 中的每个节点代表程序中的一个基本块,对应于按顺序执行的程序语句序列。也就是说,在这个语句序列中没有跳转,也没有跳转目标在序列中间。CFG 中的边代表程序中的控制流,其中跳转发生在程序内部的控制流(例如,由于if语句或循环结构)或由于函数调用的程序间的控制流。请注意,CFG 表示也被覆盖引导的模糊器(之前讨论过)使用。例如,libFuzzer 跟踪在模糊过程中覆盖的基本块和边。模糊器使用这些信息来决定是否考虑将输入用于未来的变异。

基于抽象解释的工具执行关于程序中数据流和控制流的语义分析,通常跨越函数调用。因此,它们的运行时间比之前讨论的自动化代码检查工具要长得多。虽然你可以将自动化代码检查工具集成到交互式开发环境中,比如代码编辑器,但抽象解释通常不会被类似地集成。相反,开发人员可能会偶尔(比如每晚)在提交的代码上运行基于抽象解释的工具,或者在差异设置中进行代码审查,分析只有改变的代码,同时重用未改变的代码的分析事实。

许多工具依赖于抽象解释来处理各种语言和属性。例如,Frama-C 工具允许您查找 C 语言程序中的常见运行时错误和断言违规,包括缓冲区溢出、由于悬空或空指针导致的分段错误以及除零。正如之前讨论的那样,这些类型的错误,特别是与内存相关的错误,可能具有安全影响。Infer 工具推理程序执行的内存和指针更改,并可以在 Java、C 和其他语言中找到悬空指针等错误。AbsInt 工具可以对实时系统中任务的最坏执行时间进行分析。应用安全改进(ASI)程序对上传到 Google Play 商店的每个 Android 应用进行复杂的过程间分析,以确保安全性。如果发现漏洞,ASI 会标记漏洞并提出解决问题的建议。图 13-4 显示了一个样本安全警报。截至 2019 年初,该程序已导致 Play 商店中超过 300,000 个应用开发者修复了超过 100 万个应用。

应用安全改进警报

图 13-4:应用安全改进警报

形式化方法

形式化方法允许用户指定对软件或硬件系统感兴趣的属性。其中大多数是所谓的安全属性,指定某种不良行为不应该被观察到。例如,“不良行为”可以包括程序中的断言。其他包括活性属性,允许用户指定期望的结果,例如提交的打印作业最终由打印机处理。形式化方法的用户可以验证特定系统或模型的这些属性,甚至使用基于正确构造的方法开发这些系统。正如“分析不变量”中所强调的,基于形式化方法的方法通常具有相对较高的前期成本。这部分是因为这些方法需要对系统要求和感兴趣的属性进行先验描述。这些要求必须以数学严谨和形式化的方式进行规定。

基于形式化方法的技术已成功整合到硬件设计和验证工具中。¹¹在硬件设计中,使用电子设计自动化(EDA)供应商提供的形式化或半形式化工具现在是标准做法。这些技术也已成功应用于专门领域的软件,如安全关键系统或加密协议分析。例如,基于形式化方法的方法不断分析计算机网络通信中 TLS 使用的加密协议。¹²

结论

测试软件的安全性和可靠性是一个广泛的话题,我们只是触及了表面。本章介绍的测试策略,结合编写安全代码的实践(参见第十二章),对帮助谷歌团队可靠扩展、最小化停机时间和安全问题起到了关键作用。从开发的最早阶段就考虑可测试性,并在整个开发生命周期中进行全面测试是非常重要的。

在这一点上,我们要强调将所有这些测试和分析方法完全整合到您的工程工作流程和 CI/CD 流水线中的价值。通过在整个代码库中一致地结合和定期使用这些技术,您可以更快地识别错误。您还将提高在部署应用程序时检测或预防错误的能力,这是下一章将涵盖的主题。

  1. 1 我们建议查看SRE 书的第十七章,以获得一个以可靠性为重点的视角。

  2. 2 Petrović,Goran 和 Marko Ivanković。2018 年。“Google 的突变测试现状。” 第 40 届国际软件工程大会论文集:163-171。doi:10.1145/3183519.3183521。

  3. 3 有关在 Google 遇到的常见单元测试陷阱的更多讨论,请参阅 Wright,Hyrum 和 Titus Winters。2015 年。“你所有的测试都很糟糕:来自前线的故事。” CppCon 2015。https://oreil.ly/idleN

  4. 4 请参阅SRE 工作手册的第二章

  5. 5 模糊目标比较 OpenSSL 内部两个模块化指数实现的结果,如果结果有任何不同,将会失败。

  6. 6 例如,参见 Bozzano,Marco 等人。2017 年。“航空航天系统的形式方法。”在从架构分析视角设计物理系统,由 Shin Nakajima,Jean-Pierre Talpin,Masumi Toyoshima 和 Huafeng Yu 编辑。新加坡:Springer。

  7. 7 您可以使用标准软件包管理器安装 Clang-Tidy。通常称为 clang-tidy。

  8. 8 请参阅 Sadowski,Caitlin 等人。2018 年。“在 Google 构建静态分析工具的经验教训。” ACM 通讯 61(4):58-66。doi:10.1145/3188720。

  9. 9 请参阅 Cousot,Patrick 和 Radhia Cousot。1976 年。“程序动态属性的静态确定。” 第 2 届国际编程研讨会论文集:106-130。https://oreil.ly/4xLgB

  10. 10 Souyris,Jean 等人。2009 年。“航空电子软件产品的形式验证。” 第 2 届形式方法世界会议论文集:532-546。doi:10.1007/978-3-642-05089-3_34。

  11. 11 例如,参见 Kern,Christoph 和 Mark R. Greenstreet。1999 年。“硬件设计中的形式验证:一项调查。” ACM 电子系统设计交易 4(2):123-193。doi:10.1145/307988.307989。另请参见 Hunt Jr.等人。2017 年。“使用 ACL2 进行工业硬件和软件验证。” 皇家学会哲学交易 A 数学物理和工程科学 375(2104):20150399。doi:10.1098/rsta.2015.0399。

  12. 12 请参阅 Chudnov,Andrey 等人。2018 年。“Amazon s2n 的持续形式验证。” 第 30 届国际计算机辅助验证会议论文集:430-446。doi:10.1007/978-3-319-96142-2_26。

第十四章:部署代码

原文:14. Deploying Code

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Jeremiah Spradlin 和 Mark Lodato

与 Sergey Simakov 和 Roxana Loza

在编写和测试代码时,前几章讨论了如何考虑安全性和可靠性。然而,直到代码构建和部署之后,该代码才会产生真正的影响。因此,对构建和部署过程的所有元素仔细考虑安全性和可靠性非常重要。仅通过检查构件本身很难确定部署的构件是否安全。软件供应链各个阶段的控制可以增加您对软件构件安全性的信心。例如,代码审查可以减少错误的机会,并阻止对手进行恶意更改,自动化测试可以增加您对代码操作正确性的信心。

围绕源代码、构建和测试基础设施构建的控制具有有限的效果,如果对手可以通过直接部署到您的系统来绕过它们。因此,系统应拒绝不是来自正确软件供应链的部署。为了满足这一要求,供应链中的每个步骤都必须能够提供其已正确执行的证据。

概念和术语

我们使用术语软件供应链来描述编写、构建、测试和部署软件系统的过程。这些步骤包括版本控制系统(VCS)、持续集成(CI)流水线和持续交付(CD)流水线的典型责任。

尽管实现细节在公司和团队之间有所不同,但大多数组织都有一个类似于图 14-1 的流程:

  1. 代码必须检入版本控制系统。

  2. 然后从检入的版本构建代码。

  3. 一旦构建完成,二进制文件必须经过测试。

  4. 然后将代码部署到某个环境中,进行配置和执行。

典型软件供应链的高层视图

图 14-1:典型软件供应链的高层视图

即使您的供应链比这个模型更复杂,您通常也可以将其分解为这些基本构建块。图 14-2 显示了典型部署流水线如何执行这些步骤的具体示例。

您应该设计软件供应链以减轻对系统的威胁。本章重点介绍了如何减轻内部人员(或恶意攻击者冒充内部人员)在第二章中定义的威胁,而不考虑内部人员是否具有恶意意图。例如,一个善意的工程师可能无意中构建了包含未经审查和未提交更改的代码,或者外部攻击者可能尝试使用受损工程师帐户的权限部署带有后门的二进制文件。我们同样考虑这两种情况。

在本章中,我们对软件供应链的步骤进行了广泛定义。

构建是将输入构件转换为输出构件的任何过程,其中构件是任何数据片段,例如文件、软件包、Git 提交或虚拟机(VM)镜像。测试是构建的特殊情况,其中输出构件是一些逻辑结果,通常是“通过”或“失败”,而不是文件或可执行文件。

典型的云托管基于容器的服务部署

图 14-2:典型的云托管基于容器的服务部署

构建可以链接在一起,并且一个构件可以经历多次测试。例如,发布过程可能首先从源代码“构建”二进制文件,然后从二进制文件“构建”Docker 镜像,然后通过在开发环境中运行 Docker 镜像来“测试”Docker 镜像。

部署是将某个构件分配到某个环境的任何过程。您可以将以下每个过程视为部署:

  • 推送代码:

  • 发布命令以导致服务器下载并运行新的二进制文件

  • 更新 Kubernetes 部署对象以使用新的 Docker 镜像

  • 启动虚拟机或物理机,加载初始软件或固件

  • 更新配置:

  • 运行 SQL 命令来更改数据库模式

  • 更新 Kubernetes 部署对象以更改命令行标志

  • 发布一个包或其他数据,将被其他用户使用:

  • 上传 deb 包到 apt 仓库

  • 上传 Docker 镜像到容器注册表

  • 上传 APK 到 Google Play 商店

本章不包括部署后更改。

威胁模型

在加固软件供应链以缓解威胁之前,您必须确定您的对手。在本讨论中,我们将考虑以下三种对手类型。根据您的系统和组织,您的对手清单可能会有所不同:

  • 可能犯错误的良性内部人员

  • 试图获得比其角色允许的更多访问权限的恶意内部人员

  • 外部攻击者入侵一个或多个内部人员的机器或帐户

第二章描述了攻击者的配置文件,并提供了针对内部风险建模的指导。

接下来,您必须像攻击者一样思考,尝试识别对手可以颠覆软件供应链以威胁您系统的所有方式。以下是一些常见威胁的例子;您应该根据您组织的具体威胁来调整这个清单。为了简单起见,我们使用术语工程师来指代良性内部人员,恶意对手来指代恶意内部人员和外部攻击者:

  • 工程师提交了一个意外引入系统漏洞的更改。

  • 恶意对手提交了一个启用后门或引入系统其他有意漏洞的更改。

  • 工程师意外地从包含未经审查的更改的本地修改版本的代码构建。

  • 工程师部署了一个带有有害配置的二进制文件。例如,更改启用了仅用于测试的生产调试功能。

  • 恶意对手部署了一个修改过的二进制文件到生产环境,开始窃取客户凭据。

  • 恶意对手修改了云存储桶的 ACL,允许他们窃取数据。

  • 恶意对手窃取用于签署软件的完整性密钥。

  • 工程师部署了一个带有已知漏洞的旧版本代码。

  • CI 系统配置错误,允许从任意源代码库构建请求。因此,恶意对手可以从包含恶意代码的源代码库构建。

  • 恶意对手上传一个自定义的构建脚本到 CI 系统,窃取签名密钥。然后对手使用该密钥对恶意二进制文件进行签名和部署。

  • 恶意对手欺骗 CD 系统使用带有后门的编译器或构建工具来生成恶意二进制文件。

一旦您编制了一个潜在对手和威胁的全面清单,您可以将您已经采取的缓解措施与您识别出的威胁进行映射。您还应该记录当前缓解策略的任何限制。这个练习将为您的系统中潜在风险提供一个全面的图片。没有相应缓解措施的威胁,或者现有缓解措施存在重大限制的威胁,都是需要改进的领域。

最佳实践

以下最佳实践可以帮助您缓解威胁,在您的威胁模型中填补任何安全漏洞,并持续改进您的软件供应链的安全性。

需要代码审查

代码审查是在提交或部署更改之前,让第二个人(或几个人)审查源代码的更改的做法。除了提高代码安全性外,代码审查还为软件项目提供了多种好处:它们促进知识共享和教育,灌输编码规范,提高代码可读性,减少错误,所有这些有助于建立安全和可靠的文化(有关这个想法的更多信息,请参见第二十一章)。

从安全的角度来看,代码审查是一种多方授权的形式,这意味着没有个人有权利单独提交更改。正如第五章中所描述的,多方授权提供了许多安全好处。

要成功实现,代码审查必须是强制性的。如果对手可以选择退出审查,那么他们将不会被阻止!审查还必须足够全面,以捕捉问题。审阅者必须理解任何更改的细节及其对系统的影响,或者向作者询问澄清问题,否则该过程可能会变得形式化。

许多公开可用的工具允许您实现强制性的代码审查。例如,您可以配置 GitHub、GitLab 或 BitBucket,要求每个拉取/合并请求都需要一定数量的批准。或者,您可以使用独立的审查系统,如 Gerrit 或 Phabricator,结合一个配置为只接受来自该审查系统的推送的源代码库。

从安全的角度来看,代码审查在安全方面存在一些限制,正如第十二章中所述。因此,最好将其作为“深度防御”安全措施之一,与自动化测试(在第十三章中描述)和第十二章中的建议一起实现。

依赖自动化

理想情况下,自动化系统应该执行软件供应链中的大部分步骤。自动化提供了许多优势。它可以为构建、测试和部署软件提供一致、可重复的流程。将人类从循环中移除有助于防止错误并减少劳动。当您在一个封闭的系统上运行软件供应链自动化时,您可以使系统免受恶意对手的颠覆。

考虑一个假设的场景,工程师根据需要在他们的工作站上手动构建“生产”二进制文件。这种情况会产生许多引入错误的机会。工程师可能会意外地从错误的代码版本构建,或者包含未经审查或未经测试的代码更改。同时,恶意对手,包括已经攻破工程师机器的外部攻击者,可能会故意用恶意版本覆盖本地构建的二进制文件。自动化可以防止这两种结果。

以安全的方式添加自动化可能会有些棘手,因为自动化系统本身可能会引入其他安全漏洞。为了避免最常见的漏洞类别,我们建议至少采取以下措施:

将所有构建、测试和部署步骤移至自动化系统。

至少,您应该编写所有步骤的脚本。这样可以让人类和自动化执行相同的步骤以保持一致性。您可以使用 CI/CD 系统(如Jenkins)来实现这一目的。考虑制定一个要求所有新项目都需要自动化的政策,因为将自动化应用到现有系统中通常是具有挑战性的。

软件供应链的所有配置更改都需要同行审查。

通常,将配置视为代码(如前所述)是实现这一目标的最佳方式。通过要求审查,您大大减少了出错和错误的机会,并增加了恶意攻击的成本。

锁定自动化系统,防止管理员或用户篡改。

这是最具挑战性的一步,实现细节超出了本章的范围。简而言之,考虑管理员可以在没有审查的情况下进行更改的所有路径——例如,通过直接配置 CI/CD 管道或使用 SSH 在机器上运行命令进行更改。对于每条路径,考虑采取措施以防止未经同行审查的访问。

有关锁定自动化构建系统的进一步建议,请参阅“可验证构建”。

自动化是双赢,减少了繁重的工作,同时增加了可靠性和安全性。尽可能依赖自动化!

验证构件,而不仅仅是人

如果对手可以绕过源、构建和测试基础设施的控制,直接部署到生产环境,那么这些控制的效果就会受到限制。仅仅验证发起了部署是不够的,因为该行为者可能会犯错误,或者可能是有意部署了恶意更改。相反,部署环境应该验证正在部署的内容。

部署环境应该要求证明部署过程的每个自动化步骤都已发生。除非有其他缓解控制检查该操作,否则人类不应能够绕过自动化。例如,如果您在 Google Kubernetes Engine(GKE)上运行,您可以使用二进制授权默认接受仅由您的 CI/CD 系统签名的镜像,并监视 Kubernetes 集群审计日志,以获取有人使用紧急功能部署不符合规定的镜像时的通知。

这种方法的一个局限性是,它假设您设置的所有组件都是安全的:即 CI/CD 系统仅接受允许在生产环境中使用的源的构建请求,如果使用签名密钥,则只能由 CI/CD 系统访问,等等。“高级缓解策略”描述了一种更健壮的方法,直接验证所需属性,减少了隐含的假设。

将配置视为代码

服务的配置对于安全性和可靠性同样至关重要。因此,关于代码版本控制和更改审查的所有最佳实践也适用于配置。将配置视为代码,要求在部署之前对配置更改进行检入、审查和测试,就像对任何其他更改一样。

举个例子:假设您的前端服务器有一个配置选项来指定后端。如果有人将您的生产前端指向后端的测试版本,那么您将面临严重的安全和可靠性问题。

或者,作为一个更实际的例子,考虑一个使用 Kubernetes 并将配置存储在版本控制下的YAML文件的系统。部署过程调用kubectl二进制文件并传递 YAML 文件,部署经过批准的配置。限制部署过程仅使用“经过批准”的 YAML——来自版本控制并需要同行审查的 YAML——使得误配置服务变得更加困难。

您可以重复使用本章推荐的所有控件和最佳实践,以保护您服务的配置。重用这些方法通常比其他方法更容易,后者通常需要完全独立的多方授权系统来保护部署后的配置更改。

版本控制和审查配置的做法并不像代码版本控制和审查那样普遍。即使实现了配置即代码的组织通常也不会对配置应用代码级别的严格要求。例如,工程师通常知道他们不应该从本地修改的源代码构建生产版本的二进制文件。这些工程师可能在未将更改保存到版本控制并征求审查的情况下部署配置更改。

实现配置即代码需要改变你的文化、工具和流程。在文化上,你需要重视审查流程。在技术上,你需要工具,允许你轻松比较提议的更改(即diffgrep),并提供在紧急情况下手动覆盖更改的能力。

防范威胁模型

现在我们已经定义了一些最佳实践,我们可以将这些流程映射到我们之前确定的威胁上。在评估这些流程与您特定的威胁模型相关时,问问自己:所有最佳实践都是必要的吗?它们是否足以缓解所有威胁?表 14-1 列出了示例威胁,以及它们对应的缓解措施和这些缓解措施的潜在限制。

表 14-1. 示例威胁、缓解措施和缓解措施的潜在限制

威胁 缓解 限制
一个工程师提交了一个意外引入系统漏洞的更改。 代码审查加自动化测试(见第十三章)。这种方法显著减少了错误的机会。
一个恶意的对手提交了一个改变,使系统启用了后门或引入了其他有意的漏洞。 代码审查。这种做法增加了攻击的成本和检测的机会——对手必须仔细制定更改以通过代码审查。 不能防止串通或外部攻击者能够 compromise 多个内部账户。

一个工程师意外地从包含未经审查的更改的本地修改版本的代码构建。一个自动化的 CI/CD 系统总是从正确的源代码库中拉取执行构建。

一个工程师部署了一个有害的配置。例如,更改启用了仅用于测试的生产环境中的调试功能。将配置视为源代码,并要求进行相同级别的同行审查。并非所有配置都可以被视为“代码”。

一个恶意的对手部署了一个修改后的二进制文件到生产环境,开始窃取客户凭据。生产环境需要证明 CI/CD 系统构建了二进制文件。CI/CD 系统配置为只从正确的源代码库中拉取源代码。对手可能会想出如何绕过这一要求,使用紧急部署 breakglass 程序。充分的日志记录和审计可以缓解这种可能性。

一个恶意的对手修改了云存储桶的 ACL,使他们能够窃取数据。考虑资源 ACL 作为配置。云存储桶只允许部署过程进行配置更改,因此人类无法进行更改。不能防止串通或外部攻击者能够 compromise 多个内部账户。

一个恶意的对手窃取了用于签署软件的完整性密钥。将完整性密钥存储在一个密钥管理系统中,该系统配置为只允许 CI/CD 系统访问密钥,并支持密钥轮换。有关更多信息,请参见第九章。有关构建特定的建议,请参见“高级缓解策略”。

图 14-3 显示了一个更新的软件供应链,其中包括前面表中列出的威胁和缓解措施。

典型的软件供应链-对手不应能够绕过流程

图 14-3:典型的软件供应链-对手不应能够绕过流程

我们尚未将几个威胁与最佳实践中的缓解措施相匹配:

  • 工程师部署了一个带有已知漏洞的旧版本代码。

  • CI 系统配置错误,允许从任意源代码仓库构建请求。因此,恶意对手可以从包含恶意代码的源代码仓库构建。

  • 一个恶意对手上传了一个自定义的构建脚本到 CI 系统,用于窃取签名密钥。然后对手使用该密钥对恶意二进制文件进行签名和部署。

  • 一个恶意对手欺骗 CD 系统使用一个带有后门的编译器或构建工具来生成恶意二进制文件。

为了解决这些威胁,您需要实现更多的控制,我们将在下一节中介绍。只有您才能决定是否值得为您特定的组织解决这些威胁。

深入探讨:高级缓解策略

您可能需要复杂的缓解措施来解决软件供应链中一些更高级的威胁。因为本节中的建议在行业内尚未成为标准,您可能需要构建一些自定义基础设施来采用这些建议。这些建议最适合规模大和/或特别安全敏感的组织,对于暴露于内部风险较低的小型组织可能没有意义。

二进制来源

每次构建都应该生成描述给定二进制工件是如何构建的“二进制来源”:输入、转换和执行构建的实体。

为了解释原因,考虑以下激励性例子。假设您正在调查一个安全事件,并且看到在特定时间窗口内发生了部署。您想确定部署是否与该事件有关。逆向工程二进制将成本过高。检查源代码将更容易得多,最好是查看版本控制中的更改。但是您如何知道二进制来自哪个源代码?

即使您不预期需要这些类型的安全调查,您也需要基于来源的二进制来源来制定基于来源的部署策略,如本节后面所讨论的。

二进制来源中应包含的内容

您应该在来源中包含的确切信息取决于您的系统内置的假设和最终需要来源的消费者的信息。为了实现丰富的部署策略并允许临时分析,我们建议以下来源字段:

真实性(必需)

暗示了构建的隐式信息,例如产生它的系统以及您为何可以信任来源。这通常是通过使用加密签名来保护二进制来源的其他字段来实现的。¹¹

输出(必需)

适用于此二进制来源的输出工件。通常,每个输出都由工件内容的加密哈希标识。

输入

构建中的内容。此字段允许验证者将源代码的属性与工件的属性进行关联。它应包括以下内容:

来源

构建的“主”输入工件,例如顶层构建命令运行的源代码树。例如:“来自https://github.com/mysql/mysql-server的 Git 提交270f...ce6d”¹²或“文件foo.tar.gz的 SHA-256 内容78c5...6649。”

依赖

构建所需的所有其他工件,如库、构建工具和编译器,这些工件在源代码中没有完全指定。这些输入都可能影响构建的完整性。

命令

用于启动构建的命令。例如:“bazel build //main:hello-world”。理想情况下,该字段应结构化以允许自动化分析,因此我们的示例可能变为“{"bazel": {"command": "build", "target": "//main:hello_world"}}”。

环境

需要重现构建的任何其他信息,例如架构细节或环境变量。

输入元数据

在某些情况下,构建者可能会读取有关下游系统将发现有用的输入的元数据。例如,构建者可能包括源提交的时间戳,然后策略评估系统在部署时使用。

调试信息

任何不必要用于安全性但可能对调试有用的额外信息,例如构建运行的机器。

版本控制

构建时间戳和溯源格式版本号通常很有用,以便进行将来的更改,例如使旧构建无效或更改格式而不易受到回滚攻击。

您可以省略隐含或由源代码本身覆盖的字段。例如,Debian 的溯源格式省略了构建命令,因为该命令始终是dpkg-buildpackage

输入工件通常应列出标识符(例如 URI)和版本(例如加密哈希)。通常使用标识符来验证构建的真实性,例如验证代码是否来自正确的源代码库。版本对于各种目的都很有用,例如临时分析、确保可重现的构建以及验证链式构建步骤,其中步骤i的输出是步骤i+1 的输入。

注意攻击面。您需要验证构建系统未检查的任何内容(因此由签名隐含)或包含在源代码中(因此经过同行审查)的内容。如果启动构建的用户可以指定任意编译器标志,则验证器必须验证这些标志。例如,GCC 的-D标志允许用户覆盖任意符号,因此也可以完全更改二进制文件的行为。同样,如果用户可以指定自定义编译器,则验证器必须确保使用了“正确”的编译器。一般来说,构建过程可以执行的验证越多,越好。

有关二进制溯源的一个很好的例子,请参见 Debian 的deb-buildinfo格式。有关更一般的建议,请参见可重现构建项目的文档。要签名和编码此信息的标准方法,请考虑JSON Web Tokens(JWT)

基于溯源的部署策略

“验证工件,而不仅仅是人”建议官方构建自动化流水线应该验证正在部署的内容。如何验证流水线配置正确?如果您想为某些部署环境提供特定的保证,而这些保证不适用于其他环境,该怎么办?

您可以使用明确的部署策略来描述每个部署环境的预期属性,以解决这些问题。然后,部署环境可以将这些策略与部署到它们的工件的二进制溯源进行匹配。

这种方法比纯粹基于签名的方法有几个优点:

  • 它减少了软件供应链中隐含的假设数量,使分析和确保正确性变得更容易。

  • 它澄清了软件供应链中每个步骤的合同,减少了配置错误的可能性。

  • 它允许您在每个构建步骤中使用单个签名密钥,而不是每个部署环境,因为现在您可以使用二进制溯源进行部署决策。

例如,假设您有一个微服务架构,并希望保证每个微服务只能从提交到该微服务源存储库的代码构建。使用代码签名,您需要每个源存储库一个密钥,并且 CI/CD 系统需要根据源存储库选择正确的签名密钥。这种方法的缺点是很难验证 CI/CD 系统的配置是否符合这些要求。

使用基于溯源的部署策略,CI/CD 系统生成了二进制溯源,说明了源存储库,总是由单一密钥签名。每个微服务的部署策略列出了允许的源存储库。与代码签名相比,正确性的验证要容易得多,因为部署策略在一个地方描述了每个微服务的属性。

部署策略中列出的规则应该减轻对系统的威胁。参考您为系统创建的威胁模型。您可以定义哪些规则来减轻这些威胁?例如,以下是一些您可能想要实现的示例规则:

  • 源代码已提交到版本控制并经过同行审查。

  • 源代码来自特定位置,比如特定的构建目标和存储库。

  • 构建是通过官方的 CI/CD 流水线进行的(参见“可验证构建”)。

  • 测试已通过。

  • 二进制文件在此部署环境中明确允许。例如,不要在生产环境中允许“测试”二进制文件。

  • 代码或构建的版本足够新。¹³

  • 代码不包含已知的漏洞,如最近的安全扫描所报告的。¹⁴

in-toto 框架提供了实现溯源策略的一个标准。

实现政策决策

如果您为基于溯源的部署策略实现自己的引擎,请记住需要三个步骤:

  1. 验证溯源的真实性。这一步还隐式地验证了溯源的完整性,防止对手篡改或伪造它。通常,这意味着验证溯源是否由特定密钥进行了加密签名。

  2. 验证溯源是否适用于构件。这一步还隐式地验证了构件的完整性,确保对手不能将一个“好”的溯源应用于一个“坏”的构件。通常,这意味着比较构件的加密哈希与溯源有效负载中找到的值。

  3. 验证溯源是否符合所有策略规则

这个过程的最简单的例子是一个规则,要求构件必须由特定密钥签名。这个单一的检查实现了所有三个步骤:它验证了签名本身是否有效,构件是否适用于签名,以及签名是否存在。

让我们考虑一个更复杂的例子:“Docker 镜像必须从 GitHub 存储库mysql/mysql-server构建。”假设您的构建系统使用密钥K[B]以 JWT 格式签署构建溯源。在这种情况下,令牌有效负载的模式将如下所示,其中主题subRFC 6920 URI

{
  "sub": "ni:///sha-256;...",
  "input": {"source_uri": "..."}
}

要评估构件是否符合此规则,引擎需要验证以下内容:

  1. JWT 签名使用密钥K[B]进行验证。

  2. sub 匹配了构件的 SHA-256 哈希。

  3. input.source_uri 正好是"https://github.com/mysql/mysql-server"

可验证构建

我们称构建为可验证的,如果构建产生的二进制溯源是可信的。¹⁵ 可验证性取决于观察者。您是否信任特定的构建系统取决于您的威胁模型以及构建系统如何融入您组织更大的安全故事。

考虑以下非功能性需求示例是否适合你的组织,¹⁶并添加符合你特定需求的任何需求:

  • 如果单个开发者的工作站受到损害,二进制出处或输出物品的完整性不会受到损害。

  • 对手无法在不被察觉的情况下篡改出处或输出物品。

  • 一个构建不能影响另一个构建的完整性,无论是并行运行还是串行运行。

  • 构建不能产生包含错误信息的出处。例如,出处不应该声称一个物品是从 Git 提交abc...def构建的,而实际上是来自123...456

  • 非管理员不能配置用户定义的构建步骤,比如 Makefile 或 Jenkins Groovy 脚本,以违反此列表中的任何要求。

  • 在构建后至少N个月内,所有源物品的快照都可以用于潜在的调查。

  • 构建是可复现的(参见“隔离的、可复现的或可验证的?”)。即使在可验证的构建架构中没有要求,这种方法也可能是可取的。例如,在发现安全事件或漏洞后,可复现的构建可能有助于独立重新验证物品的二进制出处。

可验证的构建架构

可验证构建系统的目的是增加验证者对构建系统产生的二进制出处的信任。无论可验证性的具体要求如何,都有三种主要的架构可供选择:

可信的构建服务

验证者要求原始构建是由验证者信任的构建服务执行的。通常,这意味着可信的构建服务用只有该服务才能访问的密钥对二进制出处进行签名。

这种方法的优点是只需要构建一次,不需要可复现性(参见“隔离的、可复现的或可验证的?”)。Google 在内部构建中使用这种模型。

你自己进行的重建

验证者在飞行中重现构建,以验证二进制出处。例如,如果二进制出处声称来自 Git 提交abc...def,验证者会获取该 Git 提交,重新运行二进制出处中列出的构建命令,并检查输出是否与问题物品完全相同。有关可复现性的更多信息,请参见下面的侧边栏。

虽然这种方法可能一开始看起来很吸引人,因为你信任自己,但它并不具备可扩展性。构建通常需要几分钟甚至几小时,而部署决策通常需要在毫秒内做出。这还要求构建是完全可复现的,这并不总是切实可行;有关更多信息,请参见侧边栏。

重建服务

验证者要求“重建者”中的一些“重建者”已经重现了构建并证明了二进制出处的真实性。这是前两种选项的混合体。在实践中,这种方法通常意味着每个重建者监视一个软件包存储库,主动重建每个新版本,并将结果存储在某个数据库中。然后,验证者在N个不同的数据库中查找条目,这些条目以问题物品的加密哈希为键。当中央管理模式不可行或不可取时,像Debian这样的开源项目使用这种模型。

实现可验证的构建

无论可验证的构建服务是“可信的构建服务”还是“重建服务”,都应该牢记一些重要的设计考虑。

基本上,几乎所有的 CI/CD 系统都按照图 14-4 中的步骤运行:服务接收请求,获取任何必要的输入,执行构建,并将输出写入存储系统。

一个基本的 CI/CD 系统

图 14-4:一个基本的 CI/CD 系统

有了这样的系统,您可以相对容易地向输出添加签名的来源,如图 14-5 所示。对于一个采用“中央构建服务”模型的小型组织来说,这个额外的签名步骤可能足以解决安全问题。

向现有的 CI/CD 系统添加签名

图 14-5:向现有的 CI/CD 系统添加签名

随着您的组织规模的增长和您有更多的资源投入到安全中,您可能希望解决另外两个安全风险:不受信任的输入和未经身份验证的输入。

不受信任的输入

对手可能使用构建的输入来颠覆构建过程。许多构建服务允许非管理员用户定义在构建过程中执行的任意命令,例如通过 Jenkinsfile、travis.yml、Makefile 或BUILD。从安全的角度来看,这种功能实际上是“远程代码执行(RCE)设计”。在特权环境中运行的恶意构建命令可以执行以下操作:

  • 窃取签名密钥。

  • 在来源中插入错误信息。

  • 修改系统状态,影响后续构建。

  • 操纵另一个同时进行的构建。

即使用户不被允许定义自己的步骤,编译是一个非常复杂的操作,提供了充分的机会进行 RCE 漏洞。

您可以通过特权分离来减轻这种威胁。使用一个受信任的编排器进程来设置初始的已知良好状态,启动构建,并在构建完成时创建签名的来源。可选地,编排器可以获取输入以解决下一小节中描述的威胁。所有用户定义的构建命令应在另一个环境中执行,该环境无法访问签名密钥或任何其他特权。您可以通过各种方式创建这个环境,例如通过与编排器相同的机器上的沙盒,或者在单独的机器上运行。

未经身份验证的输入

即使用户和构建步骤是可信的,大多数构建都依赖于其他工件。任何这样的依赖都是对手可能潜在颠覆构建的表面。例如,如果构建系统在没有 TLS 的情况下通过 HTTP 获取依赖项,攻击者可以进行中间人攻击以修改传输中的依赖项。

因此,我们建议使用隔离构建(参见“隔离的、可重现的或可验证的?”)。构建过程应该提前声明所有输入,只有编排器应该获取这些输入。隔离构建大大提高了在来源中列出的输入是正确的信心。

一旦您考虑了不受信任和未经身份验证的输入,您的系统就类似于图 14-6。这样的模型比图 14-5 中的简单模型更抵抗攻击。

一个“理想”的 CI/CD 设计,解决了不受信任和未经身份验证输入的风险

图 14-6:解决不受信任和未经身份验证输入风险的“理想”CI/CD 设计

部署阻塞点

要“验证工件,而不仅仅是人”,部署决策必须发生在部署环境内的适当阻塞点。在这种情况下,“阻塞点”是所有部署请求必须流经的点。对手可以绕过不在阻塞点发生的部署决策。

以 Kubernetes 为例,设置部署瓶颈,如图 14-7 所示。假设您想要验证特定 Kubernetes 集群中所有部署到 pod 的部署。主节点将成为一个良好的瓶颈,因为所有部署都应该通过它进行。为了使其成为一个合适的瓶颈,配置工作节点只接受来自主节点的请求。这样,对手就无法直接部署到工作节点。

Kubernetes 架构-所有部署必须通过主节点

图 14-7:Kubernetes 架构-所有部署必须通过主节点

理想情况下,瓶颈执行策略决策,可以直接执行,也可以通过 RPC 执行。Kubernetes 为此目的提供了准入控制器webhook。如果您使用 Google Kubernetes Engine,二进制授权提供了一个托管的准入控制器和许多其他功能。即使您不使用 Kubernetes,您也可以修改“准入”点以执行部署决策。

或者,您可以在瓶颈前放置一个“代理”,并在代理中执行策略决策,如图 14-8 所示。这种方法需要配置您的“准入”点,只允许通过代理访问。否则,对手可以通过直接与准入点通信来绕过代理。

使用代理进行策略决策的替代架构

图 14-8:使用代理进行策略决策的替代架构

部署后验证

即使在部署时执行部署策略或签名检查时,记录和部署后验证几乎总是可取的,原因如下:

  • 策略可能会更改,在这种情况下,验证引擎必须重新评估系统中现有的部署,以确保它们仍然符合新的策略。这在首次启用策略时尤为重要。

  • 由于决策服务不可用,请求可能已被允许继续进行。这种故障开放设计通常是必要的,以确保服务的可用性,特别是在首次推出执行功能时。

  • 在紧急情况下,操作员可能使用了紧急开关机制来绕过决策,如下一节所述。

  • 用户需要一种方法,在提交之前测试潜在的策略更改,以确保现有状态不会违反新版本的策略。

  • 出于类似于“故障开放”用例的原因,用户可能还希望有一种干预运行模式,在部署时系统始终允许请求,但监控会发现潜在问题。

  • 调查人员可能需要在事故发生后出于取证目的获取信息。

执行决策点必须记录足够的信息,以便验证器在部署后评估策略。²⁰通常需要记录完整的请求,但并不总是足够的-如果策略评估需要一些其他状态,则日志必须包括该额外状态。例如,当我们为 Borg 实现部署后验证时遇到了这个问题:因为“作业”请求包括对现有“分配”和“包”引用,我们必须连接三个日志来源-作业,分配和包-以获取做出决策所需的完整状态。²¹

实用建议

多年来,在各种情境中实现可验证的构建和部署策略时,我们学到了一些经验教训。这些经验教训大多与实际技术选择无关,而更多地与如何部署可靠、易于调试和易于理解的更改有关。本节包含一些建议,希望您会发现有用。

逐步进行

提供高度安全、可靠和一致的软件供应链可能需要您进行许多更改,从编写构建步骤到实现构建来源,再到实现配置即代码。协调所有这些更改可能很困难。这些控件中的错误或缺失功能也可能对工程生产力构成重大风险。在最坏的情况下,这些控件中的错误可能导致服务中断。

如果您一次专注于保护供应链的一个特定方面,可能会更成功。这样,您可以最大程度地减少中断风险,同时还可以帮助同事学习新的工作流程。

提供可操作的错误消息

当部署被拒绝时,错误消息必须清楚地解释出了什么问题以及如何解决。例如,如果工件被拒绝是因为从错误的源 URI 构建的,解决方法可以是更新策略以允许该 URI,或者从正确的 URI 重新构建。您的策略决策引擎应该给用户提供可操作的反馈,提供这样的建议。简单地说“不符合策略”可能会让用户感到困惑和手足无措。

在设计架构和策略语言时,请考虑这些用户旅程。一些设计选择会使为用户提供可操作的反馈变得非常困难,因此请尽早发现这些问题。例如,我们早期的策略语言原型提供了许多表达策略的灵活性,但阻止我们提供可操作的错误消息。我们最终放弃了这种方法,转而采用了一种非常有限的语言,可以提供更好的错误消息。

确保来源清晰

谷歌的可验证构建系统最初将二进制来源异步上传到数据库。然后在部署时,策略引擎使用工件的哈希作为键在数据库中查找来源。

虽然这种方法大多运行良好,但我们遇到了一个主要问题:用户可以多次构建工件,导致相同哈希的多个条目。考虑空文件的情况:我们有数百万条与空文件的哈希相关的来源记录,因为许多不同的构建生成了空文件作为其输出的一部分。为了验证这样的文件,我们的系统必须检查任何来源记录是否符合策略。这反过来又导致了两个问题:

  • 当我们未能找到通过记录时,我们无法提供可操作的错误消息。例如,我们不得不说,“源 URI 是X,但策略应该是Y”,而不是“这 497,129 条记录中没有一条符合策略”。这是糟糕的用户体验。

  • 验证时间与返回的记录数量成正比。这导致我们的延迟 SLO 超出了 100 毫秒数倍!

我们还遇到了与数据库的异步上传问题。上传可能会悄无声息地失败,这种情况下我们的策略引擎会拒绝部署。与此同时,用户不明白为什么被拒绝。我们本可以通过使上传同步来解决这个问题,但这种解决方案会使我们的构建系统不太可靠。

因此,我们强烈建议使来源清晰。在可能的情况下,避免使用数据库,而是内联传播来源与工件。这样做可以使整个系统更可靠,延迟更低,更易于调试。例如,使用 Kubernetes 的系统可以添加一个传递给 Admission Controller webhook 的注释。

创建明确的策略

与我们推荐的工件来源的方法类似,适用于特定部署的策略应该是明确的。我们建议设计系统,以便任何给定的部署只适用一个策略。考虑另一种选择:如果有两个策略适用,那么两个策略都需要通过吗,还是只需要一个策略通过?最好完全避免这个问题。如果您想在整个组织中应用全局策略,可以将其作为元策略实现:实现一个检查,以确保所有个体策略符合一些全局标准。

包括部署 breakglass

在紧急情况下,可能需要绕过部署策略。例如,工程师可能需要重新配置前端以将流量从失败的后端转移,相应的配置即代码更改可能需要通过常规 CI/CD 管道部署太长时间。绕过策略的 breakglass 机制可以让工程师快速解决故障,并促进安全和可靠性的文化(见第二十一章)。

由于对手可能利用 breakglass 机制,所有 breakglass 部署必须迅速引发警报并进行审计。为了使审计变得实用,breakglass 事件应该是罕见的——如果事件太多,可能无法区分恶意活动和合法使用。

重新审视威胁模型的安全防护

现在我们可以将高级缓解措施映射到以前未解决的威胁,如表 14-2 所示。

表 14-2. 复杂威胁示例的高级缓解措施

威胁 缓解
工程师部署了一个存在已知漏洞的旧版本代码。 部署策略要求代码在过去的N天内进行了安全漏洞扫描。
CI 系统配置错误,允许从任意源代码库构建请求。结果,恶意对手可以从包含恶意代码的源代码库构建。 CI 系统生成描述其拉取源代码库的二进制来源。生产环境强制执行部署策略,要求来源证明部署的工件来自批准的源代码库。
恶意对手向 CI 系统上传自定义构建脚本,窃取签名密钥。然后对手使用该密钥签名和部署恶意二进制文件。 可验证构建系统分离权限,以便运行自定义构建脚本的组件无法访问签名密钥。
恶意对手欺骗 CD 系统使用带有后门的编译器或构建工具生成恶意二进制文件。 严格构建要求开发人员在源代码中明确指定编译器和构建工具的选择。这个选择像所有其他代码一样经过同行评审。

通过在软件供应链周围采取适当的安全控制,您可以缓解甚至是高级和复杂的威胁。

结论

本章的建议可以帮助您加强软件供应链对各种内部威胁的防范。代码审查和自动化是防止错误和增加恶意行为攻击成本的基本策略。代码作为配置将这些好处扩展到传统上受到比代码更少关注的配置。同时,基于工件的部署控制,特别是涉及二进制来源和可验证构建的控制,可以提供对复杂对手的保护,并允许您随着组织的增长而扩展。

7.这些建议有助于确保您编写和测试的代码(遵循第十二章和第十三章的原则)实际上是部署在生产环境中的代码。然而,尽管您已经尽力,您的代码可能并不总是按预期行为。当发生这种情况时,您可以使用下一章介绍的一些调试策略。

18.(1)代码审查也适用于对配置文件的更改;请参阅“将配置视为代码”。

11.(2)Sadowski, Caitlin 等人。2018 年。“现代代码审查:谷歌的案例研究。”第 40 届国际软件工程大会论文集:181-190。doi:10.1145/3183519.3183525。

2.(3)当与配置即代码和本章描述的部署策略结合时,代码审查构成了任意系统的多方授权系统的基础。

1.(4)有关代码审查者的责任,请参阅“审查文化”

3.(5)步骤的“链”不一定需要完全自动。例如,通常可以接受人类能够启动构建或部署步骤。但是,人类不应该能够以任何有意义的方式影响该步骤的行为。

16.(6)尽管如此,这样的授权检查对于最小特权原则仍然是必要的(请参阅第五章)。

10.(7)紧急开关机制可以绕过策略,允许工程师快速解决故障。请参阅“紧急开关”

4.(8)这个概念在SRE 书籍第八章和 SRE workbook 的第十四章和第十五章中有更详细的讨论。所有这些章节中的建议都适用于这里。

12.(9)YAML 是 Kubernetes 使用的配置语言

5.(10)您必须记录和审计这些手动覆盖,以防对手使用手动覆盖作为攻击向量。

15.(11)请注意,真实性意味着完整性。

6.(12)Git 提交 ID 是提供整个源代码树完整性的加密哈希。

8.(13)有关回滚到有漏洞版本的讨论,请参阅“最低可接受的安全版本号”

13.(14)例如,您可能需要证明Cloud Security Scanner在运行此特定代码版本的测试实例时未发现任何结果。

14.(15)请记住,纯签名仍然算作“二进制来源”,如前一节所述。

17.(16)请参阅“设计目标和要求”

9.(17)例如,SRE 书籍将“hermetic”和“reproducible”这两个术语互换使用。Reproducible Builds 项目将“reproducible”定义为本章定义的方式,但有时也会将“reproducible”重载为“可验证”。

¹⁸ 作为一个反例,考虑一个构建过程,在构建过程中获取依赖的最新版本,但在其他方面产生相同的输出。只要两次构建大致同时发生,这个过程就是可重现的,但不是完全隔离的。

¹⁹ 实际上,必须有一种方式将软件部署到节点本身——引导加载程序、操作系统、Kubernetes 软件等等——而且该部署机制必须有自己的策略执行,这很可能是与用于 pod 的实现完全不同的实现。

²⁰ 理想情况下,日志是非常可靠和防篡改的,即使在停机或系统受到威胁的情况下也是如此。例如,假设 Kubernetes 主节点在日志后端不可用时接收到一个请求。主节点可以暂时将日志保存到本地磁盘。如果机器在日志后端恢复之前死机了怎么办?或者如果机器的空间用完了怎么办?这是一个具有挑战性的领域,我们仍在开发解决方案。

²¹ Borg 分配(简称 分配)是机器上一组保留的资源,其中可以在容器中运行一个或多个 Linux 进程集。软件包包含 Borg 作业的二进制文件和数据文件。有关 Borg 的完整描述,请参见 Verma, Abhishek 等人 2015 年的文章《Google 的 Borg 大规模集群管理》。第 10 届欧洲计算机系统会议论文集:1–17。doi:10.1145/2741948.2741964。

第十五章:调查系统

原文:https://google.github.io/building-secure-and-reliable-systems/raw/ch14.html

译者:飞龙

协议:CC BY-NC-SA 4.0

Pete Nuttall、Matt Linton 和 David Seidman

与 Vera Haas、Julie Saracino 和 Amaya Booker

在理想的世界中,我们都会构建完美的系统,我们的用户只会怀有最好的意图。然而,在现实中,您会遇到错误,并需要进行安全调查。当您观察生产中运行的系统时,您会确定需要改进的地方,以及可以简化和优化流程的地方。所有这些任务都需要调试和调查技术,以及适当的系统访问。

然而,即使是只读调试访问也会带来滥用的风险。为了解决这一风险,您需要适当的安全机制。您还需要在开发人员和运营人员的调试需求与存储和访问敏感数据的安全要求之间取得谨慎的平衡。

在本章中,我们使用术语调试器来指代调试软件问题的人类,而不是GDB(GNU 调试器)或类似的工具。除非另有说明,我们使用“我们”一词来指代本章的作者,而不是整个谷歌公司。

从调试到调查

“我完全意识到,我余生的很大一部分时间将花在查找自己程序中的错误上。”

——Maurice Wilkes,《计算机先驱的回忆录》(麻省理工学院出版社,1985 年)

调试声誉不佳。错误总是在最糟糕的时候出现。很难估计错误何时会被修复,或者何时系统会“足够好”让许多人使用。对大多数人来说,编写新代码比调试现有程序更有趣。调试可能被认为是没有回报的。然而,它是必要的,当通过学习新的事实和工具的视角来看待时,你甚至可能会发现这种实践是令人愉快的。根据我们的经验,调试也使我们成为更好的程序员,并提醒我们有时我们并不像我们认为的那么聪明。

示例:临时文件

考虑以下停机事件,我们(作者)在两年前调试过。[1]调查开始时,我们收到了一个警报,称Spanner 数据库的存储配额即将用完。我们经历了调试过程,问自己以下问题:

  1. 是什么导致数据库存储空间不足?

快速分诊表明,问题是由谷歌庞大的分布式文件系统Colossus中创建了许多小文件积累导致的,这可能是由用户请求流量的变化触发的。

  1. 是什么在创建所有这些小文件?

我们查看了服务指标,显示这些文件是 Spanner 服务器内存不足导致的。根据正常行为,最近的写入(更新)被缓存在内存中;当服务器内存不足时,它将数据刷新到 Colossus 上的文件中。不幸的是,Spanner 区中的每台服务器只有少量内存来容纳更新。因此,与其刷新可管理数量的较大、压缩的文件,[2]每台服务器都会刷新许多小文件到 Colossus。

  1. 内存使用在哪里?

每个服务器都作为一个 Borg 任务(在容器中)运行,这限制了可用的内存。[3]为了确定内核内存的使用位置,我们直接在生产机器上发出了slabtop命令。我们确定目录条目(dentry)缓存是内存的最大使用者。

  1. 为什么 dentry 缓存这么大?

我们做出了一个合理的猜测,即 Spanner 数据库服务器正在创建和删除大量临时文件——每次刷新操作都会有一些。每次刷新操作都会增加 dentry 缓存的大小,使问题变得更糟。

  1. 我们如何确认我们的假设?

为了验证这个理论,我们在 Borg 上创建并运行了一个程序,通过在循环中创建和删除文件来复现这个错误。几百万个文件后,dentry 缓存已经使用了容器中的所有内存,证实了假设。

  1. 这是一个内核 bug 吗?

我们研究了 Linux 内核的预期行为,并确定内核缓存了文件的不存在——一些构建系统需要这个特性来确保可接受的性能。在正常操作中,当容器满时,内核会从 dentry 缓存中驱逐条目。然而,由于 Spanner 服务器反复刷新更新,容器从未变得足够满以触发驱逐。我们通过指定临时文件不需要被缓存来解决了这个问题。

这里描述的调试过程展示了我们在本章讨论的许多概念。然而,这个故事最重要的收获是我们调试了这个问题—而你也可以!解决和修复问题并不需要任何魔法;它只需要缓慢和有条理的调查。要分解我们调查的特征:

  • 在系统显示出退化迹象后,我们使用现有的日志和监控基础设施调试了这个问题。

  • 我们能够调试这个问题,即使它发生在内核空间和调试人员以前没有见过的代码中。

  • 我们在这次停机之前从未注意到这个问题,尽管它可能已经存在了几年。

  • 系统的任何部分都没有损坏。所有部分都按预期工作。

  • Spanner 服务器的开发人员惊讶地发现,临时文件在文件被删除后仍然可以消耗内存。

  • 我们能够通过使用内核开发人员提供的工具来调试内核的内存使用情况。尽管我们以前从未使用过这些工具,但由于我们在调试技术上接受了培训并且有丰富的实践经验,我们能够相对快速地取得进展。

  • 我们最初误诊了错误为用户错误。只有在检查了我们的数据后,我们才改变了主意。

  • 通过提出假设,然后创建一种测试我们理论的方法,我们在引入系统更改之前确认了根本原因。

调试技术

本节分享了一些系统化调试技术。(4) 调试是一种可以学习和实践的技能。SRE 书的第十二章提供了成功调试的两个要求:

  • 了解系统应该如何工作。

  • 要有系统性:收集数据,假设原因,测试理论。

第一个要求更加棘手。以一个由单个开发人员构建的系统为例,突然离开公司,带走了对系统的所有了解。系统可能会继续工作数月,但有一天它神秘地崩溃了,没有人能够修复它。接下来的一些建议可能有所帮助,但事先了解系统是没有真正替代品的(参见第六章)。

区分马和斑马

当你听到马蹄声时,你首先想到的是马还是斑马?教师有时会向学习如何分诊和诊断疾病的医学生提出这个问题。这是一个提醒,大多数疾病是常见的——大多数马蹄声是由马引起的,而不是斑马。你可以想象为什么这对医学生是有帮助的建议:他们不想假设症状会导致罕见疾病,而实际上这种情况是常见的,而且很容易治疗。

相比之下,经验丰富的工程师在足够大的规模下会观察到常见的罕见的事件。构建计算机系统的人可以(也必须)努力完全消除所有问题。随着系统规模的增长,运营商随着时间消除了常见问题,罕见问题出现得更频繁。引用Bryan Cantrill的话:“随着时间的推移,马被找到了;只有斑马留下了。”

考虑一种非常罕见的内存损坏问题:位翻转引起的内存损坏。现代纠错内存模块每年遇到无法纠正的位翻转的概率不到 1%⁵。一位工程师调试一个意外的崩溃可能不会想到,“我打赌这是由于内存芯片中极不可能的电气故障引起的!”然而,在非常大的规模下,这些罕见事件变得确定。一个假设的云服务使用 25,000 台机器,可能使用 400,000 个 RAM 芯片的内存。考虑到每个芯片每年发生无法纠正错误的风险不到 0.1%,服务的规模可能导致每年发生 400 次。运行云服务的人可能每天都会观察到内存故障。

调试这些罕见事件可能具有挑战性,但通过正确类型的数据是可以实现的。举个例子,谷歌的硬件工程师曾经注意到某些 RAM 芯片的故障率远远超出预期。资产数据使他们能够追踪故障 DIMM(内存模块)的来源,并且他们能够将这些模块追溯到单个供应商。经过大量的调试和调查,工程师们确定了根本原因:在生产 DIMM 的单个工厂的无尘室中发生了环境故障。这个问题是一个“斑马”——一个只在规模上可见的罕见错误。

随着服务的增长,今天的奇怪异常错误可能会成为明年的常见错误。在 2000 年,谷歌对内存硬件损坏感到惊讶。如今,这样的硬件故障是常见的,我们通过端到端的完整性检查和其他可靠性措施来计划处理它们。

近年来,我们遇到了一些其他罕见事件:

  • 两个网络搜索请求散列到相同的 64 位缓存密钥,导致一个请求的结果被用来替代另一个请求。

  • C++将int64转换为int(只有 32 位),导致在 2³²个请求后出现问题(有关此错误的更多信息,请参见“清理代码”)。

  • 分布式再平衡算法中的一个错误只有在代码同时在数百台服务器上运行时才会触发。

  • 有人让负载测试运行了一个星期,导致性能逐渐下降。我们最终确定机器逐渐出现了内存分配问题,导致了性能下降。我们发现了这个罕见的错误,因为一个通常寿命较短的测试运行时间比正常情况下长得多。

  • 调查缓慢的 C++测试表明,动态链接器的加载时间与加载的共享库数量呈超线性关系:在加载了 10,000 个共享库时,启动运行main可能需要几分钟。

在处理较小、较新的系统时,预计会出现常见的错误。在处理较老、较大和相对稳定的系统时,预计会出现罕见的错误——操作员可能已经观察到并修复了随时间出现的常见错误。问题更有可能在系统的新部分出现。

为调试和调查留出时间

安全调查(稍后讨论)和调试通常需要很长时间,需要连续数小时的工作。前一节中描述的临时文件情况需要 5 到 10 小时的调试。在运行重大事件时,通过将调试人员和调查人员与逐分钟的响应隔离开来,为他们提供专注的空间。

调试奖励缓慢、系统的、持久的方法,人们在这种方法中会反复检查他们的工作和假设,并愿意深入挖掘。临时文件问题也提供了调试的一个负面例子:最初的第一响应者最初诊断停机是由用户流量引起的,并将系统行为不佳归咎于用户。当时,团队处于运营超载状态,并因非紧急页面而感到疲劳。

注意

SRE 工作手册的第十七章讨论了减少运营超载。SRE 书的第十一章建议每班次将工单和寻呼器数量控制在两个以下,以便工程师有时间深入研究问题。

记录你的观察和期望

写下你所看到的。另外,写下你的理论,即使你已经拒绝了它们。这样做有几个优点:

  • 这为调查引入了结构,并帮助你记住调查过程中所采取的步骤。当你开始调试时,你不知道解决问题需要多长时间——解决可能需要五分钟或五个月。

  • 另一个调试器可以阅读你的笔记,了解你观察到的情况,并快速参与或接管调查。你的笔记可以帮助队友避免重复工作,并可能激发其他人想到新的调查途径。有关这个主题的更多信息,请参见SRE 书的第十二章中的“负面结果是魔术”。

  • 在潜在的安全问题的情况下,保留每次访问和调查步骤的日志可能是有帮助的。以后,你可能需要证明(有时是在法庭上)哪些行动是攻击者执行的,哪些是调查人员执行的。

在你写下你观察到的内容之后,写下你期望观察到的内容以及原因。错误经常隐藏在你对系统的心理模型和实际实现之间的空间中。在临时文件的例子中,开发人员假设删除文件会删除所有对它的引用。

了解你的系统的正常情况

通常,调试人员开始调试实际上是预期的系统行为。以下是我们经验中的一些例子:

  • 一个名为abort的二进制文件在其shutdown代码的末尾。新开发人员在日志中看到了abort调用,并开始调试该调用,没有注意到有趣的故障实际上是shutdown调用的原因。

  • 当 Chrome 网页浏览器启动时,它会尝试解析三个随机域名(比如cegzaukxwefark.local)以确定网络是否在非法篡改 DNS。甚至 Google 自己的调查团队也曾将这些 DNS 解析误认为是恶意软件试图解析命令和控制服务器主机名。

调试器经常需要过滤掉这些正常事件,即使这些事件看起来与问题相关或可疑。安全调查人员面临的额外问题是持续的背景噪音和可能试图隐藏行动的积极对手。你通常需要过滤掉常规的嘈杂活动,比如自动 SSH 登录暴力破解、用户输错密码导致的认证错误以及端口扫描,然后才能观察到更严重的问题。

了解正常系统行为的一种方法是在你不怀疑有任何问题时建立系统行为的基线。如果你已经有了问题,你可以通过检查问题发生之前的历史日志来推断你的基线。

例如,在第一章中,我们描述了由于对通用日志库的更改而导致的全球 YouTube 中断。这个更改导致服务器耗尽内存(OOM)并失败。由于该库在 Google 内部被广泛使用,我们在事后调查中质疑这次中断是否影响了所有其他 Borg 任务的 OOM 数量。虽然日志表明那天我们有很多 OOM 条件,但我们能够将这些数据与前两周的数据基线进行比较,结果显示 Google 每天都有很多 OOM 条件。尽管这个错误很严重,但它并没有对 Borg 任务的 OOM 指标产生实质性影响。

注意不要将最佳实践的偏差标准化。通常,错误会随着时间变成“正常行为”,您不再注意到它们。例如,我们曾经在一台服务器上工作,其堆内存碎片化达到了约 10%。经过多年的断言,约 10%是预期的,因此是可以接受的损失量,我们检查了碎片化配置文件,很快发现了节省内存的重大机会。

运营超载和警报疲劳可能导致您产生盲点,并因此标准化偏差。为了解决标准化偏差,我们积极倾听团队中的新人,并为了促进新的视角,我们轮换人员参与值班轮换和响应团队——编写文档和向他人解释系统的过程也可能促使您质疑自己对系统的了解程度。此外,我们使用红队(见第二十章)来测试我们的盲点。

重现错误

如果可能的话,尝试在生产环境之外重现错误。这种方法有两个主要优点:

  • 您不会影响为实际用户提供服务的系统,因此可以随意使系统崩溃并损坏数据。

  • 因为您没有暴露任何敏感数据,所以您可以在不引起数据安全问题的情况下让许多人参与调查。您还可以启用与实际用户数据不适当的操作和额外日志记录等功能。

有时,在生产环境之外进行调试是不可行的。也许错误只会在规模上触发,或者您无法隔离其触发器。临时文件示例就是这样一种情况:我们无法通过完整的服务堆栈重现错误。

隔离问题

如果您能重现问题,下一步是隔离问题,理想情况下是将问题隔离到仍然出现问题的代码最小子集。您可以通过禁用组件或临时注释子程序来做到这一点,直到问题显现出来。

在临时文件示例中,一旦我们观察到所有服务器上的内存管理表现出异常,我们就不再需要在每台受影响的机器上调试所有组件。再举一个例子,考虑一个单独的服务器(在一个大型系统集群中)突然开始引入高延迟或错误。这种情况是对您的监控、日志和其他可观察性系统的标准测试:您能否快速找到系统中众多服务器中的一个坏服务器?有关更多信息,请参见“卡住时该怎么办”。

您还可以在代码内部隔离问题。举一个具体的例子,我们最近调查了一个具有非常有限内存预算的程序的内存使用情况。特别是,我们检查了线程堆栈的内存映射。尽管我们的心理模型假设所有线程具有相同的堆栈大小,但令我们惊讶的是,我们发现不同的线程堆栈有许多不同的大小。一些堆栈非常大,有可能消耗大量内存预算。最初的调试范围包括内核、glibc、Google 的线程库以及所有启动线程的代码。基于 glibc 的pthread_create的一个简单示例创建了相同大小的线程堆栈,因此我们可以排除内核和 glibc 作为不同大小的来源。然后我们检查了启动线程的代码,发现许多库只是随机选择了线程大小,解释了大小的变化。这种理解使我们能够通过专注于少数具有大堆栈的线程来节省内存。

谨慎对待相关性与因果关系

有时调试器会假设同时开始的两个事件或表现出类似症状的事件具有相同的根本原因。然而,相关性并不总是意味着因果关系。两个平凡的问题可能同时发生,但有不同的根本原因。

有些相关性是微不足道的。例如,延迟增加可能导致用户请求减少,仅仅是因为用户等待系统响应的时间更长。如果一个团队反复发现事后看来微不足道的相关性,可能是因为他们对系统应该如何工作的理解存在差距。在临时文件的例子中,如果你知道删除文件失败会导致磁盘满,你就不会对这种相关性感到惊讶。

然而,我们的经验表明,调查相关性通常是有用的,特别是在停机开始时发生的相关性。通过思考,“X出问题了,Y出问题了,Z出问题了;这三者之间有什么共同点?”你可以找到可能的原因。我们还在基于相关性的工具上取得了一些成功。例如,我们部署了一个系统,可以自动将机器问题与机器上运行的 Borg 任务进行相关联。因此,我们经常可以确定一个可疑的 Borg 任务导致了广泛的问题。这种自动化工具产生的相关性比人类观察更有效、统计学上更强大,而且更快。

错误也可能在部署过程中显现——参见SRE 书中的第十二章。在简单的情况下,正在部署的新代码可能存在问题,但部署也可能触发旧系统中的潜在错误。在这些情况下,调试可能错误地集中在正在部署的新代码上,而不是潜在的问题。系统性的调查——确定发生了什么,以及为什么——在这些情况下是有帮助的。我们曾经见过的一个例子是,旧代码的性能比新代码差得多,这导致了对整个系统的意外限制。当其性能改善时,系统的其他部分反而过载。停机与新部署相关联,但部署并不是根本原因。

用实际数据测试你的假设

在调试时,很容易在实际查看系统之前就对问题的根本原因进行推测。在处理性能问题时,这种倾向会引入盲点,因为问题通常出现在调试人员长时间没有查看的代码中。例如,有一次我们在调试一个运行缓慢的 Web 服务器。我们假设问题出在后端,但分析器显示,将每一点可能的输入记录到磁盘,然后调用sync导致了大量的延迟。只有当我们放下最初的假设并深入研究系统时,我们才发现了这一点。

可观察性是通过检查系统的输出来确定系统正在做什么的属性。像DapperZipkin这样的追踪解决方案对这种调试非常有用。调试会话从基本问题开始,比如,“你能找到一个慢的 Dapper 跟踪吗?”⁶

注意

对于初学者来说,确定最适合工作的工具,甚至了解存在哪些工具可能是具有挑战性的。Brendan Gregg 的《系统性能》(Prentice Hall,2013)提供了全面的工具和技术介绍,是性能调试的绝佳参考。

重新阅读文档

考虑来自Python 文档的以下指导:

比较运算符之间没有暗示的关系。x==y的真实性并不意味着x!=y是假的。因此,在定义__eq__()时,应该同时定义__ne__(),以便运算符的行为符合预期。

最近,谷歌团队花了大量时间调试内部仪表板优化。当他们陷入困境时,团队重新阅读了文档,并发现了一个明确的警告消息,解释了为什么优化从未起作用。人们习惯了仪表板的性能缓慢,他们没有注意到优化完全无效。⁷最初,这个错误似乎很显著;团队认为这是 Python 本身的问题。在他们找到警告消息后,他们确定这不是斑马,而是马——他们的代码从未起作用。

练习!

调试技能只有经常使用才能保持新鲜。通过熟悉相关工具和日志,您可以加快调查的速度,并将我们在这里提供的提示保持在脑海中。定期练习调试还提供了机会来脚本化过程中常见和乏味的部分,例如自动化检查日志。要提高调试能力(或保持敏锐),请练习,并保留在调试会话期间编写的代码。

在谷歌,我们通过定期的大规模灾难恢复测试(称为 DiRT,或灾难恢复测试计划)⁸和安全渗透测试(见第十六章)来正式练习调试。规模较小的测试,涉及一两名工程师在一个小时内进行测试,更容易设置,但仍然非常有价值。

当你陷入困境时该怎么办

当您调查一个问题已经几天了,仍然不知道问题的原因时,您应该怎么办?也许它只在生产环境中表现出来,而且您无法在不影响实际用户的情况下重现错误。也许在缓解问题时,日志轮转时丢失了重要的调试信息。也许问题的性质阻止了有用的日志记录。我们曾经调试过一个问题,其中一个内存容器耗尽了 RAM,内核为容器中的所有进程发出了 SIGKILL,停止了所有日志记录。没有日志,我们无法调试这个问题。

在这些情况下的一个关键策略是改进调试过程。有时,使用开发事后总结的方法(如第十八章中所述)可能会提出前进的方法。许多系统已经投入生产多年甚至几十年,因此改进调试的努力几乎总是值得的。本节描述了改进调试方法的一些途径。

提高可观察性

有时,您需要查看一些代码在做什么。这段代码分支被使用了吗?这个函数被使用了吗?这个数据结构可能很大吗?这个后端在 99th 百分位数上很慢吗?这个查询使用了哪些后端?在这些情况下,您需要更好地了解系统。

在某些情况下,像添加更多结构化日志以提高可观察性的方法是直接的。我们曾经调查过一个系统,监控显示它服务了太多的 404 错误,⁹但是 Web 服务器没有记录这些错误。在为 Web 服务器添加额外的日志记录后,我们发现恶意软件试图从系统中获取错误文件。

其他调试改进需要严肃的工程努力。例如,调试像Bigtable这样的复杂系统需要复杂的仪器。Bigtable 主节点是 Bigtable 区域的中央协调器。它在 RAM 中存储服务器和平板的列表,并且几个互斥体保护这些关键部分。随着谷歌的 Bigtable 部署随着时间的推移而增长,Bigtable 主节点和这些互斥体成为了扩展瓶颈。为了更好地了解可能的问题,我们实现了一个包装器,围绕互斥体暴露诸如队列深度和互斥体持有时间等统计信息。

像 Dapper 和 Zipkin 这样的追踪解决方案对于这种复杂的调试非常有用。例如,假设您有一个 RPC 树,前端调用一个服务器,服务器调用另一个服务器,依此类推。每个 RPC 树在根处分配了一个唯一的 ID。然后,每个服务器记录有关其接收的 RPC、发送的 RPC 等的跟踪。Dapper 集中收集所有跟踪,并通过 ID 将它们连接起来。这样,调试器可以看到用户请求所触及的所有后端。我们发现 Dapper 对于理解分布式系统中的延迟至关重要。同样,Google 在几乎每个二进制文件中嵌入了一个简单的 Web 服务器,以便提供对每个二进制文件行为的可见性。该服务器具有调试端点,提供计数器、所有运行线程的符号化转储、正在进行的 RPC 等。有关更多信息,请参阅 Henderson(2017)。

注意

可观察性并不是了解系统的替代品。它也不是调试时批判性思维的替代品(遗憾!)。我们经常发现自己在疯狂地添加更多日志记录和计数器,以便了解系统正在做什么,但只有当我们退后一步并思考问题时,真正发生的情况才会变得清晰。

可观察性是一个庞大且快速发展的主题,它不仅对调试有用。如果您是一个开发资源有限的较小组织,可以考虑使用开源系统或购买第三方可观察性解决方案。

休息一下

与问题保持一定的距离往往会在回到问题时带来新的见解。如果您一直在进行调试并遇到停顿,请休息一下:喝点水,出去走走,锻炼一下,或者读一本书。有时候,好好睡一觉后,bug 会显露出来。我们的法医调查团队的一位资深工程师在团队的实验室里放了一把大提琴。当他真正陷入问题时,他通常会退到实验室 20 分钟左右来演奏;然后他重新充满活力和专注。另一位调查员随时保持一把吉他,其他人在桌子上保留着素描和涂鸦本,这样他们在需要进行心智调整时可以画画或制作一个愚蠢的动画 GIF 与团队分享。

还要确保保持良好的团队沟通。当您离开一会儿休息时,让团队知道您需要休息并且正在遵循最佳实践。记录调查的进展和您陷入困境的原因也是有帮助的。这样做可以使另一位调查员更容易接手您的工作,也可以让您回到离开的地方。第十七章有关于保持士气的更多建议。

清理代码

有时您怀疑代码块中存在 bug,但却看不到它。在这种情况下,试图通用地提高代码质量可能会有所帮助。正如本章前面提到的,我们曾经调试过一段代码,它在 2³²个请求后在生产环境中失败,因为 C++将int64转换为int(仅 32 位)并截断了它。尽管编译器可以使用-Wconversion警告您有关此类转换,但我们没有使用该警告,因为我们的代码有许多良性转换。清理代码使我们能够使用编译器警告来检测更多可能的 bug,并防止与转换相关的新 bug。

以下是一些清理的其他提示:

  • 提高单元测试覆盖率。针对您怀疑可能存在 bug 的函数,或者具有出现 bug 的记录的函数。 (有关更多信息,请参见第十三章。)

  • 对于并发程序,请使用消毒剂(参见“消毒您的代码”)和注释互斥锁

  • 改善错误处理。通常,围绕错误添加一些更多的上下文就足以暴露问题。

删除它!

有时候错误隐藏在传统系统中,特别是如果开发人员没有时间熟悉或保持熟悉代码库,或者维护已经中断。传统系统也可能受到损害或产生新的错误。与其调试或加固传统系统,不如考虑删除它。

删除传统系统也可以改善您的安全姿态。例如,有一位作者曾经通过谷歌的漏洞奖励计划(如第二十章中所述)被一位安全研究人员联系,后者在我们团队的一个传统系统中发现了一个安全问题。团队之前已经将该系统隔离到自己的网络中,但已经有一段时间没有升级该系统了。团队的新成员甚至不知道传统系统的存在。为了解决研究人员的发现,我们决定删除该系统。我们不再需要它提供的大部分功能,并且我们能够用一个更简单的现代系统来替换它。

注意

在重写传统系统时要慎重。问问自己,为什么您重写的系统会比传统系统做得更好。有时候,您可能想重写一个系统,因为添加新代码很有趣,而调试旧代码很乏味。替换系统有更好的理由:有时候系统的需求会发生变化,只需少量工作,您就可以删除旧系统。或者,也许您从第一个系统中学到了一些东西,并且可以将这些知识融入到第二个系统中,使其变得更好。

当事情开始出错时停下来

许多错误很难找到,因为错误源和其影响在系统中可能相距甚远。我们最近遇到了一个问题,网络设备正在破坏数百台机器的内部 DNS 响应。例如,程序将对机器exa1进行 DNS 查找,但收到的是exa2的地址。我们的两个系统对这个错误有不同的响应:

  • 一个系统,一个档案服务,会连接到exa2,错误的机器。然而,系统随后检查连接的机器是否是预期的机器。由于机器名称不匹配,档案服务作业失败。

  • 另一个收集机器指标的系统会从错误的机器exa2收集指标。然后,系统会在exa1上触发修复。我们只有在一名技术人员指出他们被要求修复一个没有五个磁盘的机器的第五个磁盘时才检测到这种行为。

在这两种响应中,我们更喜欢档案服务的行为。当系统中的问题及其影响相距甚远时,例如当网络导致应用程序级错误时,使应用程序失败可以防止下游影响(例如怀疑错误系统上的磁盘故障)。我们在第八章中更深入地讨论了是选择失败开放还是失败关闭的话题。

改进访问和授权控制,即使对于非敏感系统也是如此

“厨房里有太多厨师”是有可能的——也就是说,您可能会遇到许多人可能是错误源的调试情况,这使得难以隔离原因。我们曾经因为一个损坏的数据库行而导致了一次宕机,我们无法找到损坏数据的来源。为了消除有人可能会错误地写入生产数据库的可能性,我们最小化了具有访问权限的角色数量,并要求对任何人类访问进行理由说明。尽管数据并不敏感,但实现标准的安全系统帮助我们预防和调查未来的错误。幸运的是,我们还能够从备份中恢复该数据库行。

协作调试:一种教学方法

许多工程团队通过亲自(或通过视频会议)共同解决实际的实时问题来教授调试技术。除了保持经验丰富的调试者的技能更新外,协作调试还有助于为新团队成员建立心理安全感:他们有机会看到团队中最优秀的调试者遇到困难、后退或者有其他困难,这向他们表明,出错和遇到困难是可以接受的。有关安全教育的更多信息,请参见第二十一章。

我们发现以下规则优化了学习体验:

  • 只有两个人可以打开笔记本电脑:

  • “驱动者”,执行其他人要求的操作

  • “笔记记录者”

  • 每个行动都应由观众决定。只有驱动者和笔记记录者被允许使用计算机,但他们不确定采取的行动。这样,参与者不会独自进行调试,然后提出答案而不分享他们的思考过程和故障排除步骤。

团队共同确定要检查的一个或多个问题,但房间里没有人事先知道如何解决这些问题。每个人都可以要求驱动者执行一个操作来排除故障(例如,打开仪表板,查看日志,重新启动服务器等)。由于每个人都在场见证这些建议,每个人都可以了解参与者建议的工具和技术。即使是经验丰富的团队成员也会从这些练习中学到新东西。

正如 SRE 书的第 28 章所述,一些团队还使用“Wheel of Misfortune”模拟练习。这些练习可以是理论性的,通过口头解决问题的步骤,也可以是实际的,测试者在系统中引入故障。这些场景还涉及两种角色:

  • “测试者”,构建和呈现测试的人

  • “测试者”,试图解决问题,也许在队友的帮助下

一些团队更喜欢安全的分阶段练习环境,但实际的 Wheel of Misfortune 练习需要非常复杂的设置,而大多数系统总是有实时问题需要共同调试。无论采取何种方法,保持一个包容的学习环境非常重要,让每个人都感到安全,积极地做出贡献。

协作调试和 Wheel of Misfortune 练习是向团队介绍新技术和强化最佳实践的绝佳方式。人们可以看到这些技术在现实情况下的用处,通常是解决最棘手的问题。团队也可以一起练习调试问题,使他们在真正的危机发生时更加有效。

安全调查和调试的不同之处

我们期望每个工程师都能调试系统,但我们建议受过训练和有经验的安全和取证专家来调查系统的妥协。当“错误调查”和“安全问题”之间的界限不清晰时,两组专家团队之间有合作的机会。

错误调查通常在系统出现问题时开始。调查侧重于系统中发生了什么:发送了什么数据,这些数据发生了什么,服务如何开始与其意图相反。安全调查开始有点不同,并迅速转向问题,比如:提交了那份工作的用户在做什么?该用户还负责其他什么活动?我们的系统中有活跃的攻击者吗?攻击者接下来会做什么?简而言之,调试更加关注代码,而安全调查可能很快就会关注攻击背后的对手。

我们之前推荐的用于调试问题的步骤在安全调查期间可能也会适得其反。添加新代码、废弃系统等可能会产生意想不到的副作用。我们曾应对过许多事件,其中调试器删除了通常不属于系统的文件,希望解决错误行为,结果发现这些文件是攻击者引入的,因此提醒了调查。在一个案例中,攻击者甚至以删除整个系统的方式做出了回应!

一旦您怀疑发生了安全妥协,您的调查可能也会变得更加紧迫。系统被故意颠覆的可能性引发了一些看起来严肃而紧迫的问题。攻击者的目的是什么?还可能颠覆其他系统吗?您是否需要呼叫执法部门或监管机构?随着组织开始解决运营安全问题,安全调查的复杂性会逐渐增加(第十七章进一步讨论了这个话题)。其他团队的专家,如法律团队,可能会在您开始调查之前介入。简而言之,一旦您怀疑发生了安全妥协,现在是向安全专业人员寻求帮助的好时机。

决定何时停止调查并宣布发生了安全事件可能是一个困难的判断。许多工程师天生倾向于避免通过升级尚未被证明与安全相关的问题来“制造场面”,但继续调查直到证明可能是错误的举动。我们的建议是记住“马和斑马”的区别:绝大多数的错误实际上都是错误,而不是恶意行为。然而,同时也要警惕那些黑白相间的条纹迅速经过。

收集适当和有用的日志

在本质上,日志和系统崩溃转储都只是您可以收集的信息,以帮助您了解系统中发生了什么,并调查问题——无论是意外还是故意的。

在启动任何服务之前,重要的是考虑服务将代表用户存储的数据类型,以及访问数据的途径。假设任何导致数据或系统访问的行为都可能成为未来调查的范围,并且将需要对该行为进行审计。调查任何服务问题安全问题都严重依赖于日志。

我们这里讨论的“日志”是指系统中结构化的、带有时间戳的记录。在调查过程中,分析人员可能还会严重依赖其他数据源,如核心转储、内存转储或堆栈跟踪。我们建议尽量像处理日志一样处理这些系统。结构化日志对许多不同的业务目的都很有用,比如按使用量计费。然而,我们这里关注的是为安全调查收集的结构化日志——你现在需要收集的信息,以便在未来出现问题时可以使用。

设计您的日志记录为不可变

您构建用于收集日志的系统应该是不可变的。当日志条目被写入时,应该很难对其进行更改(但不是不可能;参见“考虑隐私”),并且更改应该有一个不可变的审计跟踪。攻击者通常会在建立牢固立足后立即从所有日志源中擦除其在系统上的活动痕迹。对抗这种策略的一个常见最佳做法是将日志远程写入集中和分布式的日志服务器。这增加了攻击者的工作量:除了攻击原始系统外,他们还必须攻击远程日志服务器。务必仔细加固日志系统。

在现代计算机时代之前,特别关键的服务器直接记录到连接的线打印机上,就像图 15-1 中的那个,它会在生成记录时将日志记录到纸上。为了抹去它们的痕迹,远程攻击者需要有人物理上取下打印机上的纸并将其烧掉!

线打印机

图 15-1:线打印机

考虑隐私

隐私保护功能的需求在系统设计中变得越来越重要。虽然隐私不是本书的重点,但在设计安全调查和调试日志时,您可能需要考虑当地法规和您组织的隐私政策。在这个话题上一定要与您组织内的隐私和法律同事进行咨询。以下是一些您可能想要讨论的话题:

日志深度

为了对任何调查最大限度地有用,日志需要尽可能完整。安全调查可能需要检查用户(或使用其帐户的攻击者)在系统内执行的每个操作,他们登录的主机以及事件发生的确切时间。就日志记录而言,达成组织政策上的一致意见,关于可以记录哪些信息是可以接受的,是很重要的,因为许多隐私保护技术都不鼓励在日志中保留敏感用户数据。

保留

对于某些调查,长时间保留日志可能是有益的。根据 2018 年的一项研究,大多数组织平均需要约200 天才能发现系统被入侵。谷歌的内部威胁调查依赖于可以追溯数年的操作系统安全日志。您组织内部关于保留日志的时间长度的讨论是很重要的。

访问和审计控制

我们推荐用于保护数据的许多控制措施也适用于日志。一定要像保护其他数据一样保护日志和元数据。请参阅第五章以获取相关策略。

数据匿名化或假名化

匿名化不必要的数据组件——无论是在写入时还是一段时间后——是一种越来越常见的隐私保护方法,用于处理日志。您甚至可以实现此功能,以便调查人员和调试人员无法确定给定用户是谁,但可以清楚地构建该用户在调试过程中的会话期间的时间线。匿名化很难做到。我们建议咨询隐私专家并阅读有关此话题的已发表文献。¹³

加密

您还可以使用数据的非对称加密来实现隐私保护的日志记录。这种加密方法非常适合保护日志数据:它使用一个非敏感的“公钥”,任何人都可以使用它来安全地写入数据,但需要一个秘密(私钥)来解密数据。像每日密钥对这样的设计选项可以让调试人员从最近的系统活动中获取小的日志数据子集,同时防止某人获取大量的日志数据。一定要仔细考虑如何存储密钥。

确定要保留哪些安全日志

尽管安全工程师通常更喜欢有太多的日志而不是太少的日志,但在记录和保留日志时要有所选择也是值得的。存储过多的日志可能会很昂贵(如“日志预算”中所讨论的),并且筛选过大的数据集可能会减慢调查人员的速度并使用大量资源。在本节中,我们讨论了一些您可能想要捕获和保留的日志类型。

操作系统日志

大多数现代操作系统都内置了日志记录功能。Windows 拥有 Windows 事件日志,而 Linux 和 Mac 拥有 syslog 和 auditd 日志。许多供应商提供的设备(如摄像头系统、环境控制和火警面板)也有标准操作系统,同时也会产生日志(如 Linux)在幕后。内置的日志框架对于调查非常有用,而且几乎不需要任何努力,因为它们通常默认启用或者很容易配置。一些机制,比如 auditd,出于性能原因默认情况下未启用,但在现实世界的使用中启用它们可能是可以接受的折衷方案。

主机代理

许多公司选择通过在工作站和服务器上安装主机入侵检测系统(HIDS)或主机代理来启用额外的日志记录功能。

现代(有时被称为“下一代”)主机代理使用旨在检测日益复杂威胁的创新技术。一些代理结合了系统和用户行为建模、机器学习和威胁情报,以识别以前未知的攻击。其他代理更专注于收集有关系统操作的额外数据,这对离线检测和调试活动很有用。一些代理,如OSQueryGRR,提供了对系统的实时可见性。

主机代理总是会影响性能,并且经常成为最终用户和 IT 团队之间摩擦的源头。一般来说,代理可以收集的数据越多,其性能影响可能就越大,因为它需要更深入的平台集成和更多的主机处理。一些代理作为内核的一部分运行,而另一些作为用户空间应用程序运行。内核代理具有更多功能,因此通常更有效,但它们可能会因为要跟上操作系统功能变化而遭受可靠性和性能问题。作为应用程序运行的代理更容易安装和配置,并且往往具有较少的兼容性问题。主机代理的价值和性能差异很大,因此我们建议在使用之前对主机代理进行彻底评估。

应用程序日志

日志记录应用程序——无论是供应商提供的,如 SAP 和 Microsoft SharePoint,还是开源的,或者是自定义的——都会生成您可以收集和分析的日志。然后,您可以使用这些日志进行自定义检测,并增强调查数据。例如,我们使用来自 Google Drive 的应用程序日志来确定受损计算机是否下载了敏感数据。

在开发自定义应用程序时,安全专家和开发人员之间的合作可以确保应用程序记录安全相关的操作,例如数据写入、所有权或状态的更改以及与帐户相关的活动。正如我们在“提高可观察性”中提到的,为日志记录仪器化您的应用程序也可以促进调试,以解决其他情况下难以排查的安全和可靠性问题。

云日志

越来越多的组织正在将其业务或 IT 流程的部分转移到基于云的服务,从软件即服务(SaaS)应用程序中的数据到运行关键客户端工作负载的虚拟机。所有这些服务都呈现出独特的攻击面,并生成独特的日志。例如,攻击者可以破坏云项目的帐户凭据,部署新的容器到项目的 Kubernetes 集群,并使用这些容器从集群的可访问存储桶中窃取数据。云计算模型通常每天启动新实例,这使得在云中检测威胁变得动态和复杂。

在检测可疑活动时,云服务具有优势和劣势。使用诸如 Google 的 BigQuery 之类的服务,收集和存储大量日志数据甚至在云中直接运行检测规则都很容易且相对便宜。Google 云服务还提供了内置的日志记录解决方案,如Cloud Audit LogsStackdriver Logging。另一方面,由于有许多种云服务,很难识别、启用和集中所有您需要的日志。由于开发人员很容易在云中创建新的 IT 资产,许多公司发现很难识别所有基于云的资产。云服务提供商还可能预先确定对您可用的日志,并且这些选项可能无法配置。了解您的提供商日志记录的限制以及您潜在的盲点非常重要。

各种商业软件,通常本身就基于云,旨在检测针对云服务的攻击。大多数成熟的云提供商提供集成的威胁检测服务,例如 Google 的事件威胁检测。许多公司将这些内置服务与内部开发的检测规则或第三方产品结合使用。

云访问安全代理(CASBs)是一类显着的检测和预防技术。CASBs 作为终端用户和云服务之间的中介,以强制执行安全控制并提供日志记录。例如,CASB 可能会阻止用户上传某些类型的文件,或记录用户下载的每个文件。许多 CASB 具有警报检测功能,可向检测团队发出关于潜在恶意访问的警报。您还可以将 CASB 的日志集成到自定义检测规则中。

基于网络的日志记录和检测

自 20 世纪 90 年代末以来,捕获和检查网络数据包的网络入侵检测系统(NIDSs)和入侵预防系统(IPSs)已成为常见的检测和记录技术。IPSs 还可以阻止一些攻击。例如,它们可能捕获有关哪些 IP 地址交换了流量以及有关该流量的有限信息,例如数据包大小。一些 IPSs 可能具有根据可定制标准记录某些数据包的整个内容的能力,例如发送到高风险系统的数据包。其他人还可以实时检测恶意活动并向适当的团队发送警报。由于这些系统非常有用且成本较低,我们强烈建议几乎任何组织使用它们。但是,请仔细考虑谁能有效地处理它们产生的警报。

DNS 查询日志也是有用的基于网络的来源。DNS 日志使您能够查看公司中是否有任何计算机解析了主机名。例如,您可能想查看网络上是否有任何主机对已知恶意主机名执行了 DNS 查询,或者您可能想检查先前解析的域以识别攻击者控制的每台机器访问的域。安全运营团队还可能使用 DNS“陷阱”,虚假解析已知恶意域,以便攻击者无法有效使用。然后,检测系统往往在用户访问陷阱域时触发高优先级警报。

您还可以使用用于内部或出口流量的任何网络代理的日志。例如,您可以使用网络代理扫描网页以查找钓鱼或已知漏洞模式的指示器。在使用代理进行检测时,您还需要考虑员工隐私,并与法律团队讨论使用代理日志。一般来说,我们建议尽可能将检测调整到恶意内容,以最小化您在处理警报时遇到的员工数据量。

日志预算

调试和调查活动会消耗资源。我们曾经处理过的一个系统有 100TB 的日志,其中大部分从未被使用过。由于日志记录消耗了大量资源,并且在没有问题的情况下日志通常被监视得较少,因此很容易在日志记录和调试基础设施上投资不足。为了避免这种情况,我们强烈建议您提前预算日志记录,考虑您可能需要多少数据来解决服务问题或安全事件。

现代日志系统通常整合了关系型数据系统(例如 Elasticsearch 或 BigQuery),以便实时快速地查询数据。该系统的成本随着需要存储和索引的事件数量、需要处理和查询数据的机器数量以及所需的存储空间而增长。因此,在长时间保留数据时,有必要优先考虑来自相关数据源的日志以进行长期存储。这是一个重要的权衡决定:如果攻击者擅长隐藏自己的行踪,可能需要相当长的时间才能发现发生了事件。如果只存储一周的访问日志,可能根本无法调查入侵事件!

我们还建议以下投资策略用于面向安全的日志收集:

  • 专注于具有良好信噪比的日志。例如,防火墙通常会阻止许多数据包,其中大部分是无害的。即使是被防火墙阻止的恶意数据包也可能不值得关注。收集这些被阻止的数据包的日志可能会消耗大量带宽和存储空间,但几乎没有任何好处。

  • 尽可能压缩日志。因为大多数日志包含大量重复的元数据,压缩通常非常有效。

  • 将存储分为“热”和“冷”。您可以将过去的日志转移到廉价的离线云存储(“冷存储”),同时保留与最近或已知事件相关的日志在本地服务器上以供立即使用(“热存储”)。同样,您可能会长时间存储压缩的原始日志,但只将最近的日志放入具有完整索引的昂贵关系型数据库中。

  • 智能地轮换日志。通常,最好首先删除最旧的日志,但您可能希望保留最重要的日志类型更长时间。

强大、安全的调试访问

要调试问题,通常需要访问系统和它们存储的数据。恶意或受损的调试器能否看到敏感信息?安全系统的故障(记住:所有系统都会出现故障!)能否得到解决?您需要确保您的调试系统是可靠和安全的。

可靠性

日志记录是系统可能出现故障的另一种方式。例如,系统可能会因为磁盘空间不足而无法存储日志。在这种情况下,采取开放式失败会带来另一个权衡:这种方法可以使整个系统更具弹性,但攻击者可能会干扰您的日志记录机制。

计划应对可能需要调试或修复安全系统本身的情况。考虑必要的权衡,以确保您不会被系统锁定,但仍然可以保持安全。在这种情况下,您可能需要考虑在安全位置离线保存一组仅用于紧急情况的凭据,当使用时会触发高置信度的警报。例如,最近的一次Google 网络故障导致严重的数据包丢失。当响应者试图获取内部凭据时,认证系统无法连接到一个后端并且失败关闭。然而,紧急凭据使响应者能够进行身份验证并修复网络。

安全性

我们曾经使用的一个用于电话支持的系统允许管理员模拟用户并从他们的角度查看用户界面。作为调试工具,这个系统非常棒;您可以清楚快速地重现用户的问题。然而,这种类型的系统提供了滥用的可能性。从模拟到原始数据库访问的调试端点都需要得到保护。

对于许多事件,调试异常系统行为通常不需要访问用户数据。例如,在诊断 TCP 流量问题时,线上的速度和质量通常足以诊断问题。在传输数据时加密可以保护数据免受第三方可能的观察尝试。这有一个幸运的副作用,即在需要时允许更多的工程师访问数据包转储。然而,一个可能的错误是将元数据视为非敏感信息。恶意行为者仍然可以通过跟踪相关的访问模式(例如,在同一会话中注意到同一用户访问离婚律师和约会网站)从元数据中了解用户的很多信息。您应该仔细评估将元数据视为非敏感信息的风险。

此外,一些分析确实需要实际数据,例如,在数据库中查找频繁访问的记录,然后找出这些访问为什么是常见的。我们曾经调试过一个由单个帐户每小时接收数千封电子邮件引起的低级存储问题。"零信任网络"有关这些情况的访问控制的更多信息。

结论

调试和调查是管理系统的必要方面。重申本章的关键点:

  • 调试是一种必不可少的活动,通过系统化的技术而不是猜测来取得结果。您可以通过实现工具或记录来提供对系统的可见性,从而使调试变得更加容易。练习调试以磨练您的技能。

  • 安全调查与调试不同。它们涉及不同的人员、策略和风险。您的调查团队应包括经验丰富的安全专业人员。

  • 集中式日志记录对于调试目的很有用,对于调查至关重要,并且通常对业务分析也很有用。

  • 通过查看一些最近的调查,并问自己什么信息会帮助您调试问题或调查问题,来迭代。调试是一个持续改进的过程;您将定期添加数据源并寻找改进可观察性的方法。

  • 设计安全性。您需要日志。调试工具需要访问系统和存储的数据。然而,随着您存储的数据量的增加,日志和调试端点都可能成为对手的目标。设计日志系统以收集您需要的信息,但也要求具有强大的权限、特权和政策来获取这些数据。

调试和安全调查通常依赖于突然的洞察力和运气,即使最好的调试工具有时也会不幸地被置于黑暗中。记住,机会青睐有准备的人:通过准备好日志和一个用于索引和调查它们的系统,您可以利用到来的机会。祝你好运!

¹尽管故障发生在一个大型分布式系统中,但维护较小和自包含系统的人会看到很多相似之处,例如,在涉及单个邮件服务器的故障中,其硬盘已经用完空间!

² Spanner 将数据存储为日志结构合并(LSM)树。有关此格式的详细信息,请参阅 Luo, Chen 和 Michael J. Carey. 2018 年的“基于 LSM 的存储技术:一项调查。”arXiv 预印本arXiv:1812.07527v3

³有关 Borg 的更多信息,请参阅 Verma, Abhishek 等人 2015 年的“在 Google 使用 Borg 进行大规模集群管理。”第 10 届欧洲计算机系统会议论文集:1-17。doi:10.1145/2741948.2741964。

⁴ 您可能还对 Julia Evans 的博客文章“调试程序是什么样子?”感兴趣。

⁵ Schroeder, Bianca, Eduardo Pinheiro 和 Wolf-Dietrich Weber. 2009 年。《野外的 DRAM 错误:大规模实地研究》。ACM SIGMETRICS Performance Evaluation Review 37(1)。doi:10.1145/2492101.1555372。

⁶ 这些工具通常需要一些设置;我们将在“当你陷入困境时该怎么办”中进一步讨论它们。

⁷ 这是另一个规范偏差的例子,人们习惯于次优行为!

⁸ 请参阅 Krishnan, Kripa. 2012 年。《应对意外情况》。ACM Queue 10(9)。https://oreil.ly/xFPfT

⁹ 404 是“文件未找到”的标准 HTTP 错误代码。

¹⁰ 亨德森,弗格斯。2017 年。《谷歌的软件工程》。arXiv 预印本arXiv:1702.01715v2

¹¹ 要全面了解这个主题,请参阅 Cindy Sridharan 的“云原生时代的监控”博客文章。

¹² 请参阅 Julia Rozovsky 的博客文章“成功的谷歌团队的五个关键”和查尔斯·杜希格的纽约时报文章“谷歌从寻求打造完美团队的过程中学到了什么”

¹³ 例如,参见 Ghiasvand, Siavash 和 Florina M. Ciorba. 2017 年。《用于隐私和存储收益的系统日志匿名化》。arXiv 预印本arXiv:1706.04337。另请参阅 Jan Lindquist 关于个人数据假名化以符合《通用数据保护条例》(GDPR)的规定。

¹⁴ 例如,参见 Joxean Koret 在 44CON 2014 年的演讲“破解杀毒软件”

第四部分:维护系统

原文:Part IV. Maintaining Systems

译者:飞龙

协议:CC BY-NC-SA 4.0

准备应对不舒适情况的组织有更好的机会处理关键事件。

尽管不可能为可能扰乱您组织的每种情况制定计划,但作为综合灾难规划策略的第一步,正如第十六章中所讨论的那样,是务实和可行的。这些步骤包括建立一个事件响应团队,在事件发生之前为系统和人员进行预置,并测试系统和响应计划——这些准备步骤也将有助于为危机管理做好准备,这是第十七章的主题。在处理安全危机时,具有各种技能和角色的人员需要能够有效地合作和沟通,以保持系统运行。

在遭受攻击后,您的组织将需要控制恢复并处理后果,正如第十八章中所讨论的那样。在这些阶段进行一些前期规划也将有助于您进行彻底的响应,并从中吸取教训,以防止再次发生。

为了更全面地了解情况,我们还包括了一些特定章节的背景示例:

  • 第十六章讲述了谷歌如何制定应对旧金山湾区毁灭性地震的应急计划的故事。

  • 第十七章讲述了一位工程师发现一个他们不认识的服务账户被添加到一个他们以前没有见过的云项目中的故事。

  • 第十八章讨论了在面对大规模网络钓鱼攻击等事件时,驱逐攻击者、减轻其即时行动以及进行长期变革之间的权衡。

第十六章:灾难规划

原文:16. Disaster Planning

译者:飞龙

协议:CC BY-NC-SA 4.0

由 Michael Robinson 和 Sean Noonan‎

与 Alex Bramley 和 Kavita Guliani 一起

复杂系统可能以简单和复杂的方式失败,从意外的服务中断到恶意行为者的攻击,以获取未经授权的访问。您可以通过可靠性工程和安全最佳实践预见和防止其中一些故障,但从长远来看,故障几乎是不可避免的。

与仅仅希望系统能够在灾难或攻击中幸存下来,或者您的员工能够做出合理的响应不同,灾难规划确保您不断努力提高从灾难中恢复的能力。好消息是,制定全面战略的第一步是务实和可行的。

定义“灾难”

很少有人在灾难全面爆发时才意识到灾难。与其偶然发现一栋被大火吞没的建筑物,您更有可能首先看到或闻到烟雾——一些看似微小的迹象,并不一定看起来像灾难。火灾逐渐蔓延,直到您深陷其中,您才意识到情况的极端性。同样,有时候一个小事件——比如第二章中提到的会计错误——可能会引发全面的事件响应。

灾难有各种形式:

  • 自然灾害,包括地震、龙卷风、洪水和火灾。这些往往是显而易见的,对系统的影响程度也不同。

  • 基础设施灾难,如组件故障或错误配置。这些并不总是容易诊断的,它们的影响可以从小到大。

  • 服务或产品中断,这些中断可以被客户或其他利益相关者观察到。

  • 服务的退化接近阈值。有时很难识别。

  • 外部攻击者可能在被发现之前长时间获得未经授权的访问。

  • 敏感数据的未经授权披露

  • 紧急安全漏洞,需要您立即应用补丁来纠正新的、关键的漏洞。这些事件被视为即将发生的安全攻击(见“妥协与漏洞”)。

在本章和下一章中,我们将灾难和危机这两个术语互换使用,意思是任何可能需要宣布事件并采取响应的情况。

动态灾难响应策略

潜在灾难的范围很广,但设计灵活的灾难响应计划将使您能够适应迅速变化的情况。提前考虑可能遇到的情景,已经是为其做准备的第一步。与任何技能一样,您可以通过规划、实践和程序的迭代来磨练灾难响应技能,直到它们变得自然而然。

事件响应不像骑自行车。没有经常性的练习,响应者很难保持良好的肌肉记忆。缺乏练习可能导致断裂的响应和更长的恢复时间,因此经常排练和完善您的灾难响应计划是一个好主意。熟练的事件管理技能让专业人员在事件响应期间自然而然地发挥作用——如果这些技能是第二天性的,专家们就不会为了遵循过程本身而挣扎。

将响应计划分为立即响应、短期恢复、长期恢复和恢复运营等阶段是有帮助的。图 16-1 显示了与灾难恢复相关的一般阶段。

灾难恢复响应工作的阶段

图 16-1:灾难恢复响应工作的阶段

短期恢复阶段应包括为事件制定退出标准,即宣布事件响应完成的标准。成功的恢复可能意味着将服务恢复到完全运行状态,但基础解决方案可能具有提供相同服务水平的新设计。标准还可能要求完全消除风险分析中确定的安全威胁。

在制定应对即时响应、短期和长期恢复以及恢复运营的策略时,您的组织可以通过以下方式做好准备:

  • 进行可能影响组织或具有重大影响的潜在灾难的分析

  • 建立一个响应团队

  • 创建响应计划和详细的操作手册

  • 适当配置系统

  • 测试您的程序和系统

  • 吸收测试和评估的反馈意见

灾难风险分析

进行灾难风险分析是确定组织最关键运营的第一步,这些运营的缺失将导致完全中断。关键运营功能不仅包括重要的核心系统,还包括它们的基础依赖,如网络和应用层组件。灾难风险分析应该确定以下内容:

  • 如果受损或离线,可能会使运营陷入瘫痪的系统。您可以将系统分类为任务关键、任务重要或非关键。

  • 在应对事件时需要的技术或人力资源。

  • 每个系统可能发生的灾难情景。这些情景可以按发生的可能性、频率和对运营的影响(低、中、高或关键)进行分组。

尽管您可能能够直观地对运营进行评估,但更正式的风险评估方法可以避免群体思维,并突出那些并不明显的风险。为了进行彻底的分析,我们建议使用标准矩阵对组织面临的风险进行排名,该矩阵考虑了每种风险发生的概率以及对组织的影响。附录 A 提供了一个样本风险评估矩阵,大型和小型组织都可以根据其系统的具体情况进行调整。

风险评级为您提供了一个关于首要关注的地方的良好经验法则。在对其进行排名后,您应该审查潜在的异常风险清单,例如,一个不太可能发生的事件可能因其潜在影响而被评为关键。您可能还希望征求专家的意见,以审查评估结果,以确定具有隐藏因素或依赖性的风险。

您的风险评估可能会因组织资产的位置而有所不同。例如,日本或台湾的站点应考虑台风,而美国东南部的站点应考虑飓风。随着组织成熟并将容错系统(如冗余互联网电路和备用电源)纳入其系统中,风险评级也可能会发生变化。大型组织应在全球和每个站点级别上进行风险评估,并随着运营环境的变化定期审查和更新这些评估。通过风险评估确定需要保护的系统,您可以准备好创建一个配备工具、程序和培训的响应团队。

建立应急响应团队

有各种方法可以配置应急响应(IR)团队的人员。组织通常以以下几种方式配置这些团队:

  • 创建一个专门的全职 IR 团队

  • 通过将 IR 职责分配给个人,除了他们现有的工作职能。

  • 通过将 IR 活动外包给第三方。

由于预算和规模的限制,许多组织依赖现有员工兼职,一些员工除了执行常规工作职责外,还要在需要时进行事件响应。对于需要更复杂的组织,可能会发现用内部员工配置一个专门的 IR 团队是值得的,以确保响应者始终可用于响应,接受适当的培训,具有必要的系统访问权限,并有足够的时间来应对各种事件。

无论您实现哪种人员配置模型,都可以使用以下技术来创建成功的团队。

确定团队成员和角色

在准备响应计划时,您需要确定将响应事件的核心团队,并明确确定他们的角色。虽然小型组织可能会有来自各个团队的个人贡献者,甚至是一个单一团队来应对每一个事件,但资源更丰富的组织可能会选择为每个功能领域设立一个专门的团队,例如,一个安全响应团队,一个隐私团队,以及一个专注于公共面向站点可靠性的运营团队。

您还可以外包一些功能,同时保留其他功能内部。例如,您可能没有足够的资金和工作量来完全配备内部取证团队,因此您可能会外包该专业知识,同时保留内部的事件响应团队。外包的一个潜在缺点是外部响应者可能无法立即提供帮助。在确定哪些功能保留在内部以及在紧急情况下外包和调用哪些资源时,考虑响应时间是很重要的。

您可能需要以下一些或全部角色来进行事件响应:

事件指挥官

领导对个别事件的响应的个人。

SREs

可以重新配置受影响的系统或实现代码以修复错误的人。

公共关系

可以回应公众查询或发布媒体声明的人。这些人经常与传播负责人合作制定信息。

客户支持

可以回应客户查询或主动联系受影响的客户的人。

法律

可以就法律事务提供咨询的律师,例如适用的法律、法规或合同。

隐私工程师

可以解决技术隐私问题的影响的人。

取证专家

可以进行事件重建和归因以确定发生了什么以及如何发生的人。

安全工程师

可以审查事件的安全影响并与 SRE 或隐私工程师合作保护系统的人。

在确定哪些角色将由内部人员担任时,您可能需要实现轮换人员配置模型,其中 IR 团队轮班运作。在事件期间配置轮班人员至关重要,以减轻疲劳并在事件期间提供持续支持。您还可以采用这种模式以提供灵活性,因为主要工作职责随着时间的推移而发展和变化。请记住,这些是角色,而不是个人。一个人在事件期间可能担任多个角色。

确定哪些角色应该由内部人员担任后,您可以创建一个最初的人员名单来组成个别 IR 团队。提前确定这些人有助于澄清角色、责任和所有权,并最大程度地减少混乱和设置时间。确定 IR 团队的冠军也是有帮助的——一个有足够资历承诺资源并消除障碍的人。冠军可以帮助组建团队,并在存在竞争优先级时与高级领导合作。查看第二十一章获取更多信息。

建立团队宪章

IR 团队的宪章应从团队的使命开始,即一句话描述他们将处理的事件类型。使命让读者能够快速了解团队的工作内容。

宪章的范围应描述您所在的工作环境,重点放在技术、最终用户、产品和利益相关者上。这一部分应清楚地定义团队将处理的事件类型,应由内部人员处理的事件,以及应分配给外包团队处理的事件。

为了确保 IR 团队专注于合格事件,组织领导和 IR 冠军就范围达成一致意见非常重要。例如,虽然 IR 团队当然可以回应关于系统防火墙配置和日志启用/验证的个别客户查询,但这些任务可能更适合客户支持团队。

最后,定义团队的成功看起来是非常重要的。换句话说,当 IR 团队的工作完成或可以宣布完成时,您如何知道呢?

建立严重性和优先级模型

严重性和优先级模型可以帮助 IR 团队量化和理解事件的严重性以及响应所需的操作节奏。您应同时使用这两种模型,因为它们是相关的。

严重性模型允许团队根据事件对组织的影响严重程度对事件进行分类。您可以使用五点(0-4)评分来对事件进行排名,其中 0 表示最严重的事件,4 表示最不严重的事件。您应该采用最适合您组织文化的评分标准(颜色、动物等)。例如,如果您使用五点评分标准,网络上的未经授权个人可能被归类为严重性 0 事件,而安全日志的临时不可用可能是严重性 2 事件。在建立模型时,应审查先前执行的风险分析,以便为事件分配适当的严重性评级。这样做将确保不是所有事件都接收到关键或中等严重性评级。准确的评级将帮助事件指挥官在同时报告多个事件时进行优先排序。

优先级模型定义了人员需要多快回应事件。这个模型建立在您对事件严重性的理解之上,也可以使用五点(0-4)评分,其中 0 表示高优先级,4 表示低优先级。优先级决定了所需工作的节奏:评级为 0 的事件需要立即响应,团队成员在处理其他工作之前要先回应这个事件。评级为 4 的事件可以与日常运营工作一起处理。达成一致的优先级模型还有助于保持各个团队和运营负责人的同步。想象一下,一个团队将事件视为优先级 0,而对总体情况了解有限的第二个团队将其视为优先级 2。这两个团队可能会以不同的节奏运作,延迟了适当的事件响应。

通常情况下,一旦了解了事件的严重性,它在事件生命周期内将保持不变。另一方面,优先级可能在事件过程中发生变化。在对事件进行分类和实现关键修复的早期阶段,优先级可能是 0。在关键修复完成后,您可以将优先级降低到 1 或 2,因为工程团队进行清理工作。

为 IR 团队参与设定操作参数

在建立了严重性和优先级模型之后,您可以定义操作参数,描述事件响应团队的日常运作。当团队除了事件响应工作外还执行定期运营工作,或者需要与虚拟团队或外包团队进行沟通时,这变得越来越重要。操作参数确保严重性 0 和优先级 0 的事件得到及时响应。

操作参数可能包括以下内容:

  • 对报告的事件最初做出响应的预期时间——例如,在 5 分钟内,30 分钟内,一个小时内,或者在下一个工作日

  • 执行初始分类评估并制定响应计划和运营时间表的预期时间

  • 服务水平目标(SLOs),以便团队成员了解何时应中断日常工作进行事件响应。

有许多方法可以组织值班轮换,以确保事件响应工作在团队中得到适当的负载平衡,或者根据定期安排的持续工作进行平衡。有关详细讨论,请参阅 SRE 书的第十一章和第十四章,SRE 工作手册的第 8 章和第 9 章,以及 Limoncelli、Chalup 和 Hogan(2014)的第十四章。¹

制定响应计划

在严重事件期间做出决策可能具有挑战性,因为响应者试图在有限的信息下迅速工作。精心制定的响应计划可以指导响应者,减少浪费的步骤,并为如何应对不同类别的事件提供一个总体方法。虽然一个组织可能有一项全公司范围的事件响应政策,但 IR 团队需要制定一套涵盖以下主题的响应计划:

事件报告

如何向 IR 团队报告事件。

分类

将响应初始报告并开始分类事件的 IR 团队成员名单。

服务水平目标

关于响应者将采取行动的速度目标的参考。

角色和责任

明确定义 IR 团队参与者的角色和责任。

外联

如何联系可能需要协助事件响应的工程团队和参与者。

沟通

在事件期间进行有效的沟通不是没有提前规划的。您需要建立如何执行以下每一项任务:

  • 通知领导有关事件的信息(例如,通过电子邮件、短信或电话),以及这些沟通中应包括的信息。

  • 在事件期间进行组织内部沟通,包括响应团队内部和之间的沟通(建立聊天室、视频会议、电子邮件、安全 IRC 频道、错误跟踪工具等)。

  • 在必要时与监管机构或执法部门等外部利益相关者进行沟通。您需要与您组织的法律部门和其他部门合作规划和支持这种沟通。考虑为每个外部利益相关者保留联系方式和沟通方法的索引。如果您的 IR 团队足够庞大,您可能需要适当地自动化这些通知机制。

  • 向与客户互动的支持团队通报事件。

  • 在通信系统不可用时,或者怀疑通信系统受到损害时,如何与响应者和领导进行沟通,而不让对手察觉。

每个响应计划都应概述高级程序,以便受过培训的响应者可以行动。计划应包含对足够详细的 playbook 的引用,受过培训的响应者可以使用它们来执行特定的操作,并且还可能是一个很好的主意概述对特定类别的事件做出响应的总体方法。例如,在处理网络连接问题时,响应计划应包含对要分析的区域和要执行的故障排除步骤的广义概述,并且应引用一个包含如何访问适当网络设备的具体指令的 playbook,例如登录路由器或防火墙。响应计划还可能概述事件响应者确定何时通知高级领导事件以及何时与本地工程团队合作的标准。

创建详细的 playbook

playbook 是响应计划的补充,列出了响应者应如何从头到尾执行特定任务的具体指令。例如,playbook 可能描述如何向响应者授予对某些系统的紧急临时管理访问权限,如何输出和解析特定日志进行分析,或者如何故障转移系统以及何时实现优雅降级。playbook 具有程序性质,应经常修订和更新。它们通常是团队特定的,这意味着对任何灾难的响应可能涉及多个团队通过其自己的特定程序 playbook 进行工作。

确保访问和更新机制已经就位

您的团队应该定义一个存储文档的地方,以便在灾难期间可以使用材料。灾难可能会影响对文档的访问,例如,如果公司服务器离线,所以确保您在紧急情况下可以访问的位置上有副本,并且这些副本保持最新。

系统会进行补丁、更新和重新配置,威胁态势也会发生变化。您可能会发现新的漏洞,并且新的利用程序会出现。定期审查和更新您的响应计划,以确保它们准确并反映任何最近的配置或操作变化。

良好的事件管理需要频繁和健壮的信息管理。团队应该确定一个适合跟踪事件信息和保留事件数据的系统。处理安全和隐私事件的团队可能希望建立一个严格控制访问的系统,而处理服务或产品中断的响应团队可能希望创建一个在公司范围内广泛可访问的系统。

在事件发生之前预置系统和人员

在进行风险分析、创建 IR 团队和记录适当的程序之后,您需要确定在灾难发生之前可以执行的预置活动。确保您考虑了与事件响应生命周期的每个阶段相关的预置活动。通常,预置活动涉及配置具有定义的日志保留、自动响应和明确定义的人工程序的系统。通过了解这些元素,响应团队可以消除数据来源、自动响应和人工响应之间的覆盖间隙。响应计划和 playbook(在前一节中讨论)描述了人类互动所需的大部分内容,但响应者还需要访问适当的工具和基础设施。

为了促进对事件的快速响应,IR 团队应预先确定事件响应的适当访问级别,并提前建立升级程序,以便获取紧急访问的过程不会缓慢和复杂。IR 团队应具有日志分析和事件重建的读取访问权限,以及用于分析数据、发送报告和进行法证检查的工具访问权限。

配置系统

在灾难或事件发生之前,您可以对系统进行多项调整,以减少 IR 团队的初始响应时间。例如:

  • 在本地系统中构建容错性并创建故障转移。有关此主题的更多信息,请参见第八章和第九章。

  • 部署取证代理,例如GRR代理或EnCase远程代理,跨网络启用日志。这将有助于您的响应和后续的取证分析。请注意,安全日志可能需要长时间的保留期,如第十五章中所讨论的(检测入侵的行业平均时间约为 200 天,而在检测到事件之前删除的日志无法用于调查)。然而,一些国家,如欧盟国家,对日志的保留期有特定要求。在制定保留计划时,请咨询您组织的律师。

  • 如果您的组织将备份提交到磁带或其他媒体,请保留一套与用于创建备份的硬件和软件相同的备份,以便在主要备份系统不可用时及时恢复备份。您还应定期进行恢复演练,以确保您的设备、软件和程序正常工作。IR 团队应确定他们将用于与各个域团队(例如电子邮件团队或网络备份团队)合作的程序,以在事件期间测试、验证和执行数据恢复。

  • 在紧急情况下,为访问和恢复设置多个备用路径。影响生产网络的中断可能很难恢复,除非您有安全的替代路径来访问网络控制平面。同样,如果发现有人侵入并且不确定公司工作站受到的侵害有多广泛,如果您有一组已知安全的空气隔离系统,您仍然可以信任,那么恢复将更容易。

培训

IR 团队成员应接受严重性/优先级模型、IR 团队的运行模式、响应时间和响应计划和手册的位置的培训。您可以在《SRE 工作手册》第九章中阅读更多关于谷歌对事件响应的方法。

然而,事件响应的培训要求不仅限于 IR 团队。在紧急情况下,一些工程师可能会在不考虑或意识到其行为后果的情况下做出响应。为了减轻这种风险,我们建议对将协助 IR 团队的工程师进行培训,使其了解各种 IR 角色及其责任。我们使用了谷歌的事件管理系统(IMAG),它基于事件指挥系统。IMAG 框架分配了关键角色,如事件指挥官、运营负责人和通信负责人。

培训您的员工识别、报告和升级事件。工程师、客户/用户、自动警报或管理员可能会发现事件。每个报告事件的一方应有单独明确的渠道,公司员工应接受如何何时将事件升级给 IR 团队的培训。这种培训应支持组织的 IR 政策。

工程师在升级之前应有一个有限的时间限制来处理事件。第一响应者可用的时间取决于组织准备接受的风险水平。您可以从 15 分钟的时间窗口开始,并根据需要调整该时间窗口。

您应该在紧急情况发生之前建立决策标准,以确保响应者选择最合乎逻辑的行动方案,而不是在临时决定时做出直觉决定。第一响应者经常面临需要立即决定是否将受损系统脱机或使用何种封锁方法的情况。有关此主题的更多讨论,请参见第十七章。

您应该培训工程师了解,事故响应可能需要解决看似相互矛盾的竞争性优先事项,例如,需要保持最大的正常运行时间和可用性,同时还要保留法医调查的证据。您还应该培训工程师记录其响应活动的笔记,以便他们以后可以区分这些活动和攻击者留下的痕迹。

流程和程序

通过在事故发生之前建立一套流程和程序,您可以大大减少响应时间和响应者的认知负荷。例如,我们建议以下操作:

  • 为硬件和软件定义快速采购方法。在紧急情况下,您可能需要额外的设备或资源,例如服务器、软件或发电机燃料。

  • 建立外包服务的合同批准流程。对于较小的组织来说,这可能意味着识别外包能力,如法医调查服务。

  • 制定政策和程序,在安全事件期间保留证据和日志,防止日志覆盖。有关更多详细信息,请参见第十五章。

深入研究:测试系统和响应计划

一旦您已经创建了组织为应对事件所需的所有材料,如前文所述,评估这些材料的有效性并改进您发现的任何不足是至关重要的。我们建议从多个角度进行测试:

  • 评估自动化系统以确保其正常运行。

  • 测试流程,消除响应者和工程团队使用的程序和工具中的任何漏洞。

  • 培训将在事件期间做出响应的人员,以确保他们具有应对危机的必要技能。

您应该定期进行这些测试,至少每年一次,以确保您的系统、程序和响应在实际紧急情况下是可靠和适用的。

每个组件在将受灾系统恢复到运行状态中发挥着至关重要的作用。即使您的 IR 团队技能非常高,如果没有程序或自动化系统,其应对灾难的能力将是不一致的。如果您的技术程序已记录但不可访问或不可用,它们可能永远不会被实现。测试灾难响应计划的每一层的弹性可以降低这些风险。

对于许多系统,您需要记录减轻威胁的技术程序,定期审计控制(例如,每季度或每年一次)以确保它们仍在实现,并向工程师提供修复列表,以纠正您发现的任何弱点。刚开始进行 IR 规划的组织可能希望调查有关灾难恢复和业务连续性规划的认证,以获得灵感。

审计自动化系统

您应该审计所有关键系统和依赖系统,包括备份系统、日志系统、软件更新程序、警报生成器和通信系统,以确保它们正常运行。完整的审计应确保以下内容:

备份系统正常运行。

备份应该正确创建,存储在安全位置,存储适当的时间,并以正确的权限存储。定期进行数据恢复和验证练习,以确保您可以从备份中检索和使用数据。有关 Google 数据完整性方法的更多信息,请参阅SRE 书籍第 26 章

事件日志(在上一章中讨论)被正确存储。

这些日志允许响应者在法证调查期间重建事件时构建准确的时间线。您应该根据组织的风险水平和其他适用的考虑因素存储事件日志的时间段。

关键漏洞应及时修补。

审核自动和手动的补丁流程,以减少人为干预的需求和人为错误的可能性。

警报正确生成。

系统生成警报 - 电子邮件警报,仪表板更新,短信等 - 当满足特定标准时。验证每个警报规则,以确保它正确触发。还要确保考虑依赖关系。例如,如果在网络中断期间 SMTP 服务器离线,您的警报会受到什么影响?

通信工具,如聊天客户端,电子邮件,电话会议桥接服务和安全 IRC,按预期工作。

运作良好的通信渠道对于响应团队至关重要。您还应该审核这些工具的故障转移能力,并确保它们保留您需要编写事后分析的消息。

进行非侵入式桌面练习

桌面练习是测试记录程序和评估响应团队表现的非常有价值的工具。这些练习可以作为评估端到端事件响应的起点,并且在实际测试不可行时也很有用 - 例如,引发实际地震。模拟可以从小到大,范围广泛,并且通常是非侵入式的:因为它们不会使系统脱机,所以不会干扰生产环境。

类似于SRE 书籍第十五章中描述的 Wheel of Misfortune 练习,您可以通过呈现参与者各种后续情节变化的事件情景来进行桌面练习。要求参与者描述他们将如何应对情景,以及他们将遵循哪些程序和协议。这种方法可以让参与者发挥他们的决策能力,并获得建设性的反馈。这些练习的开放结构意味着它们可以吸纳各种参与者,包括以下人员:

  • 一线工程师,按照详细的手册恢复瘫痪系统的服务

  • 高级领导层,就运营方面的业务决策进行决策

  • 公共关系专业人士,协调外部沟通

  • 律师,提供上下文法律指导,并帮助制定公共沟通

这些桌面练习最重要的一点是挑战响应者,并为所有参与者提供在真正事件发生之前练习相关程序和决策过程的机会。

在实现桌面练习时,以下是一些要考虑的关键特点:

可信度

桌面场景应该是可信的 - 一个引人入胜的情景可以激励参与者跟随而不需要悬置怀疑。例如,一个练习可能假设用户受到网络钓鱼攻击,使对手能够利用用户工作站上的漏洞。您可以基于现实攻击和已知的漏洞和弱点来确定攻击者在网络中移动的关键点。

细节

制定桌面情景的人应该提前研究该情景,引导者应该熟悉事件的细节和对情景的典型响应。为了增加可信度,桌面的创建者可以创建参与者在真实事件中会遇到的文物,如日志文件、客户或用户的报告和警报。

决策点

就像“选择你自己的冒险”故事一样,桌面练习应该有决策点,帮助情节展开。典型的 60 分钟桌面练习包含大约 10-20 个情节决策点,让参与者参与决策,影响练习的结果。例如,如果桌面练习的参与者决定将受损的电子邮件服务器下线,那么在剩下的情景中参与者就不能发送电子邮件通知。

参与者和引导者

尽量使桌面练习尽可能互动。随着练习的展开,引导者可能需要对响应者执行的行动和命令做出回应。与其仅仅讨论他们如何应对事件,参与者应该展示他们将如何应对。例如,如果 IR 手册要求事件响应者将勒索软件攻击升级到取证团队的成员进行调查,同时也要升级到网络安全团队的成员来阻止对敌对网站的流量,那么响应者应该在桌面练习中执行这些程序。“执行响应”有助于事件响应者建立肌肉记忆。引导者应提前熟悉情景,以便在需要时即兴并引导响应者朝正确的方向发展。再次强调,这里的目标是让参与者积极参与情景。

结果

一个成功的桌面练习不应该让参与者感到挫败,而应该以对工作得好和工作不太好的可行反馈结束。参与者和引导者应该能够就事件响应团队的改进领域提出具体建议。在适当的情况下,参与者应该建议对系统和政策进行改变,以解决他们发现的固有弱点。为了确保参与者解决这些建议,创建具体负责人的行动项。

在生产环境中测试响应

虽然桌面练习对模拟各种事件情景很有用,但你需要在真实的生产环境中测试一些事件情景、攻击向量和漏洞。这些测试在安全和可靠性的交汇处运作,通过让 IR 团队了解运营约束、在真实参数下进行实践,并观察他们的响应如何影响生产环境和正常运行时间。

单系统测试/故障注入

与其测试整个系统的端到端,你可以将大型系统分解为单独的软件和/或硬件组件进行测试。测试可以采用各种形式,可以涉及单个本地组件或具有组织范围的单个组件。例如,当一个恶意内部人连接 USB 存储设备到工作站并尝试下载敏感内容时会发生什么?本地日志是否跟踪本地 USB 端口活动?日志是否足够聚合并及时升级,使安全团队能够快速响应?

我们特别建议您通过故障注入进行单系统测试。将故障注入到您的系统中可以让您运行有针对性的测试,而不会干扰整个系统。更重要的是,故障注入框架允许各个团队在不涉及其依赖关系的情况下测试其系统。例如,考虑常用于负载平衡的开源 Envoy HTTP 代理。除了其许多负载平衡功能外,该代理支持故障注入 HTTP 过滤器,您可以使用它来为一部分流量返回任意错误或延迟请求一定时间。使用这种类型的故障注入,您可以测试系统是否正确处理超时,并且超时是否会导致生产中的不可预测行为。

当您在生产环境中发现异常行为时,经过充分练习的故障注入框架可以实现更有结构的调查,您可以以受控的方式重现生产问题。例如,想象以下情景:当公司的用户尝试访问特定资源时,基础设施使用单一来源检查所有认证请求。然后,公司迁移到一个需要对各种来源进行多次认证检查以获取类似信息的服务。结果,客户开始超过这些功能调用的配置超时。认证库的缓存组件内的错误处理错误地将这些超时视为永久性(而不是临时性)故障,触发基础设施中许多其他小故障。通过使用故障注入的已建立的事故响应框架,在一些调用中注入延迟,响应团队可以轻松地重现行为,确认他们的怀疑,并开发修复方案。

人力资源测试

虽然许多测试涉及系统的技术方面,但测试也应考虑人员故障。当特定人员不可用或未能行动时会发生什么?通常,事故响应团队依赖于对组织具有强大机构知识的个人,而不是遵循既定流程。如果关键决策者或经理在响应期间不可用,那么 IR 团队的其他成员将如何继续?

多组件测试

使用分布式系统意味着任何数量的依赖系统或系统组件可能会失败。您需要计划多组件故障并创建相关的事故响应程序。考虑一个依赖于多个组件的系统,每个组件都经过了单独测试。如果两个或更多组件同时失败,那么事故响应的哪些部分必须以不同的方式处理?

思考练习可能不足以测试每个依赖项。在考虑安全环境中的服务中断时,您需要考虑安全问题以及故障场景。例如,当故障切换发生时,系统是否尊重现有的 ACL?有什么保障可以确保这种行为?如果您正在测试授权服务,依赖服务是否会关闭失败?要深入了解这个话题,请参阅第五章。

系统范围的故障/故障切换

除了测试单个组件和依赖项外,还要考虑整个系统失败时会发生什么。例如,许多组织运行主要和次要(或灾难恢复)数据中心。在切换到从次要位置运行之前,您无法确信故障切换策略是否会保护您的业务和安全姿态。谷歌定期对整个数据中心建筑进行电源循环,以测试故障是否会导致用户可见的影响。这种练习确保服务保持在没有特定数据中心位置的情况下运行的能力,并且执行此工作的技术人员在管理断电/通电程序方面经验丰富。

对于在另一个提供商的云基础设施上运行的服务,考虑如果整个可用区域或区域发生故障,您的服务会发生什么情况。

红队测试

除了宣布的测试,谷歌还进行灾难准备演习,称为红队演习:由其信息安全保障组织进行的进攻性测试。类似于 DiRT 演习(参见“DiRT 演习测试紧急访问”),这些演习模拟真实攻击,以测试和改进检测和响应能力,并展示安全问题的业务影响。

红队通常不会提前通知事件响应人员,除了高级领导层。由于红队熟悉谷歌的基础设施,他们的测试比标准网络渗透测试更加有效。由于这些练习是在内部进行的,它们提供了在完全外部攻击(攻击者在谷歌之外)和内部攻击(内部风险)之间取得平衡的机会。此外,红队练习通过测试安全性端到端以及通过攻击(如钓鱼和社会工程)测试人类行为,补充了安全审查。有关红队的更深入探讨,请参阅“特殊团队:蓝队和红队”

评估响应

在应对实际事件和测试场景时,创建有效的反馈循环非常重要,这样您就不会反复遭受相同的情况。实际事件应该需要具体的事后分析和行动项目;您也可以为测试创建事后分析和相应的行动项目。虽然测试既可以是一种有趣的练习,也可以是一种极好的学习经验,但这种实践需要一些严谨性——跟踪测试的执行情况,并对其影响以及您组织中的人员如何应对测试进行批判性评估是非常重要的。进行一项练习而不实现所学到的教训只是娱乐。

在评估组织对事件和测试的响应时,考虑以下最佳实践:

  • 衡量响应。评估者应该能够确定哪些工作得很好,哪些没有。衡量实现响应的每个阶段所需的时间,以便您可以确定纠正措施。

  • 编写无过失的事后分析,并专注于如何改进系统、程序和流程。

  • 创建反馈循环,以改进现有计划或根据需要制定新计划。

  • 收集证据并将其反馀到信号检测中。确保您解决任何发现的差距。

  • 为了进行取证分析并解决差距,请确保保存适当的日志和其他相关材料,特别是在进行安全性练习时。

  • 评估甚至“失败”的测试。什么有效,你需要改进什么?

  • “特殊团队:蓝队和红队”中所讨论的,实现颜色团队以确保您的组织能够应对所学到的教训。您可能需要一个混合的紫队,以确保蓝队及时解决红队利用的漏洞,从而防止攻击者重复利用相同的漏洞。您可以将紫队视为漏洞的回归测试。

谷歌的例子

为了使本章描述的概念和最佳实践更具体,这里有一些真实世界的例子。

具有全球影响的测试

2019 年,谷歌对旧金山湾区发生大地震的响应进行了测试。该场景包括模拟对物理设施、交通基础设施、网络组件、公用事业和电力、通信、业务运营和高管决策的影响。我们的目标是测试谷歌对大规模中断的响应以及对全球运营的影响。具体来说,我们测试了以下内容:

  • 谷歌如何为地震和多次余震中受伤的人提供立即的急救?

  • 谷歌如何向公众提供帮助?

  • 员工如何向谷歌的领导层升级信息?在通信中断的情况下,例如,网络中断或局域网/城域网/广域网中断,谷歌如何向员工传播信息?

  • 如果员工有利益冲突,例如,如果员工需要照顾家人和家庭,谁会提供现场响应?

  • 无法通行的次要道路会对该地区产生什么影响?溢出会对主要道路产生什么影响?

  • 谷歌如何为滞留在谷歌校园的员工、承包商和访客提供帮助?

  • 谷歌如何评估其建筑物的损坏,其中可能包括破裂的管道、污水问题、破碎的玻璃、停电和破裂的网络连接?

  • 如果受影响的团队无法启动权责转移,SRE 和地理区域外的各种工程团队如何接管系统?

  • 如何使受影响地理区域之外的领导层能够继续业务运营和做出与业务相关的决策?

DiRT 演习测试紧急访问

有时我们可以同时测试可靠性和安全运营的稳健性。在我们的年度灾难恢复培训(DiRT)演习中,SRE 测试了紧急访问凭证的程序和功能:当标准 ACL 服务中断时,他们能否获得对公司和生产网络的紧急访问?为了增加安全测试层,DiRT 团队还纳入了信号检测团队。当 SRE 启动紧急访问程序时,检测团队能够确认正确的警报触发,并且访问请求是合法的。

行业范围内的漏洞

2018 年,谷歌提前得知 Linux 内核中的两个漏洞,这些漏洞支撑着我们大部分的生产基础设施。通过发送特制的 IP 片段和 TCP 段,SegmentSmack(CVE-2018-5390)或 FragmentSmack(CVE-2018-5391)可以导致服务器执行昂贵的操作。通过使用大量的 CPU 和挂钟时间,这个漏洞可以允许攻击者获得显著的规模提升,超出了正常的拒绝服务攻击——一个通常可以应对 1 Mpps 攻击的服务在大约 50 Kpps 时会崩溃,韧性降低了 20 倍。

灾难规划和准备使我们能够在两个方面减轻风险:技术方面和事件管理方面。在事件管理方面,潜在的灾难如此重大,以至于谷歌指派了一组事件经理全职处理问题。团队需要确定受影响的系统,包括供应商固件图像,并执行一项全面的计划来减轻风险。

在技术方面,SRE 已经为 Linux 内核实现了深度防御措施。一个运行时补丁,或者ksplice,使用函数重定向表来使重新启动新内核变得不必要,可以解决许多安全问题。Google 还保持内核推出纪律:我们定期向整个机器群推送新内核,目标是少于 30 天,并且我们有明确定义的机制,以增加这一标准操作程序的推出速度,如果有必要的话。⁸

如果我们无法使用 ksplice 修复漏洞,我们可以以较快的速度进行紧急推出。然而,在这种情况下,我们可以使用内核 splice 来解决受影响的两个函数——tcp_collapse_ofo_queuetcp_prune_ofo_queue。SRE 能够在生产系统中应用 ksplice,而不会对生产环境产生不利影响。由于推出程序已经经过测试和批准,SRE 很快获得了副总裁的批准,在代码冻结期间应用补丁。

结论

当考虑如何从零开始启动灾难恢复测试和计划时,可能会感到可能的方法的数量令人不知所措。然而,即使在小规模上,您也可以应用本章的概念和最佳实践。

首先,确定您最重要的系统或关键数据,然后确定如何应对影响它的各种灾难。您需要确定在没有服务的情况下可以运行多长时间,以及影响它的人员或其他系统的数量。

从这一重要的第一步开始,您可以逐步扩大覆盖范围,形成一个强大的灾难准备战略。从最初的确定和预防引发火灾的火花,您可以逐步应对不可避免的大火。

¹ Limoncelli, Thomas A., Strata R. Chalup, and Christina J. Hogan. 2014. 云系统管理实践:设计和操作大型分布式系统。波士顿,马萨诸塞州:Addison-Wesley。

² 这些主题在SRE 书籍第 22 章中有描述。

³ 参见SRE 书籍第十四章SRE 工作手册第九章中的生产特定的实例。

⁴ 参见SRE 书籍第十五章

⁵ 参见SRE 书籍第十三章

⁶ 参见 Kripa Krishnan 的文章“应对意外”和她的 USENIX LISA15 演讲“Google 十年的崩溃”

⁷ 一个打破玻璃机制可以绕过政策,允许工程师快速解决故障。参见“打破玻璃”

⁸ 第九章讨论了准备组织快速应对事故的其他设计方法。

第十七章:危机管理

原文:17. Crisis Management

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Matt Linton

与 Nick Soda 和 Gary O'Connor 一起

事件响应对于可靠性和安全事件都至关重要。《SRE 书》的第十四章和《SRE 工作手册》的第九章探讨了与可靠性故障相关的事件响应。我们使用相同的方法——谷歌的事件管理(IMAG)框架——来应对安全事件。

安全事件是不可避免的。行业中有一句常见的格言是“只有两种公司:知道自己受到了侵害的和不知道自己受到了侵害的。”安全事件的结果取决于你的组织做好了多少准备,以及你的响应有多好。为了实现成熟的安全姿态,你的组织需要建立和实践事件响应(IR)能力,正如前一章所讨论的那样。

除了确保未经授权的人无法访问您的系统和数据保持在应有的位置之外,今天的 IR 团队还面临着新的和困难的挑战。随着安全行业朝着更大的透明度¹和与用户更开放的需求趋势,这一期望对于习惯于在公众关注的聚光灯下运作的任何 IR 团队来说都是一个独特的挑战。此外,像欧盟的《通用数据保护条例(GDPR)》和与注重安全的客户的服务合同不断推动着调查必须开始、进行和完成的速度的边界。如今,客户要求在最初检测到潜在安全问题后的 24 小时(或更短时间内)内通知他们并不罕见。

事件通知已成为安全领域的核心特性,与云计算的易用性和普及性、工作场所“自带设备”(BYOD)政策的广泛采用以及物联网(IoT)等技术进步并驾齐驱。这些进步为 IT 和安全人员带来了新的挑战,例如对组织所有资产的控制和可见性的限制。

这是危机吗?

并非每个事件都是危机。事实上,如果你的组织状况良好,相对较少的事件应该会演变成危机。一旦发生升级,响应者评估升级的第一步是分诊——利用他们掌握的知识和信息对事件的严重性和潜在后果进行合理和明智的假设。

分诊是急诊医疗界的一项成熟技能。一名急救医生(EMT)到达车祸现场时,首先要确保现场没有进一步伤害的立即风险,然后进行分诊。例如,如果一辆公共汽车与一辆小汽车相撞,已经可以逻辑推断出一些信息。小汽车上的人可能受了重伤,因为与重型公共汽车的碰撞可能会造成很大的伤害。公共汽车可以载有许多乘客,所以乘客可能会受伤。通常情况下,两辆车都不会携带危险化学品。在到达的第一分钟内,急救医生知道他们需要叫更多的救护车,可能要警报重症监护单位,并叫消防部门来解救小车上被困的人。他们可能不需要危险物质清理队。

你的安全响应团队应该使用相同的评估方法来对待不断发生的事件。作为第一步,他们必须估计攻击的潜在严重程度。

事件分诊

在分诊时,负责调查的工程师必须收集基本事实,以帮助决定升级是否属于以下情况之一:

  • 一个错误(即误报)

  • 一个容易纠正的问题(可能是一次机会性的妥协)

  • 一个复杂且可能具有破坏性的问题(比如有针对性的妥协)

他们应该能够使用预定的流程对可预测的问题、错误和其他简单问题进行分诊。更大更复杂的问题,比如有针对性的攻击,可能需要组织和管理的响应。

每个团队都应该有预先计划的标准,以帮助确定什么构成了一起事件。理想情况下,他们应该在事件发生之前确定在他们的环境中哪些风险是严重的,哪些是可以接受的。对事件的响应将取决于事件发生的环境类型,组织预防控制的状态以及其响应程序的复杂性。考虑三个组织如何应对相同的威胁——勒索软件攻击:

  • 组织 1拥有成熟的安全流程和分层防御,包括只允许加密签名和批准的软件执行的限制。在这种环境中,众所周知的勒索软件几乎不可能感染机器或在整个网络中传播。如果确实发生,检测系统会发出警报,然后有人进行调查。由于成熟的流程和分层防御,一个工程师可以处理这个问题:他们可以检查是否发生了可疑活动,超出了尝试恶意软件执行,并使用标准流程解决问题。这种情况不需要危机式的事件响应努力。

  • 组织 2的销售部门在云环境中举办客户演示,想要了解组织软件的人安装和管理他们的测试实例。安全团队注意到这些用户倾向于犯安全配置错误,导致系统受到威胁。为了不需要人工干预,安全团队建立了一个机制,自动清除并替换受损的云测试实例。在这种情况下,勒索软件蠕虫也不需要太多的取证或事件响应关注。虽然组织 2 没有阻止勒索软件的执行(如组织 1 的情况),但组织 2 的自动化缓解工具可以控制风险。

  • 组织 3的分层防御较少,对其系统是否受到威胁的可见性有限。该组织面临着勒索软件在其网络中传播的更大风险,可能无法快速做出响应。在这种情况下,如果蠕虫传播,可能会影响大量业务关键系统,并且组织将受到严重影响,需要大量技术资源来重建受损的网络和系统。这种蠕虫对组织 3 构成严重风险。

虽然这三个组织都在应对相同的风险来源(勒索软件攻击),但它们的分层防御和流程成熟度水平的差异影响了攻击的潜在严重性和影响。虽然组织 1 可能只需要启动一个流程驱动的响应,组织 3 可能面临需要协调的事件管理的危机。随着事件对组织构成严重风险的可能性增加,需要组织多个参与者进行有组织的响应的可能性也增加。

您的团队可以进行一些基本评估,以确定升级是否需要标准的流程驱动方法或危机管理方法。问问自己以下问题:

  • 您存储了哪些可能对系统上的某人可访问的数据?这些数据的价值或关键性是什么?

  • 潜在受损系统与其他系统之间存在什么信任关系?

  • 是否有补偿控制,攻击者也必须突破(并且似乎完好无损)才能利用他们的立足点?

  • 攻击是否看起来是商品机会主义恶意软件(例如广告软件),还是看起来更先进或有针对性(例如看起来是为你的组织量身定制的网络钓鱼活动)?

仔细考虑你组织的所有相关因素,并根据这些事实确定组织风险的最高可能水平。

妥协与漏洞

长期以来,IR 团队一直负责应对疑似入侵和妥协事件。但是对于软件和硬件漏洞,也就是安全漏洞呢?你会把系统中新发现的安全漏洞视为尚未被发现的妥协吗?

软件漏洞是不可避免的,你可以为它们制定计划(如第八章中所述)。良好的防御实践在漏洞开始之前消除或限制潜在的负面后果。²如果你计划良好并实现了深度防御和额外的安全层,你就不需要像处理事件那样处理漏洞修复。也就是说,可能需要使用事件响应流程来管理复杂或影响较大的漏洞,这可以帮助你组织并快速响应。

在谷歌,我们通常将具有极端风险的漏洞视为事件。即使漏洞没有被积极利用,尤其严重的漏洞仍然可能引入极端风险。如果你在漏洞被公开披露之前参与修复(这些努力通常被称为协调漏洞披露或 CVDs),操作安全和保密问题可能需要加强响应。另外,如果你在公开披露后匆忙修补系统,保护具有复杂相互依赖关系的系统可能需要紧急努力,部署修复可能会困难且耗时。

一些特别危险的漏洞示例包括 Spectre 和 Meltdown(CVE-2017-5715 和 5753)、glibc(CVE-2015-0235)、Stagefright(CVE-2015-1538)、Shellshock(CVE-2014-6271)和 Heartbleed(CVE-2014-0160)。

掌握你的事件

现在我们已经讨论了分类和风险评估的过程,接下来的三节假设“大事”已经发生:你已经确定或怀疑受到了有针对性的妥协,需要进行全面的事件响应。

第一步:不要惊慌!

许多应对者将严重事件升级与不断上升的恐慌感和肾上腺素飙升联系在一起。消防、救援和医疗领域的紧急应对人员在基础培训中被告知不要在紧急情况下奔跑。奔跑不仅会增加现场事故的可能性,使问题变得更糟,还会给应对者和公众灌输恐慌感。类似地,在安全事件中,匆忙行动所获得的额外几秒钟很快就会被计划失败的后果所掩盖。

尽管谷歌的 SRE 和安全团队执行事件管理的方式类似,但在开始安全事件的危机管理响应和可靠性事件(如故障)之间存在差异。当发生故障时,值班的 SRE 准备采取行动。他们的目标是快速找到错误并修复它们,以恢复系统到良好状态。最重要的是,系统并不知道自己的行为,并且不会抵制被修复。

在潜在的妥协中,攻击者可能会密切关注目标组织采取的行动,并在响应者试图修复问题时对抗他们。在没有进行全面调查之前尝试修复系统可能是灾难性的。因为典型的 SRE 工作不具备这种风险,SRE 通常的响应是先修复系统,然后记录他们从失败中学到的东西。例如,如果工程师提交了一个破坏生产环境的更改列表(CL),SRE 可能会立即采取行动来撤销 CL,使问题消失。问题解决后,SRE 将开始调查发生了什么。而安全响应则要求团队在尝试纠正事情之前完成对发生情况的全面调查。

作为安全事件响应者,您的第一个任务是控制自己的情绪。在升级的前五分钟内,深呼吸,让任何恐慌的感觉过去,提醒自己需要一个计划,并开始考虑下一步。虽然立即做出反应的愿望很强烈,但实际上,事后分析很少报告说,如果员工早五分钟做出反应,安全响应就会更有效。更有可能的是,提前进行一些额外的规划将增加更大的价值。

开始您的响应

在谷歌,一旦工程师确定他们面临的问题是一个事件,我们就会遵循一个标准流程。这个流程称为谷歌的事件管理,在SRE 工作手册的第九章中有详细描述,以及我们应用该协议的故障和事件的案例研究。本节描述了如何使用 IMAG 作为标准框架来管理安全妥协。

首先,快速回顾一下:如前一章所述,IMAG 基于一种称为事件指挥系统(ICS)的正式流程,该流程被全球的消防、救援和警察机构使用。像 ICS 一样,IMAG 是一个灵活的响应框架,足够轻便以管理小事件,但又能够扩展到包括大规模和广泛的问题。我们的 IMAG 计划旨在规范流程,以确保在事件处理的三个关键领域(指挥、控制和通信)取得最大成功。

管理事件的第一步是指挥。在 IMAG 中,我们通过发布声明来做到这一点:“我们的团队宣布涉及X的事件,我是事件指挥官(IC)。”明确地说“这是一个事件”可能看起来简单,甚至可能是不必要的,但为了避免误解,明确是指挥和通信的核心原则。以声明开始您的事件是对齐每个人期望的第一步。事件复杂而不寻常,涉及高度紧张,并且发生速度很快。参与其中的人需要专注。高管应该被通知,团队可能会忽略或绕过正常流程,直到事件得到控制。

在响应者接管并成为 IC 之后,他们的工作是保持对事件的控制。IC 指挥响应,并确保人们始终朝着特定目标前进,以便危机中的混乱和不确定性不会使团队偏离轨道。为了保持控制,IC 和他们的领导必须与所有参与者保持卓越的通信

谷歌使用 IMAG 作为各种事件的通用响应框架。所有值班工程师(理想情况下)都接受相同基本知识的培训,并学习如何使用它们来扩展和专业地管理响应。虽然 SRE 和安全团队的重点可能不同,但最终,拥有相同的响应框架使这两个团队能够在压力下无缝合作,当与陌生团队合作可能最困难时。

建立您的事件团队

根据 IMAG 模型,一旦宣布发生事故,宣布事故的人要么成为事故指挥官,要么从其他可用的工作人员中选择一个 IC。无论选择哪种路线,都要明确指定这项任务,以避免响应者之间的误解。被指定为 IC 的人也必须明确承认他们接受了这项任务。

接下来,IC 将评估需要立即采取的行动,以及谁可以担任这些角色。您可能需要一些熟练的工程师来启动调查。大型组织可能会有一个专门的安全团队;非常大型的组织甚至可能会有一个专门的事件响应团队。小型组织可能会有一个专门的安全人员,或者有人在其他运营责任之外兼职处理安全事务。

无论组织的规模或构成如何,IC 都应该找到熟悉潜在受影响系统的员工,并将这些人员委派为事件响应团队。如果事件规模扩大并需要更多人员,最好指定一些领导人负责调查的某些方面。几乎每个事件都需要一个运营负责人(OL):战术上的对应和 IC 的合作伙伴。虽然 IC 专注于制定实现事件响应进展所需的战略目标,OL 专注于实现这些目标并确定如何实现。大多数执行调查、修复和打补丁系统等技术人员应该向 OL 汇报。

您可能需要填补的其他主要角色包括以下内容:

管理联络人

您可能需要有人立即做出艰难的决定。您的组织中谁可以决定必要时关闭一个产生收入的服务?谁可以决定让员工回家或撤销其他工程师的凭证?

法律负责人

您的事件可能会引发您需要帮助回答的法律问题。您的员工是否有增强的隐私期望?例如,如果您认为有人通过其网络浏览器下载了恶意软件,您是否需要额外的权限来检查他们的浏览器历史记录?如果您认为他们通过个人浏览器配置文件下载了恶意软件怎么办?

沟通负责人

根据事件的性质,您可能需要与客户、监管机构等进行沟通。擅长沟通的专业人员可能是您响应团队的重要补充。

深入探讨:运营安全

在危机管理的背景下,运营安全(OpSec)指的是保持响应活动秘密的做法。无论您是在怀疑妥协、内部滥用调查还是危险漏洞的工作,如果公开存在可能导致广泛利用的危险漏洞,您可能会有需要保密的信息,至少在有限的时间内。我们强烈建议您今天就建立一个 OpSec 计划——在您遇到事件之前——这样您就不必在最后一分钟匆忙制定计划。一旦秘密泄露,就很难重新获得。

IC 最终负责确保保密规则得到制定、传达和遵守。要求参与调查的每个团队成员都应该接受简要介绍。您可能有特定的数据处理规则,或者对使用哪些通信渠道有期望。例如,如果您怀疑您的电子邮件服务器在违规范围内,您可能会禁止员工之间就违规进行电子邮件交流,以防攻击者能够看到这些对话。

作为最佳实践,我们建议您的 IC 为每个事件响应团队成员制定具体的指导。每个团队成员在开始处理事件之前应该审查并确认这些指导。如果您没有清楚地向所有相关方传达保密规则,您就有可能发生信息泄漏或过早披露。

除了保护响应活动免受攻击者的影响外,一个良好的操作安全性计划还解决了如何在不进一步暴露组织的情况下进行响应。考虑一下,一个攻击者入侵了您公司员工的帐户,并试图从服务器的内存中窃取其他密码。如果在调查期间系统管理员使用管理凭据登录受影响的机器,攻击者会很高兴。提前计划如何在不给攻击者提供额外杠杆的情况下访问数据和机器是很重要的。一种方法是提前部署远程取证代理到所有系统。这些软件包可以为授权的响应者提供一条访问路径,让他们可以在不通过登录系统的方式冒风险获取取证物证。

向攻击者透露您已发现他们的攻击的后果可能很严重。一个决心要在您的调查之外持续存在的攻击者可能会保持安静。这会使您失去对他们妥协程度的宝贵洞察,并可能导致您错过他们的一个(或多个)立足点。而已经完成目标并且不想保持安静的攻击者可能会在发现您的行动后尽可能地摧毁您的组织!

以下是一些常见的操作安全性错误:

  • 在可能让攻击者监视响应活动的媒介(如电子邮件)中传达或记录事件。

  • 登录被入侵的服务器。这会向攻击者暴露潜在有用的身份验证凭据。

  • 连接到并与攻击者的“命令和控制”服务器进行交互。例如,不要尝试通过从正在进行调查的机器下载来访问攻击者的恶意软件。您的行为将在攻击者的日志中显得异常,并可能警告他们您的调查。此外,不要对攻击者的机器进行端口扫描或域查找(这是新手响应者常犯的错误)。

  • 在调查完成之前锁定受影响用户的帐户或更改密码。

  • 在了解攻击的全部范围之前将系统下线。

  • 允许攻击者可能窃取的相同凭据访问您的分析工作站。

在您的操作安全性响应中考虑以下良好的做法:

  • 尽可能进行面对面的会议和讨论。如果需要使用聊天或电子邮件,使用新的机器和基础设施。例如,面临未知程度的妥协的组织可能会建立一个新的临时基于云的环境,并部署与其常规设备不同的机器(例如,Chromebook)供响应者进行通信。理想情况下,这种策略提供了一个干净的环境,可以在攻击者的视线之外进行聊天、记录和交流。

  • 在可能的情况下,确保您的机器配置了远程代理或基于密钥的访问方法。这样可以在不泄露登录凭据的情况下收集证据。

  • 在请求他人帮助时,要明确和明确地说明保密性——除非您告诉他们,否则他们可能不知道特定信息应该保密。

  • 在调查的每个步骤中,考虑一个精明的攻击者可能从您的行动中得出的结论。例如,入侵 Windows 服务器的攻击者可能会注意到一波突然的组策略收紧,并得出他们已经被发现的结论。

为了更大的利益而交换良好的操作安全性

在保持您的事件响应保密的一般建议中有一个明显的例外:如果您面临即将到来且明确可识别的风险。如果您怀疑对一个如此关键以至于重要数据、系统甚至生命可能处于危险之中的系统进行了妥协,可能会有正当理由采取极端措施。在处理作为事件的漏洞或错误的情况下,有时该漏洞可能如此容易被利用并且如此广为人知(例如,Shellshock^(3)),以至于关闭或完全禁用系统可能是保护它的最佳方式。这样做当然会让您的攻击者和其他人明显地意识到出现了问题。

这些重大决定和组织权衡不太可能仅由事件指挥官做出,除非在极端危险的情况下(例如,关闭电网控制系统以防止灾难)。通常,组织内的高管做出这样的决定。然而,当这些决定被辩论时,事件指挥官是房间里的专家,他们的建议和安全专业知识至关重要。在一天结束时,组织在危机中的许多决策并不是关于做出正确的决定;而是在一系列次优选择中做出最佳可能的选择。

深入探讨:调查过程

调查安全妥协涉及尝试通过攻击的每个阶段来追溯攻击者的步骤。理想情况下,您的 IR 团队(由分配到该工作的任何工程师组成)将尝试在多个任务之间保持紧密的努力循环。⁴ 这一努力侧重于确定组织的所有受影响部分,并尽可能多地了解发生了什么事情。

数字取证是指确定攻击者可能在设备上采取的所有行动的过程。取证分析师(进行取证的工程师,最好是经过专门培训和经验的人)分析系统的所有部分,包括可用的日志,以确定发生了什么事情。分析师可能会执行以下一些或所有的调查步骤:

取证成像

制作与受损系统连接的任何数据存储设备的安全只读副本(和校验和)。这样可以在副本被拍摄时将数据保留在已知状态,而不会意外覆盖或损坏原始磁盘。法庭诉讼通常需要原始磁盘的取证图像作为证据。

内存成像

制作系统内存的副本(或在某些情况下,运行二进制文件的内存)。内存可能包含许多数字证据,这些证据在调查过程中可能会有用,例如进程树、正在运行的可执行文件,甚至攻击者可能已加密的文件密码。

文件刻录

提取磁盘内容,以查看是否可以恢复某些文件类型,特别是可能已被删除的文件,例如攻击者试图删除的日志。一些操作系统在删除文件时不会将文件内容清零。相反,它们只会取消文件名的链接,并将磁盘区域标记为以后重用。因此,您可能能够恢复攻击者试图删除的数据。

日志分析

调查与日志中出现的与系统相关的事件,无论是在系统本身还是来自其他来源。网络日志可能显示谁何时与系统交谈;来自其他服务器和台式机的日志可能显示其他活动。

恶意软件分析

对攻击者使用的工具进行分析,以确定这些工具的功能、工作原理以及这些工具可能与哪些系统进行通信。此分析的数据通常反馈给进行取证和检测工作的团队,以提供有关系统可能已被入侵的潜在迹象的更好见解。

在数字取证中,事件之间的关系与事件本身一样重要。

取证分析师所做的大部分工作都是为了获取证据,以建立取证时间线的目标。⁵通过收集按时间顺序排列的事件列表,分析师可以确定攻击者活动的相关性和因果关系,证明这些事件发生的原因

分片调查

如果你有足够的人员来同时支持多个努力(参见“并行化事件”),考虑将你的努力分成三个轨道,每个轨道负责一个重要的调查部分。例如,你的 OL 可能会将他们的临时团队分成三个小组:

  • 一个取证小组调查系统并确定攻击者触及的系统。

  • 一个逆向小组研究可疑的二进制文件,确定作为妥协指标(IOCs)的唯一指纹。⁶

  • 一个狩猎小组搜索所有系统,寻找这些指纹。该组在识别嫌疑系统时随时通知取证组。

图 17-1 显示了各组之间的关系。OL 负责保持这些团队之间的紧密反馈循环。

调查组之间的关系

图 17-1:调查组之间的关系

最终,你发现新线索的速度会放缓。在这一点上,IC 决定是时候转向补救了。你是否已经学到了所有需要学到的东西?可能没有。但你可能已经学到了所有需要知道的东西,以成功地清除攻击者并保护他们追求的数据。确定在哪里划线可能很困难,因为涉及到所有未知因素。做出这个决定很像知道何时停止加热一袋爆米花:当爆裂之间的间隔明显增加时,你应该在整袋爆米花烧焦之前离开。在进行补救之前,需要进行大量的规划和协调。第十八章会更详细地介绍这一点。

控制事件

一旦宣布事件并分配了团队成员的责任,IC 的工作就是保持工作顺利进行。这项任务涉及预测响应团队的需求,并在问题出现之前解决这些需求。为了有效,IC 应该把所有的时间都用来控制和管理事件。如果作为 IC,你发现自己在检查日志、执行快速取证任务或以其他方式参与运营,那么是时候退后一步重新评估你的优先事项了。如果没有人掌舵,船肯定会偏离航线甚至坠毁。

并行化事件

理想情况下,经验丰富的 IR 团队可以通过将事件响应过程的所有部分分解并同时运行,尽可能地并行化事件。如果你预料到在事件生命周期中会有未分配的任务或需要的信息,就指派某人完成任务或为工作做准备。例如,你可能还没有准备好与执法部门或第三方公司分享你的取证结果,但如果你计划在将来分享你的发现,你的原始调查笔记将没有帮助。指派某人在调查进行过程中准备一个经过编辑和可共享的指标列表。

在进行取证调查的早期阶段开始准备清理环境可能看起来有些违反直觉,但如果你有可用的工作人员,那么现在是分配这项任务的好时机。IMAG 允许您随时创建自定义角色,因此您可以在事件的任何时候分配修复主管(RL)角色。RL 可以在运营团队发现受损区域时开始研究这些区域。凭借这些信息,RL 可以制定清理和修复这些受损区域的计划。当运营团队完成他们的调查时,IC 将已经制定了清理计划。在这一点上,他们可以启动下一阶段的工作,而不是当场决定下一步该怎么做。同样,如果您有可用的工作人员,现在就指定某人开始事后分析也不为时过早。

借用编程构造,大型事件中的 IC 角色看起来像是通过while循环的一组步骤:

(While the incident is still ongoing):
  1\. Check with each of your leads:
    a. What’s their status?
    b. Have they found new information that others may need to act upon?
    c. Have they hit any roadblocks?
    d. Do they need more personnel or resources?
    e. Are they overwhelmed?
    f. Do you spot any issues that may pop up later or cause problems?
    g. How’s the fatigue level of each of your leads? Of each team?
  2\. Update status documents, dashboards, or other information sources.
  3\. Are relevant stakeholders (executives, legal, PR, etc.) informed?
  4\. Do you need any help or additional resources?
  5\. Are any tasks that could be done in parallel waiting?
  6\. Do you have enough information to make remediation and cleanup 
     decisions?
  7\. How much time does the team need before they can give the next 
     set of updates?
  8\. Is the incident over?
Loop to beginning.

OODA 循环是与事件相关的决策制定的另一个相关框架:响应者应该观察定位决策行动。这是一个有用的记忆法,提醒您仔细考虑新信息,思考它如何适用于您事件的整体情况,有意识地决定行动方案,然后才采取行动。

移交

没有人能够长时间不间断地解决一个问题而不出现问题。大多数时候,我们遇到的危机需要时间来解决。在响应者之间顺利地传递工作是必不可少的,并有助于建立安全和可靠的文化(参见第二十一章)。

对抗大型森林火灾是在体力和情感上都非常具有挑战性的,可能需要数天、数周甚至数月。为了对抗这样的火灾,加利福尼亚州林业和消防局将其分为“区域”,并为每个区域指定一个事件指挥官。每个区域的 IC 都会为其区域制定长期目标和实现目标的短期目标。他们将可用资源分配到不同的班次中,这样当一组团队工作时,另一组团队就休息,并准备在第一组团队疲惫时接替他们的工作。

当灭火工作的人员接近极限时,IC 从所有团队负责人那里收集状态更新,并指定新的团队负责人来取代他们。然后,IC 向新的团队负责人简要介绍整体目标、他们的任务、他们预期完成的具体目标、可用资源、安全隐患和其他相关信息。然后,新的团队负责人和他们的工作人员接替上一班的工作人员。每位新负责人都会迅速与前任负责人沟通,以确保他们没有遗漏任何相关事实。疲惫的工作人员现在可以休息,而灭火工作可以继续进行。

安全事件响应并不像灭火那样需要体力,但您的团队会经历类似的情绪和与压力相关的疲惫。当有必要时,将工作移交给其他人是至关重要的。最终,疲惫的响应者会开始犯错误。如果团队工作了足够长的时间,他们会犯的错误会比他们纠正的错误还要多——这种现象通常被称为边际效益递减法则。员工疲劳不仅仅是善待您的团队的问题;疲劳可能会通过错误和低士气削弱您的响应。为了避免过度工作,我们建议将班次(包括 IC 班次)限制在不超过 12 小时的连续工作时间内。

如果你有大量员工并希望加快响应速度,考虑将响应团队分成两个较小的小组。这些团队可以在事件解决之前每天 24 小时进行响应。如果你没有大量员工,你可能需要接受响应速度较慢的风险,以便让员工回家休息。虽然小团队可以在“英雄模式”下工作更长时间,但这种模式是不可持续的,结果质量较低。我们建议你非常节制地使用英雄模式。

考虑一个在美洲、亚太和欧洲地区都有团队的组织。这种组织可以安排“追随太阳”轮换,以便根据时间表不断地有新的响应者参与事件,如图 17-2 所示。规模较小的组织可能会有类似的时间表,但地点更少,轮换时间更长,或者可能是一个地点,一半的运营人员在夜班工作,以保持响应,而另一半休息。

追随太阳轮换

图 17-2:追随太阳轮换

IC 应该提前准备好事件交接。交接包括更新跟踪文档、证据笔记和文件,以及在班次期间保留的任何其他书面记录。提前安排好交接的物流、时间和沟通方式。会议应该以当前事件状态和调查方向的总结开始。你还应该包括每个负责人(运营、通信等)向他们对应的接替者进行正式交接。

你应该向接替团队传达的信息取决于事件。在谷歌,我们发现离任团队的 IC 问自己“如果我不是要把这个调查交给你,我接下来会花 12 小时做什么?”这个问题的答案应该在交接会议结束之前由接替团队的 IC 给出。

例如,交接会议的议程可能如下所示:

  1. [离任 IC] 指派一人做笔记,最好是来自接替团队的人。

  2. [离任 IC] 总结当前状态。

  3. [离任 IC] 概述如果他们没有交接事件,接下来 12 小时 IC 会做的任务。

  4. [所有与会者] 讨论这个问题。

  5. [接替 IC] 概述你预期在接下来的 12 小时内处理的任务。

  6. [接替 IC] 确定下次会议的时间。

士气

在任何重大事件中,指挥官需要保持团队士气。这个责任经常被忽视,但是至关重要。事件可能会带来压力,每个工程师对这些高压情况的反应都会不同。有些人会迎接挑战并积极参与,而其他人会发现高强度的努力和模糊不清的情况让他们深感沮丧,他们最想做的就是离开事件现场回家。

作为 IC,不要忘记激励、鼓励和跟踪团队的整体情绪状态是实现积极事件结果的关键因素。以下是在危机中保持士气的一些建议:

吃饭

当你的响应团队努力取得进展时,饥饿势必会袭来。这会降低团队的效率,可能会让人们感到不安。提前计划休息时间和尽可能带来食物。这有助于在需要时保持团队的愉快和专注。

睡觉

人类也适用边际效益递减法则。随着疲惫的产生,您的员工在时间推移中会变得不那么有效,每个成员都会超过自己的疲劳极限点。在这一点之后,继续工作可能会导致更多的错误而不是进展,使事件响应倒退。密切关注响应者的疲劳情况,并确保他们在需要时有休息时间。领导者甚至可能需要介入,以确保那些没有意识到自己需要休息的人也能休息。

减压

当机会出现时,例如,如果团队正在等待文件阵列完成重建,无法同时取得进展,可以让大家一起进行减压活动。几年前,在谷歌发生一起特别大规模且持续时间较长的事件时,响应团队花了一个小时的时间用锤子和液氮粉碎了一个损坏的硬盘。多年后,参与其中的团队回忆起这个活动作为响应的一个亮点。

观察倦怠

作为一名 IC,您应该积极观察团队(包括自己)是否出现倦怠的迹象。一名关键工程师是否开始变得更加愤世嫉俗?员工是否表达了对挑战无法克服的恐惧?这可能是他们第一次处理重大事件,他们可能有很多恐惧。与他们坦诚交谈,并确保他们理解您的期望以及他们如何满足这些期望。如果团队成员需要休息,而您有人可以替代他们,可以提供休息时间。

以身作则

领导者公开表达的现实但积极的展望对于设定团队对成功的期望是非常有帮助的。这种展望鼓励团队,并让他们觉得他们可以实现目标。此外,响应团队的成员可能怀疑是否真的鼓励自我关怀(例如,充分进食和睡眠),直到他们看到 IC 或 OL 公开这样做作为最佳实践。

沟通

在事件响应中涉及的所有技术问题中,沟通仍然是最具挑战性的。即使在最好的情况下,与同事和团队之外的其他人进行有效的沟通也可能很困难。在受到压力、紧迫的截止日期和安全事件的高风险的情况下,这些困难可能会加剧,并迅速导致延迟或错过响应。以下是您可能遇到的一些主要沟通挑战,以及管理它们的建议。

注意

许多优秀的书籍都全面涵盖了沟通主题。为了更深入地了解沟通,我们推荐尼克·摩根的《你能听到我吗?》(哈佛商业评论出版社,2018 年)和艾伦·奥尔达的《如果我理解了你,我会有这种表情吗?》(兰登书屋,2017 年)。

误解

响应者之间的误解很可能构成您沟通问题的主要部分。习惯一起工作的人可能会对未说出的事情做出假设。不习惯一起工作的人可能会使用陌生的行话、不同团队之间意义不同的首字母缩略词,或者假设一个实际上并不普遍的共同参照系。

例如,拿短语“当攻击得到缓解时,我们可以重新启动服务。”来说。对产品团队来说,这可能意味着一旦攻击者的工具被删除,系统就可以安全使用。对安全团队来说,这可能意味着在进行完整调查并得出结论表明攻击者不再能存在于环境中,或者返回环境之前,系统才能安全使用。

当您发现自己在处理事件时,作为一个经验法则,始终明确和过度沟通是有帮助的。解释当您要求某事或设定期望时,即使您认为对方应该知道您的意思。在前面段落的例子中,即使您认为对方应该知道您的意思,也不妨说一下,“当我们确信所有回路都已修复,并且攻击者不再能够访问我们的系统时,我们可以重新启动服务。”请记住,沟通的责任通常属于沟通者——只有他们才能确保他们正在与之沟通的人收到预期的信息。

避免性语言

避免在紧张局势下表达确定性的另一个常见沟通错误是使用避免性语言。当人们在紧张的情况下被期望给出建议,但又没有信心时,他们往往倾向于在陈述中加入限定词。在不确定的情况下避免表达确定性,比如说“我们相当确定找到了所有的恶意软件”,可能会让人感到更安全,但避免性语言往往会使情况变得模糊,并导致决策者的不确定性。再次强调,明确和过度沟通是最好的解决办法。如果您的 IC 问您是否已经确定了所有攻击者的工具,“我们相当确定”是一个含糊的回答。最好的回答是,“我们对我们的服务器、NAS、电子邮件和文件共享感到肯定,但对我们的托管系统不太有信心,因为我们在那里的日志可见性较低。”

会议

要在事件中取得进展,您需要让人们一起协调他们的努力。与事件响应中的关键参与者进行定期、快速的同步会议是一种特别有效的方式,可以保持对正在发生的一切的控制和可见性。IC、OL、公司律师和一些关键高管可能每两到四个小时开一次会,以确保团队能够迅速适应任何新的发展。

我们建议严格限制这些会议的与会者。如果您的会议室或视频会议室里挤满了人,而只有少数人在发言,其他人在听或查看电子邮件,那么您的邀请名单就太长了。理想情况下,事件负责人应该参加这些会议,而其他人则应该完成需要完成的任务。会议结束后,负责人可以更新各自的团队。

虽然会议通常是共同工作的必要部分,但管理不当的会议会危及您的进展。我们强烈建议 IC 在每次会议开始时都提前制定一个议程。这可能看起来是一个显而易见的步骤,但在持续事件的肾上腺素飙升和压力下很容易忘记。以下是谷歌在安全事件启动会议上使用的一个示例议程:

  1. 【IC】委派一人做笔记。

  2. 【IC】所有与会者介绍自己,从 IC 开始:

  3. 名称

  4. 团队

  5. 角色

  6. 【IC】参与规则:

  7. 您是否需要考虑保密性?

  8. 是否有特定的操作安全问题需要考虑?

  9. 谁负责做决定?谁应该参与决策过程?谁不应该参与?

  10. 让您的负责人知道您是否在切换任务/完成某事。

  11. 【IC】描述当前状态:我们正在解决什么问题?

  12. 【所有人】讨论问题。

  13. 【IC】总结行动和责任人。

  14. 【IC】询问小组:

  15. 我们是否需要任何其他资源?

  16. 我们需要牵涉其他任何团队吗?

  17. 有什么障碍吗?

  18. 【IC】确定下次同步会议的时间和预期出席人员:

  19. 谁是必需的?

  20. 谁是可选的?

以下是进行中的同步会议:

  1. 【IC】委派一人做笔记。

  2. 【IC 或运营主管】总结当前状态。

  3. 【IC】从每个与会者/活动线程接收更新。

  4. 【IC】讨论下一步。

  5. 【运营主管】分配任务,并让每个人重复他们认为自己要做的事情。

  6. 【IC】确定下次会议的时间。

  7. 【运营主管】更新行动追踪器。

在两个示例议程中,IC 指定了一名记录员。通过经验,我们已经了解到,记录员对于保持调查的顺利进行至关重要。负责人忙于处理会议中出现的所有问题,没有精力来做好记录。在事件发生时,你经常会忘记你认为自己会记住的事项。这些记录在撰写事后总结时也变得非常宝贵。请记住,如果事件涉及任何法律后果,您将希望就最佳管理此信息的方式与您的法律团队进行咨询。

向正确的人员提供正确细节水平的信息

确定传达的正确细节水平是事件沟通的一个重大挑战。多个层次的多个人员将需要了解与事件相关的某些内容,但出于操作安全的原因,他们可能不能或不应该知道所有细节。请注意,对事件只有模糊了解的员工可能会进行填补信息的行为。员工之间的谣言很快就会在组织外传播时变得错误和有害。

长期进行的调查可能需要向以下人员提供不同细节水平的频繁更新:

高管和高级领导

他们应该收到关于进展、障碍和潜在后果的简短、简洁的更新。

IR 团队

这个团队需要关于调查的最新信息。

未参与事件的组织人员

您的组织可能需要决定告知人员的内容。如果您告知所有员工有关事件,您可能能够请求他们的帮助。另一方面,这些人可能会传播您意料之外的信息。如果您不告知组织人员有关事件,但他们无论如何发现了,您可能需要处理谣言。

客户

客户可能有法律上的权利在一定时间内被告知事件,或者您的组织可能选择自愿通知他们。如果响应涉及关闭对客户可见的服务,他们可能会有问题并要求答案。

法律/司法系统参与者

如果您将事件升级给执法部门,他们可能会有问题,并且可能开始要求您最初没有打算分享的信息。

为了帮助管理所有这些需求,而不会不断分散 IC 的注意力,我们建议任命一名沟通负责人(CL)。CL 的核心工作是随着事件的发展保持对事件的了解,并向相关利益相关者准备沟通。CL 的主要责任包括以下内容:

  • 与销售、支持和其他内部合作伙伴团队合作,回答他们可能有的任何问题。

  • 为高管、法律人员、监管机构和其他监督角色准备简报。

  • 与新闻和公关合作,确保人们在必要时获得准确和及时的关于事件的陈述。确保响应团队之外的人员不发表相互矛盾的声明。

  • 对事件信息的传播保持持续和谨慎的关注,以便事件人员遵守任何“需要知道”的准则。

CL 将希望考虑与领域专家、外部危机沟通顾问或其他需要帮助管理与事件相关信息的人联系,以最小化延迟。

将所有内容整合在一起

本节通过演示一个任何规模的组织可能遇到的妥协情况,将本章内容联系在一起。考虑这样一个情景,一个工程师发现一个他不认识的服务账户被添加到一个他以前没有见过的云项目中。中午,他将自己的担忧上升到了安全团队。经过初步调查,安全团队确定一个工程师的账户很可能已经被入侵。使用之前提出的建议和最佳实践,让我们从头到尾走一遍你可能如何应对这样的妥协。

分类

你响应的第一步是分类。从最坏的情况假设开始:安全团队的怀疑是正确的,工程师的账户被入侵了。使用特权访问查看敏感的内部信息和/或用户数据的攻击者将是一个严重的安全漏洞,因此你宣布了一起事件。

宣布事件

作为事件指挥官,你通知安全团队其他成员:

  • 发生了一起事件。

  • 你将承担 IC 的角色。

  • 你需要团队的额外支持来进行调查。

通信和运营安全

既然你已经宣布了事件,组织中的其他人——高管、法律部门等——需要知道事件正在进行中。如果攻击者入侵了你组织的基础设施,给这些人发邮件或聊天可能是有风险的。遵循运营安全的最佳实践。假设你的应急计划要求使用组织信用卡在与你组织无关的云环境中注册一个商业账户,并为每个参与事件的人创建账户。为了创建和连接到这个环境,你使用了与你组织的管理基础设施没有连接的全新重建的笔记本电脑。

使用这个新环境,你给你的高管和关键的法律人员打电话,告诉他们如何获得安全的笔记本电脑和云账户,以便他们可以通过电子邮件和聊天参与。由于所有当前的应对人员都在办公室附近,你使用附近的会议室讨论事件的细节。

开始事件

作为 IC,你需要指派工程师进行调查,所以你要求安全团队中具有取证经验的工程师担任运营主管。你的新 OL 立即开始他们的取证调查,并根据需要招募其他工程师。他们首先从云环境中收集日志,重点关注服务账户凭证被添加的时间段。在确认凭证是在工程师离开办公室的时间段内由工程师的账户添加后,取证团队得出结论,该账户已被入侵。

取证团队现在从仅调查嫌疑账户转变为调查账户被添加时周围的所有其他活动。团队决定收集与被入侵账户相关的所有系统日志,以及工程师的笔记本电脑和工作站。团队确定这项调查可能需要一个分析师相当长的时间,因此他们决定增加更多的人手并分配工作。

你的组织没有一个庞大的安全团队,因此没有足够的熟练的取证分析师来充分分配取证任务。然而,你有了解他们系统的系统管理员,并且可以帮助分析日志。你决定将这些系统管理员分配给取证团队。你的 OL 通过电子邮件联系他们,要求通过电话讨论“一些事情”,并在通话中全面介绍情况。OL 要求系统管理员从组织中的任何系统收集与被入侵账户相关的所有日志,而取证团队则分析笔记本电脑和台式机。

到了下午 5 点,很明显调查将比你的团队能够继续工作的时间长得多。作为 IC,你正确地预计到你的团队在解决事件之前会变得疲惫并开始犯错误,因此你需要制定一个交接或连续性计划。你通知你的团队,他们有四个小时的时间来完成尽可能多的日志和主机分析。在此期间,你保持与领导和法律部门的最新信息,并与 OL 联系,看看他们的团队是否需要额外的帮助。

在晚上 9 点的团队同步会议上,OL 透露他们的团队已经找到了攻击者的初始入口点:一封非常精心制作的钓鱼邮件发送给工程师,工程师被骗以运行一个命令,下载了攻击者的后门并建立了持久的远程连接。

交接

到了晚上 9 点的团队同步会议,许多解决问题的工程师已经工作了 12 个小时甚至更长时间。作为一名勤勉的 IC,你知道继续以这样的速度工作是有风险的,而且这个事件将需要更多的努力。你决定交接一些工作。虽然你的组织在旧金山总部之外没有一个完整的安全团队,但在伦敦有一些资深员工。

你告诉你的团队用接下来的一个小时来完成他们的发现文档,同时你联系伦敦团队。伦敦办公室的一名资深工程师被任命为下一任 IC。作为即将离任的 IC,你向接替的 IC 简要介绍了你迄今为止学到的一切。在获得所有与事件相关的文件的所有权并确保伦敦团队了解下一步之后,伦敦 IC 确认他们将负责直到第二天早上 9 点 PST。旧金山团队松了一口气,回家休息。在夜间,伦敦团队继续调查,重点是分析攻击者执行的后门脚本和行动。

交接事件

第二天早上 9 点,旧金山和伦敦团队进行交接同步。在夜间,伦敦团队取得了很多进展。他们确定受损工作站上运行的脚本安装了一个简单的后门,使攻击者能够从远程机器登录并开始查看系统。注意到工程师的 shell 历史记录包括登录到云服务账户,对手利用了保存的凭据,并将自己的服务账户密钥添加到管理令牌列表中。

在这样做之后,他们没有对工作站采取进一步行动。相反,云服务日志显示,攻击者直接与服务 API 进行了交互。他们上传了一个新的机器镜像,并在新的云项目中启动了数十个该虚拟机的副本。伦敦团队尚未分析任何正在运行的镜像,但他们审计了所有现有项目中的凭据,并确认他们所知道的恶意服务账户和 API 令牌是唯一无法验证为合法的凭据。

得到伦敦团队的更新后,你确认了新的信息,并确认自己将接任 IC 的职责。接下来,你提炼新的信息,并向高管和法律部门提供简明扼要的更新。你还向团队介绍了新的发现。

尽管你知道攻击者对生产服务拥有管理访问权限,但你还不知道用户数据是否存在风险或受到影响。你给你的取证团队一个新的高优先级任务:查看攻击者可能对现有生产机器采取的所有其他行动。

准备沟通和纠正

随着调查的进行,你决定是时候将事件响应的一些组件并行化。如果攻击者可能访问了你组织的用户数据,你可能需要通知用户。你还需要减轻攻击。你选择了一位技术写作能力强的同事担任沟通负责人(CL)。你要求一位不参与取证工作的系统管理员成为整改负责人(RL)。

与组织的律师合作,CL 起草了一篇博客文章,解释了发生了什么以及潜在的客户影响。尽管还有许多空白(比如“<填写此处>数据被<填写此处>”),但提前准备好并获得批准的结构可以帮助你在了解全部细节时更快地传达你的信息。

与此同时,RL 制定了组织已知受攻击者影响的每一项资源清单,以及清理每个资源的方案。即使你知道工程师的密码并没有在最初的钓鱼邮件中泄露,你也需要更改他们的账户凭据。为了安全起见,你决定防范尚未发现但可能随后出现的后门。你复制了工程师的重要数据,然后擦除了他们的主目录,在新安装的工作站上创建了一个新的主目录。

随着你的应对工作的进行,你的团队得知攻击者并没有访问任何生产数据,这让大家都松了一口气!攻击者启动的额外虚拟机似乎是一群挖矿服务器,用于挖掘数字货币并将资金转入攻击者的虚拟钱包。你的 RL 指出,你可以删除这些机器,或者如果你希望以后向执法部门报告事件,也可以对这些机器进行快照和归档。你还可以删除创建这些机器的项目。

结束

到了下午中期,你的团队已经没有了线索。他们搜索了可能受到恶意 API 密钥影响的每一个资源,制定了减轻方案,并确认攻击者没有触及任何敏感数据。幸运的是,这只是一次机会主义的挖矿行为,攻击者并不关心你的任何数据,只是想在别人的账单上使用大量计算能力。你的团队决定是时候执行你的整改计划了。在与法律和领导层核实关闭事件决定得到他们的批准后,你向团队发出了行动的信号。

完成了取证任务后,运营团队现在根据整改计划分配任务,并尽快完成,确保攻击者被迅速而彻底地关闭。然后,他们花了下午的时间写下他们的观察结果以供事后分析。最后,你(IC)进行了简报,团队中的每个人都有机会讨论事件的进展。你明确地传达了事件已经结束,不再需要紧急响应。在每个人回家休息之前,你最后的任务是向伦敦团队简报,让他们也知道事件已经结束。

解决方案

  1. 事故管理在规模化时,成为一门独立的艺术,与一般项目管理和较小的事故响应工作有所不同。通过专注于流程、工具和适当的组织结构,可以组建一个能够以当今市场所需的速度有效应对任何危机的团队。无论您是一个小型组织,其工程师和核心团队根据需要成为临时响应团队,还是一个全球范围内设有响应团队的大型组织,或者介于两者之间,您都可以应用本章描述的最佳实践,有效高效地应对安全威胁。并行化您的事故响应和取证工作,并使用 ICS/IMAG 专业管理团队,将帮助您可扩展、可靠地应对任何突发事件。

  2. 例如,参见 Google 的透明报告。

  3. 在 Google 的设计审查中,一位工程师建议说:“有两种软件开发人员:那些对 Ghostscript 进行了沙箱隔离的人,和那些本应该对 Ghostscript 进行沙箱隔离的人。”

  4. Shellshock 是一个远程利用漏洞,非常简单易部署,以至于在发布几天后,数百万台服务器已经受到积极攻击。

  5. 这种紧密的工作循环在步骤之间有最小的延迟,以至于没有人因为等待别人完成他们的部分而无法完成自己的部分。

  6. 反向工程时间轴是系统上发生的所有事件的列表,理想情况下是以与调查相关的事件为中心,按事件发生的时间顺序排列。

  7. 恶意软件逆向工程是相当专业的工作,并不是所有组织都有熟练掌握这种实践的人员。

  8. 如果一个人没有接触到实际信息,他们往往会编造一些东西。

第十八章:恢复和事后

原文:18. Recovery and Aftermath

译者:飞龙

协议:CC BY-NC-SA 4.0

由 Alex Perry,Gary O’Connor 和 Heather Adkins

与 Nick Soda

如果您的组织遭遇严重事件,您会知道如何恢复吗?谁来执行恢复,他们知道要做出什么决定吗? SRE 书的第十七章和 SRE 工作手册的第九章讨论了预防和管理服务中断的做法。这些做法中的许多做法也与安全相关,但是从安全攻击中恢复具有独特的元素,特别是当事件涉及主动恶意攻击者时(参见第二章)。因此,虽然本章提供了处理许多种恢复工作的一般概述,但我们特别强调恢复工程师需要了解有关安全攻击的内容。

正如我们在第八章和第九章中讨论的,根据良好设计原则构建的系统可以抵御攻击并且易于恢复。无论系统是单个计算实例、分布式系统还是复杂的多层应用程序,都是如此。为了促进恢复,良好构建的系统还必须与危机管理策略相结合。如前一章所述,有效的危机管理需要在继续威慑攻击者的同时恢复任何受损资产到已知(可能改进的)良好状态之间取得微妙的平衡。本章描述了良好的恢复检查表所包含的微妙考虑,以实现这些目标。

根据我们的经验,恢复工程师通常是每天设计、实现和维护这些系统的人。在攻击期间,您可能需要召集安全专家担任特定角色,例如执行取证活动、对安全漏洞进行分类或做出微妙的决定,但将系统恢复到已知良好状态需要来自每天与系统一起工作的专业知识。事件协调和恢复工作之间的合作允许安全专家和恢复工程师双向共享信息以恢复系统。

从安全攻击中恢复往往涉及比预先计划的 playbooks 能够适应的更模糊的环境。攻击者可以在攻击过程中改变他们的行为,恢复工程师可能会犯错或发现关于他们的系统的意外特征或细节。本章介绍了一种动态的恢复方法,旨在匹配您的攻击者的灵活性。

恢复行为也可以成为推动改进安全姿态的强大工具。恢复采取短期战术缓解和长期战略改进的形式。我们在本章中介绍了一些思考安全事件、恢复和下一个事件之间的静默期之间的连续性的方式。

恢复物流

如前一章所讨论的,良好管理的事件受益于并行化响应。并行化在恢复过程中尤其有益。参与恢复工作的人员应与调查事件的人员不同,原因有几个:

  • 事件的调查阶段通常是耗时且详细的,需要长时间的专注。在持续的事件中,调查团队通常需要休息,而恢复工作开始时。

  • 事件的恢复阶段可能在您的调查仍在进行中开始。因此,您需要能够并行工作的独立团队,彼此之间提供信息。

  • 进行调查所需的技能可能与进行恢复工作所需的技能不同。

在准备恢复并考虑您的选择时,您应该建立一个正式的团队结构。根据事件的范围,这个团队可以小到一个人,也可以大到整个组织。对于更复杂的事件,我们建议创建协调机制,如正式团队、频繁会议、共享文档存储库和同行评审。许多组织通过使用冲刺、Scrum 团队和紧密的反馈循环,将恢复团队的运作模式建模在其现有的敏捷开发流程上。

从复杂事件中组织良好的恢复可能看起来像精心编排的芭蕾舞表演,不同个体在恢复过程中的行动相互影响。重要的是,恢复芭蕾中的舞者们要避免踩到彼此的脚。因此,您应该明确定义准备、审查和执行恢复的角色,确保每个人都了解操作风险,并且参与者经常进行面对面的沟通。

随着事件的进展,事件指挥官(IC)和运营负责人(OL)应任命一名纠正负责人(RL)开始规划恢复,如第十七章所述。RL 应与 IC 密切协调,制定恢复检查表,以确保恢复工作与调查的其他部分保持一致。RL 还负责组建具有相关专业知识的团队,并制定恢复检查表(在“恢复检查表”中讨论)。

在谷歌,执行恢复的团队是日常构建和运行系统的团队。这些人包括 SRE、开发人员、系统管理员、帮助台人员和管理常规流程(如代码审计和配置审查)的相关安全专家。

信息管理和沟通在恢复过程中是成功响应的重要组成部分。原始事件轨迹、草稿笔记、恢复检查表、新的操作文档以及有关攻击本身的信息将是重要的文档。确保这些文档对恢复团队可用,但对攻击者不可用;使用类似空气隔离的计算机进行存储。例如,您可以使用诸如 bug 跟踪系统、基于云的协作工具、白板,甚至贴在墙上的便签卡等信息管理工具的组合。确保这些工具不在攻击者可能威胁到您系统的最广泛范围之内。考虑从便签卡开始,并在确保没有恢复团队成员的机器受到威胁后添加独立的服务提供商。

良好的信息管理是确保顺利恢复的另一个关键方面。使用每个人都可以访问并实时更新的资源,以便在问题出现或检查表项完成时进行更新。如果您的恢复计划只能由您的纠正负责人访问,这将成为快速执行的障碍。

在恢复系统的同时,保持关于恢复过程中发生的事情的可靠记录也很重要。如果您在途中犯了错误,您的审计轨迹将帮助您解决任何问题。指定专门的记录员或文档专家可能是一个好主意。在谷歌,我们利用技术撰稿人来优化我们的恢复工作中的信息管理。我们建议阅读第二十一章,其中讨论了更多的组织方面。

恢复时间表

开始恢复阶段的最佳时间因调查性质而异。如果受影响的基础设施至关重要,您可能会选择几乎立即从攻击中恢复过来。这在从拒绝服务攻击中恢复时经常发生。另外,如果您的事件涉及攻击者完全控制您的基础设施,您可能会几乎立即开始规划恢复,但只有在完全了解攻击者所做的事情后才执行计划。本章讨论的恢复流程适用于任何恢复时间线:在调查仍在进行时,调查阶段结束后,或者在这两个阶段都进行时。

对事件的足够了解和了解恢复范围将决定采取哪种路线。通常,在启动恢复操作时,调查团队已经开始了事后分析文档(可能是初步原始笔记的形式),恢复团队在进行过程中更新。该文档中的信息将指导恢复团队的规划阶段(参见“规划恢复”),这应该在启动恢复之前完成(参见“启动恢复”)。

由于最初的计划可能随着时间而发展,规划和执行恢复可能会重叠。但是,您不应该在没有某种计划的情况下开始恢复工作。同样,我们建议在进行恢复之前创建恢复检查表。恢复工作完成后,您的恢复后行动(参见“恢复后”)应该尽快开始。在这两个阶段之间允许时间过长可能会导致您忘记先前行动的细节,或者推迟必要的中长期修复工作。

规划恢复

您的恢复工作的目标是减轻攻击并使系统恢复到正常运行状态,并在此过程中应用任何必要的改进。复杂的安全事件通常需要并行化事件管理,并设置结构化团队来执行事件的不同部分。

恢复规划过程将依赖调查团队发现的信息,重要的是在采取行动之前仔细规划恢复。在这些情况下,一旦您对攻击者所做的事情有足够的基线信息,您应该立即开始规划恢复。以下各节描述了一些准备最佳实践和常见陷阱。

确定恢复范围

您如何定义事件的恢复将取决于您遇到的攻击类型。例如,从单个机器上的勒索软件等较小问题中恢复可能相对简单:您只需重新安装系统。然而,要从在整个网络上存在的国家行为者以及窃取敏感数据的攻击者那里恢复,您将需要来自组织各个部门的多种恢复策略和技能。请记住,恢复所需的工作量可能与攻击的严重程度或复杂性不成比例。一家对简单勒索软件攻击毫无准备的组织可能最终会有许多受损的机器,并需要进行资源密集型的恢复工作。

要从安全事件中启动恢复,您的恢复团队需要完整的系统、网络和数据受到攻击的清单。他们还需要足够的关于攻击者战术、技术和程序(TTPs)的信息,以识别可能受到影响的任何相关资源。例如,如果您的恢复团队发现配置分发系统已被入侵,那么这个系统就在恢复范围内。从这个系统接收配置的任何系统也可能在范围内。因此,调查团队需要确定攻击者是否修改了任何配置,以及这些配置是否被推送到其他系统。

正如在第十七章中提到的,理想情况下,指挥官会指派某人在调查早期为缓解文档维护行动项目(在“事后检讨”中讨论)。缓解文档和随后的事后检讨将确定解决妥协根本原因的步骤。您需要足够的信息来优先处理行动项目,并将其分类为短期缓解措施(如修补已知漏洞)或战略性的长期变更(如更改构建流程以防止使用易受攻击的库)。

为了了解如何在未来保护这些资产,您应该检查每个直接或间接受到影响的资产,以及攻击者的行为。例如,如果攻击者能够利用 Web 服务器上的一个易受攻击的软件堆栈,您的恢复将需要了解攻击的方式,以便您可以修补运行该软件包的任何其他系统中的漏洞。同样,如果攻击者通过钓鱼用户的帐户凭据获得访问权限,您的恢复团队需要计划如何阻止另一个攻击者明天做同样的事情。请注意了解攻击者可能能够利用哪些资产进行未来的攻击。您可以考虑制作攻击者的行为和恢复工作的可能防御措施清单,就像我们在第二章中所做的那样(参见表 2-3)。您可以将此清单用作操作文档,以解释为什么要引入某些新的防御措施。

汇总受损资产和短期缓解措施的清单需要进行紧密的沟通和反馈循环,涉及您的事后笔记、调查团队和事件指挥官。您的恢复团队需要尽快了解新的调查发现。如果调查和恢复团队之间的信息交换不高效,攻击者可以绕过缓解措施。您的恢复计划还应该考虑到您的攻击者可能仍然存在并观察您的行动。

深入探讨:恢复考虑

在设计事件的恢复阶段时,您可能会遇到一些难以回答的开放性问题。本节涵盖了一些常见的陷阱和关于如何平衡权衡的想法。这些原则对于经常处理复杂事件的安全专家来说会感到熟悉,但这些信息对于参与恢复工作的任何人都是相关的。在做出决定之前,请问自己以下问题。

您的攻击者将如何回应您的恢复工作?

您的缓解和恢复清单(参见“恢复清单”和“示例”)将包括切断攻击者与您资源的任何连接,并确保他们无法返回。实现这一步骤需要进行微妙的平衡,需要对攻击有近乎完美的了解,并制定一个执行驱逐的坚实计划。一个错误可能导致攻击者采取您未能预料或看到的额外行动。

考虑这个例子:在事件中,您的调查团队发现攻击者已经妥协了六个系统,但团队无法确定最初的攻击是如何开始的。甚至不清楚您的攻击者是如何首次访问您的系统的。您的恢复团队制定并执行了一个重建这六个受损系统的计划。在这种情况下,恢复团队在没有完全了解攻击是如何开始、攻击者将如何回应,或者攻击者是否仍在其他系统上活动的情况下行动。仍在活动中的攻击者将能够从其在另一个受损系统中的位置看到您已经将这六个系统下线,并可能继续破坏其余可访问的基础设施。

除了损害您的系统外,攻击者还可以窃听电子邮件、错误跟踪系统、代码更改、日历和其他资源,这些资源您可能希望用来协调您的恢复。根据事件的严重程度和您正在恢复的妥协类型,您可能希望使用对攻击者不可见的系统进行调查和恢复。

考虑一个协调使用即时消息系统的恢复团队,而其中一个团队成员的帐户已被攻击。攻击者也登录并观看聊天,可以在恢复过程中看到所有私人通信 - 包括调查的任何已知元素。攻击者甚至可能推断出恢复团队不知道的信息。您的攻击者可能利用这些知识以不同的方式妥协更多系统,绕过调查团队可能具有的所有可见性。在这种情况下,恢复团队应该建立一个新的即时消息系统,并部署新的机器 - 例如,廉价的 Chromebook - 用于响应者通信。

这些例子可能看起来很极端,但它们阐明了一个非常简单的观点:在攻击的另一侧有一个人在对您的事件响应做出反应。您的恢复计划应考虑在了解攻击者的访问权限并采取行动以最小化进一步危害的风险。

注意

今天,安全事件响应者普遍认为,在驱逐攻击者之前,您应该等到对攻击有全面的了解。这可以防止攻击者观察您的缓解措施,并帮助您进行防御性响应。

尽管这是一个好建议,但要谨慎应用。如果您的攻击者已经在做一些危险的事情(例如获取敏感数据或破坏系统),您可能会选择在完全了解他们的行动之前采取行动。如果您选择在完全了解攻击者的意图和攻击范围之前驱逐攻击者,那么您就进入了一场国际象棋游戏。做好准备,并知道您需要采取的步骤以达到将军!

如果您正在处理复杂的事件,或者有活跃的攻击者正在与您的系统交互,您的恢复计划应包括与调查团队的紧密整合,以确保攻击者无法重新访问您的系统或绕过您的缓解措施。确保告知调查团队您的恢复计划 - 他们应该确信您的计划将阻止攻击。

您的恢复基础设施或工具是否受到妥协?

在恢复计划的早期阶段,确定您需要进行响应的基础设施和工具,并询问调查团队他们是否认为这些恢复系统已被妥协。他们的答案将决定您是否可以进行安全的恢复,以及您可能需要为更完整的响应做准备的额外补救步骤。

例如,假设攻击者已经入侵了您网络上的几台笔记本电脑和管理它们设置的配置服务器。在这种情况下,您需要在重建任何受损的笔记本电脑之前为配置服务器制定一个补救计划。同样,如果攻击者已经在您的自定义备份恢复工具中引入了恶意代码,您需要找到他们的更改并将代码恢复到正常状态,然后再恢复任何数据。

更重要的是,您必须考虑如何恢复资产——无论是位于目前受攻击者控制的基础设施上的系统、应用程序、网络还是数据。在攻击者控制基础设施的情况下恢复资产可能会导致同一攻击者再次威胁。在这种情况下的常见恢复模式是建立一个“干净”或“安全”的资产版本,例如一个与任何受损版本隔离的干净网络或系统。这可能意味着完全复制整个基础设施,或者至少是其中的关键部分。

回到我们的一个受损配置服务器的例子,您可以选择创建一个隔离网络,并使用全新的操作系统安装重建这个系统。然后您可以手动配置系统,以便您可以从中引导新的机器,而不会引入任何受攻击者控制的配置。

攻击的变体有哪些?

假设您的调查团队报告说,攻击者利用了缓冲区溢出漏洞来攻击您的网络服务基础设施。虽然攻击者只能访问一个系统,但您知道其他 20 台服务器也在运行相同有缺陷的软件。在规划恢复过程时,您应该处理已知受损的系统,但还要考虑另外两个因素:其他 20 台服务器是否也受到攻击,以及您将如何在将来减轻所有这些机器的漏洞影响。

同样值得考虑的是,您的系统是否(在短期内)容易受到您当前经历的攻击类型的变体的影响。在缓冲区溢出的例子中,您的恢复规划应该寻找基础设施中任何相关的软件漏洞——无论是相关的漏洞类别,还是另一个软件中相同的漏洞。这一考虑在自定义代码或使用共享库的情况下尤为重要。我们在第十三章中介绍了几种测试变体的选项,比如模糊测试。

如果您使用的是开源或商业软件,并且测试变体超出了您的控制范围,希望维护软件的人已经考虑了可能的攻击变体并实现了必要的保护措施。值得检查软件堆栈其他部分的可用补丁,并将广泛的升级作为恢复的一部分。

您的恢复是否会重新引入攻击向量?

许多恢复方法旨在将受影响的资源恢复到已知的良好状态。这一努力可能依赖于系统镜像、存储在代码库中的源代码或配置。您恢复的一个关键考虑因素应该是您的恢复操作是否会重新引入使系统容易受攻击的攻击向量,或者是否会使您已经取得的耐久性或安全性进展倒退。考虑一个包含有漏洞软件的系统镜像,允许攻击者威胁系统。如果您在恢复过程中重用这个系统镜像,您将重新引入有漏洞的软件。

这种漏洞重新引入是许多环境中的常见陷阱,包括依赖于通常由整个系统快照组成的“黄金镜像”的现代云计算和内部环境。在系统重新上线之前,重要的是更新这些黄金镜像并删除受损的快照,无论是在源头还是安装后立即进行。

如果攻击者能够修改您的恢复基础设施的部分内容(例如,存储在源代码仓库中的配置),并且您使用这些受损的设置恢复系统,那么您将通过保留攻击者的更改来使恢复倒退。将系统恢复到良好状态可能需要很长时间,以避免这种倒退。这也意味着您需要仔细考虑攻击时间轴:攻击者何时进行了修改,您需要回溯多久来撤销他们的更改?如果无法确定攻击者进行修改的确切时间,您可能需要并行从头开始重建基础设施的大部分。

在从传统备份(如磁带备份)中恢复系统或数据时,您应该考虑您的系统是否也备份了攻击者的修改。您应该销毁或隔离包含攻击者证据的任何备份或数据快照,以供以后分析。

您有哪些缓解选择?

在系统中遵循弹性设计的良好实践(参见第九章)可以帮助您快速从安全事件中恢复。如果您的服务是分布式系统(而不是单片二进制),您可以相对快速、容易地对各个模块应用安全修复:您可以对有缺陷的模块执行“就地”更新,而不会对周围的模块引入重大风险。同样,在云计算环境中,您可以建立机制来关闭受损的容器或虚拟机,并迅速用已知良好的版本替换它们。

然而,根据攻击者已经妥协的资产(如机器、打印机、摄像头、数据和账户),您可能会发现自己只剩下一些不太理想的缓解选择。您可能需要决定哪个选项是最不好的,并为短期内永久驱逐攻击者离开系统而承担不同程度的技术债务。例如,为了阻止攻击者的访问,您可能选择手动向路由器的实时配置添加拒绝规则。为了防止攻击者看到您所做的更改,您可能会绕过正常的程序,不经同行审查和跟踪版本控制系统就进行此类更改。在这种情况下,您应该在将新的防火墙规则添加到配置的规范版本之前禁用自动规则推送。您还应该设置一个提醒,在未来的某个时候重新启用这些自动规则推送。

在决定是否在短期缓解措施中接受技术债务以驱逐攻击者时,问问自己以下问题:

  • 我们可以多快(以及何时)替换或移除这些短期缓解措施?换句话说,这些技术债务会持续多久?

  • 组织是否致力于在其寿命期间维护缓解措施?拥有新技术债务的团队是否愿意接受这笔债务,并在以后通过改进来偿还?

  • 缓解措施会影响我们系统的正常运行时间吗?我们会超出错误预算吗?

  • 组织中的人如何识别这种缓解措施是短期的?考虑将缓解措施标记为以后要删除的技术债务,以便任何其他在系统上工作的人都能看到其状态。例如,向代码添加注释和描述性的提交或推送消息,以便依赖新功能的任何人知道它在未来可能会发生变化或消失。

  • 对于没有关于事件领域专业知识的未来工程师来说,他们如何证明这种缓解措施不再必要,并且可以在不带来风险的情况下将其移除?

  • 如果短期缓解措施长时间保持(无论是意外还是情况所致),其效果会有多大?想象一下,攻击者已经入侵了您的数据库之一。您决定在对数据进行清理和迁移至新系统的同时保持数据库在线。您的短期缓解措施是将数据库隔离在一个单独的网络上。问问自己:如果迁移需要六个月而不是原定的两周,效果会怎样?组织中的人会不会忘记数据库曾经受到攻击,并意外地将其重新连接到安全网络?

  • 领域专家是否已经确定了您对前面问题的答案中存在的漏洞?

恢复检查表

一旦您确定了恢复的范围,您应该列出您的选择(如“启动恢复”中所讨论的),并仔细考虑您需要做出的权衡。这些信息构成了您的恢复检查表的基础(或者根据事件的复杂程度可能有多个检查表)。您进行的每项恢复工作都应该利用常规和经过测试的做法。彻底记录和分享您的恢复步骤可以让参与事件响应的人更容易合作并就恢复计划提供建议。一份记录完善的检查表还可以让您的恢复团队确定可以并行化的工作领域,并帮助您协调工作。

如图 18-1 中的模板恢复检查表所示,检查表上的每一项都对应一个单独的任务,以及完成该任务所需的相应技能。⁴ 恢复团队的成员可以根据自己的技能来领取任务。然后,事件指挥官可以确信所有已完成的恢复步骤都已经被勾选。

您的检查表应包含所有相关细节,比如用于恢复的具体工具和命令。这样,当您开始清理工作时,所有团队成员都将对需要完成的任务有清晰、达成一致的指导,并知道完成的顺序。检查表还应考虑在计划失败时需要的任何清理步骤或回滚程序。我们将在本章末尾的实例中使用图 18-1 中的模板检查表。

检查表模板

图 18-1. 检查表模板

启动恢复

在发生安全事件后,系统的安全可靠恢复在很大程度上依赖于有效的流程,比如精心构建的检查表。根据您正在处理的事件类型,您需要考虑有效的技术选项。您的缓解和恢复工作的目标是将攻击者从您的环境中驱逐出去,确保他们无法返回,并使您的系统更加安全。第九章涵盖了提前将恢复选项设计到系统中的原则。本节涵盖了在考虑这些原则的情况下执行恢复的实际现实,以及做出某些决定的利弊。

隔离资产(隔离)

隔离(也称为*隔离)是减轻攻击影响的一种非常常见的技术。一个经典的例子是将恶意二进制文件移动到隔离文件夹的防病毒软件,文件权限会阻止系统上的其他任何东西读取或执行该二进制文件。隔离也常用于隔离单个受损的主机。您可以在网络层面(例如,通过禁用交换机端口)或主机本身(例如,通过禁用网络)对主机进行隔离。甚至可以使用网络分割来隔离整个受损机器的网络——许多 DoS 响应策略会将服务从受影响的网络中移开。

如果你需要让受损的基础设施继续运行,隔离资产也可能是有用的。考虑这样一个场景:你的一个关键数据库已经受到了攻击。由于它的重要性,你需要在减轻过程中保持数据库在线——也许这个数据库是使命关键的,并且需要几周时间来重建,你的组织不想为此关闭整个业务。你可以通过将数据库隔离在自己的网络上,并对它可以发送和接收的网络流量(来自互联网和你的基础设施的其他部分)进行限制,来减少攻击者的影响。

一个警告:让受损的资产在线上是一种顽固的技术债务。如果你不及时解决这个债务,并且让它们在线上的时间超过预期,这些受损的资产可能会造成重大损害。这可能会发生出于几个原因:因为这是隔离数据的唯一副本(没有备份),因为替换隔离资产存在挑战,或者因为在事件的忙碌中人们简单地忘记了受损的资产。在最坏的情况下,你的组织中的新人(或者事件中的新人)甚至可能会取消隔离受损的资源!

考虑一些标记这些资产受损的方法,比如在设备上使用高度可见的贴纸,或者保持一个最新的隔离系统的 MAC 地址列表,并监控这些地址是否出现在你的网络上。贴纸可以主动避免重复使用,而地址列表可以快速地进行反应性移除。确保你的恢复清单和事后总结覆盖了任何隔离资产是否安全和永久地得到了补救。

系统重建和软件升级

考虑以下难题:你在三个系统上发现了攻击者的恶意软件,并且正在进入事件的恢复阶段。为了驱逐攻击者,你是删除恶意软件并让系统继续运行,还是重新安装系统?根据你的系统的复杂性和关键性,你可能需要考虑这些选项之间的权衡。一方面,如果受影响的系统是使命关键的,并且难以重建,你可能会倾向于删除攻击者的恶意软件并继续前进。另一方面,如果攻击者安装了多种类型的恶意软件,而你并不知道所有的恶意软件,你可能会错过全面清理的机会。通常,从头开始重新安装系统并使用已知的良好镜像和软件是最佳解决方案。

如果你使用可靠和安全的设计原则来操作你的环境,重建系统或升级软件应该相对简单。第九章提供了一些关于了解系统状态的提示,包括主机管理和固件。

例如,如果你使用的系统具有硬件支持的引导验证,可以通过加密的信任链一直到操作系统和应用程序(Chromebook 是一个很好的例子),那么恢复系统只是一个简单的断电重启的问题,这将使系统返回到一个已知的良好状态。像 Rapid 这样的自动发布系统(如SRE 书第八章中讨论的)也可以提供一种可靠和可预测的方式来在恢复过程中应用软件更新。在云计算环境中,你可以依靠即时的容器和软件发布来用安全的标准镜像替换任何受损的系统。

如果您在没有源代码控制系统或标准系统镜像来管理配置或使用已知良好版本的系统的情况下进入事件的恢复阶段,请考虑将这些机制作为短期恢复计划的一部分引入。有一些开源选项可用于管理系统构建,例如Bazel;配置,例如Chef;以及应用程序集成和部署,例如Helm用于 Kubernetes。在短时间内采用新的解决方案可能一开始看起来令人望而生畏,而在设置这些解决方案时,您可能需要对正确的配置进行初步尝试。如果弄清楚正确的配置需要时间和精力,而这会牺牲其他重要的技术工作,您可能需要稍后完善您的配置。确保您仔细考虑您为了短期安全而积累的技术债务,并制定改进这些新系统设置的计划。

数据清理

根据您的事件范围,您应该确认攻击者没有篡改您的源代码、二进制文件、图像和配置,或者您用于构建、管理或发布它们的系统。清理系统生态系统的一种常见技术是从其原始来源(如开源或商业软件提供商)、备份或未被篡改的版本控制系统中获取已知的良好副本。一旦您获得了已知的良好副本,您可以对您想要使用的版本进行校验和比较,以确保其与已知的良好状态和软件包一致。如果您的旧良好副本托管在受损的基础设施上,请确保您非常有信心,知道攻击者何时开始篡改您的系统,并确保审查您的数据来源。

代码来源的强大二进制溯源(如第十四章中所讨论的)使恢复更加简单。想象一下,您发现攻击者在您的构建系统上使用的glibc库中引入了恶意代码。您需要识别在“风险”时间范围内构建的所有二进制文件,这些二进制文件部署在哪里,以及它们具有的任何依赖关系。在进行这项检查时,清楚地标记已知的受损代码、库和二进制文件。您还应该创建测试,以防止您重新引入有漏洞或后门的代码。这些预防措施将确保您的恢复团队中的其他人在恢复过程中不会无意中使用受损的代码,也不会意外地重新引入有漏洞的版本。

您还应该检查攻击者是否篡改了任何应用程序级别的数据,例如数据库中的记录。如第九章所述,确保备份的强大加密完整性会增加您对这些备份的信心,并使您能够确保您需要与潜在受损的实时数据进行比较的任何比较都是准确的。调和攻击者所做的更改可能也会非常复杂,并且可能需要您构建特殊的工具。例如,为了利用部分恢复,您可能需要定制工具将从备份中获取的文件或记录拼接到您的生产系统中,同时进行同时的完整性检查。理想情况下,在制定可靠性策略时,您应该构建和测试这些工具。

恢复数据

恢复过程通常依赖于支持一系列操作的工具,例如回滚、恢复、备份、隔离重建和事务回放。“持久数据”讨论了安全存储用于这些操作的数据。

许多这些工具都有参数,可以在进展速度和数据安全之间进行交易。除非这些工具经常针对真实的生产规模工作负载进行测试,否则我们不建议更改这些参数的默认值。测试、暂存或(更糟糕的是)模拟不能真实地测试基础设施系统。例如,很难真实地模拟内存缓存填充所需的延迟,或者负载平衡估算器在真实的生产条件之外稳定所需的时间。这些参数因服务而异,由于延迟通常在监控数据中可见,您应该在事件之间调整这些设置。当您试图从敌对的安全攻击中恢复时,处理一个行为不端的工具就像面对一个新的攻击者一样具有挑战性。

您可能已经有监控措施来检测由软件错误导致的重大数据丢失。也有可能您的攻击者避免触发这些警报。即便如此,审查这些日志总是值得的:数据可能会确定攻击开始的拐点。如果指标显示了这样的拐点,那么您现在有了一个独立的下限,可以确定要跳过多少个备份。

不足够旧的现场恢复备份可能会重新激活在事件期间备份的任何妥协。如果检测入侵花费了一些时间,您最旧的“安全”备份可能已经被覆盖。如果是这样,数据整治可能是您唯一的选择。

恢复的备份可能包含被意外修改的数据和被损坏的数据。这种损坏可能是由恶意更改(攻击者)或随机事件(例如磁带损坏或硬盘故障)引起的。重建数据的工具往往专注于从随机损坏或恶意损坏中恢复,但不是两者兼顾。了解您的恢复和重建工具提供的功能,以及方法的局限性非常重要。否则,数据恢复的结果可能不符合您的期望。使用常规的完整性程序,例如根据已知的良好加密签名验证恢复的数据,将有所帮助。最终,冗余和测试是应对随机事件的最佳防御措施。

凭据和秘密轮换

攻击者常常会劫持您基础设施中使用的现有帐户,以冒充合法用户或服务。例如,进行密码钓鱼攻击的攻击者可能会尝试获取他们可以用来登录到您系统的帐户凭据。同样,通过诸如传递哈希的技术,攻击者可以获取和重复使用凭据,包括管理员帐户的凭据。第九章讨论了另一种情况:SSH authorized_keys文件的妥协。在恢复过程中,您通常需要通过诸如密钥轮换(例如 SSH 认证中使用的密钥)的方法来轮换系统、用户、服务和应用帐户的凭据。根据您的调查结果,辅助设备(例如网络设备和带外管理系统)以及云端服务(如 SaaS 应用程序)也可能在范围之内。

您的环境可能有其他需要关注的秘密,例如用于数据静态加密和用于 SSL 的加密密钥。如果您的前端 Web 服务基础设施受到破坏或者可能被攻击者访问,您可能需要考虑轮换 SSL 密钥。如果在攻击者窃取密钥后您不采取行动,他们可能会使用密钥进行中间人攻击。同样,如果数据库中记录的加密密钥在受损的数据库服务器上,最安全的方法是轮换密钥并重新加密数据。

加密密钥通常也用于应用级通信。如果攻击者可以访问存储这些应用级密钥的系统,你需要对密钥进行轮换。仔细考虑存储 API 密钥的位置,比如你用于云服务的密钥。将服务密钥存储在源代码或本地配置文件中是一个常见的漏洞:如果攻击者可以访问这些文件,他们以后可以访问其他环境。作为恢复的一部分,你应该确定攻击者是否可以访问这些文件(尽管可能无法证明他们被访问),并保守而经常地轮换这些服务密钥。

根据情况,凭证轮换可能需要谨慎执行。对于单个被钓鱼的账户,要求用户更改密码可能是一个简单的任务。然而,如果攻击者可以访问各种账户,包括管理员账户,或者你不确定他们可能已经 compromise 了哪些账户,你可能需要轮换所有用户的凭证。在创建恢复清单时,确保列出重置账户的顺序,优先考虑管理员凭证、已知被 compromise 的账户以及授予对敏感资源访问权限的账户。如果你有大量的系统用户,你可能需要通过一次性事件中断所有用户。

凭证轮换的另一个复杂之处在于,如果你的组织处理实践不够严密,一次攻击和应对可能会使情况变得更糟。假设你的公司使用集中系统(如 LDAP 数据库或 Windows Active Directory)来管理员工账户。其中一些系统会存储密码历史记录(通常是经过哈希和加盐的)。通常,系统会保留密码历史记录,以便你可以将新密码与旧密码进行比较,并防止用户随时间重新使用密码。如果攻击者可以访问所有哈希密码并破解这些密码,他们可能能够推断用户更新密码的模式。例如,如果用户在每个密码中使用了一年(password2019password2020 等),攻击者可能能够预测下一个密码。当密码更改是你的补救策略的一部分时,这可能是危险的。

如果你可以接触到安全专家,最好在制定恢复计划时咨询他们。这些专家可以进行威胁建模,并提供如何改进你的凭证处理实践的建议。他们可能会建议通过采用双因素认证来消除复杂的密码方案的需要。

恢复后

一旦你驱逐了攻击者并完成了恢复,下一步是过渡出事故并考虑发生的长期影响。如果你经历了像单个员工账户或单个系统被 compromise 这样的小事件,恢复阶段和事后可能相对简单。如果你经历了一个更严重的事件,影响更大,恢复和事后阶段可能会更加广泛。

在 2009 年的奥罗拉行动之后,谷歌着手对其环境进行系统性和战略性的改变。我们有些改变是在一夜之间完成的,而其他一些改变,比如 BeyondCorp 和我们与 FIDO 联盟合作,推动广泛采用使用安全密钥的双因素认证,需要更多的时间。你的事后分析应该区分短期内和长期内你可以实现的目标。

事后分析

对于处理事件的团队来说,保留关于他们工作的笔记是一个良好的做法,之后可以整合到官方的事后总结中。每个事后总结都应包括一份行动项目清单,解决你在事件中发现的根本问题。一个强大的事后总结涵盖了攻击者利用的技术问题,并且认识到了改进事件处理的机会。此外,你应该记录与这些行动项目相关的时间框架和努力,并决定哪些行动项目属于短期和长期路线图。我们在 SRE 书的第十五章中详细介绍了无过失的事后总结,但这里有一些额外的关注安全的问题需要考虑:

  • 事件的主要影响因素是什么?环境中是否存在其他变体和类似问题,你可以解决?

  • 哪些测试或审计流程应该更早地检测到这些因素?如果它们还不存在,你是否可以建立这样的流程来捕捉未来类似的因素?

  • 这一事件是否被预期的技术控制(如入侵检测系统)检测到?如果没有,你如何改善检测系统?

  • 事件被发现和应对的速度有多快?这是否在可接受的时间范围内?如果不是,你如何改善你的响应时间?

  • 重要数据是否受到足够的保护,以阻止攻击者访问?你应该引入哪些新的控制措施?

  • 恢复团队是否能够有效地使用工具,包括源代码版本控制、部署、测试、备份和恢复服务?

  • 团队在恢复过程中绕过了哪些正常程序,比如正常测试、部署或发布流程?你现在可能需要应用什么补救措施?

  • 是否对基础设施或工具进行了临时缓解的更改,现在需要重构?

  • 在事件和恢复阶段,你识别并记录了哪些错误,现在需要解决?

  • 行业和同行群体中存在哪些最佳实践,可以在防范、检测或应对攻击的任何阶段帮助你?

我们建议制定一套明确的行动项目清单,明确指定所有者,并按照短期和长期举措的顺序进行排序。短期举措通常比较简单,实现起来不需要太长时间,并且解决的问题范围相对较小。我们通常称这些行动项目为“低挂果”,因为你可以轻松地识别和解决它们。例如,添加双因素身份验证、缩短应用补丁的时间或建立漏洞发现程序。

长期的举措可能会融入到你的组织安全姿态改进的整体计划策略中。这些行动项目通常更基本地影响你的系统和流程的运作方式,例如成立专门的安全团队、部署骨干加密或改变操作系统选择。

在理想的情况下,从妥协到安全姿态改进的完整事件生命周期将在下一次事件发生之前完成。然而,请记住,在上一次事件之后的例行稳定状态期间也是下一次事件之前的例行状态。事件活动的这段时间是你学习、适应、识别新威胁并为下一次事件做准备的机会。本书的最后部分专注于在这些例行状态期间改进和维护你的安全姿态。

例子

以下的工作示例展示了事后分析、恢复清单的建立以及恢复执行的关系,以及长期缓解措施如何融入更大的安全计划。这些示例并未涵盖所有的事件响应考虑,而是侧重于如何在驱逐攻击者、减轻他们的即时行动和进行长期变更之间进行权衡。

受损的云实例

场景:贵组织使用的基于网络的软件包在云提供商的基础设施中用于为用户流量提供服务的虚拟机(VM)存在常见的软件漏洞。一名机会主义的攻击者使用漏洞扫描工具发现了您的易受攻击的网络服务器并利用了它们。一旦攻击者接管了虚拟机,他们就会利用它们发起新的攻击。

在这种情况下,修复负责人使用了调查小组的笔记,确定团队需要修补软件,重新部署虚拟机,并关闭受损的实例。修复负责人还确定,短期的缓解措施应该发现并解决相关的漏洞。图 18-2 提供了这一事件的一个假设恢复清单。出于简洁起见,我们省略了具体的命令和执行步骤,而您的实际恢复清单应包括这些细节。

受损云实例的恢复清单

图 18-2. 受损云实例的恢复清单

恢复完成后,由所有参与事件的人共同开发的事故事后分析确定了需要一个正式的漏洞管理计划,以便及时主动地发现和修补已知问题。这一建议被纳入了改善组织安全姿态的长期战略中。

大规模钓鱼攻击

场景:在七天内,攻击者对贵组织发起了密码钓鱼攻击,而贵组织并未使用双因素认证。70%的员工上当受骗。攻击者使用这些密码读取了贵组织的电子邮件,并向媒体泄露了敏感信息。

作为这种情况下的 IC,您面临着许多复杂性:

  • 调查小组确定,攻击者尚未尝试使用密码访问除电子邮件之外的任何系统。

  • 贵组织的 VPN 和相关 IT 服务,包括云系统的管理,使用独立的认证系统与电子邮件服务不同。然而,许多员工在不同服务之间共享密码。

  • 攻击者表示他们将在未来几天对贵组织采取更多行动。

与修复负责人合作,您必须权衡快速驱逐攻击者(通过取消他们对电子邮件的访问权限)和确保攻击者在此期间无法访问任何其他系统之间的权衡。这项恢复工作需要精确的执行,图 18-3 提供了一个假设的清单。同样,出于简洁起见,我们省略了确切的命令和程序,但您的真实清单应包括这些细节。

大规模钓鱼攻击的恢复清单

图 18-3. 大规模钓鱼攻击的恢复清单

恢复完成后,您的正式事后分析强调了以下需求:

  • 更广泛地在关键通信和基础设施系统上使用双因素认证

  • 单点登录解决方案

  • 员工关于钓鱼攻击的教育

贵组织还注意到需要一名 IT 安全专家为您提供建议标准最佳实践。这一建议被纳入了改善贵组织安全姿态的长期战略中。

需要复杂恢复的有针对性攻击

情景:你并不知情,攻击者已经能够访问你的系统一个多月了。他们窃取了用于保护与客户在网上通信的 SSL 密钥,向你的源代码中插入了后门,以每笔交易中盗取 0.02 美元,并监视你组织中高管的电子邮件。

作为 IC,你面临着许多复杂性:

  • 你的调查团队不清楚攻击是如何开始的,或者攻击者如何继续访问你的系统。

  • 你的攻击者可以监视高管和事件响应团队的活动。

  • 你的攻击者可以修改源代码并部署到生产系统。

  • 你的攻击者已经能够访问存储 SSL 密钥的基础设施。

  • 与客户进行的加密网络通信可能会受到损害。

  • 你的攻击者已经窃取了钱。

我们不会为这种相当复杂的攻击提供详细的恢复检查表,而是会专注于事件的一个非常重要的方面:需要并行恢复。与 RL 合作,你需要为这些问题中的每一个创建一个恢复检查表,以便更好地协调恢复步骤。在此期间,你还需要与正在了解攻击者过去和当前行动的调查团队保持联系。

例如,你的努力可能需要一些或所有以下恢复团队(你可能还可以想到其他的):

电子邮件团队

这个团队将切断攻击者对电子邮件系统的访问,并确保他们无法重新进入。团队可能需要重置密码,引入双因素认证等。

源代码团队

这个团队将确定如何切断对源代码的访问,清理受影响的源代码文件,并重新部署经过清理的代码到生产系统。为了完成这项工作,团队需要知道攻击者何时在生产环境中更改了源代码,以及是否存在安全版本。这个团队还需要确保攻击者没有对持久版本控制历史进行更改,并且不能进行额外的源代码更改。

SSL 密钥团队

这个团队将确定如何安全地部署新的 SSL 密钥到网络服务基础设施。为了完成这项工作,团队需要知道攻击者是如何获取密钥的,并且如何防止未经授权的访问。

客户数据团队

这个团队将确定攻击者可能访问到了哪些客户信息,以及是否需要进行补救措施,比如客户密码更改或会话重置。这个补救团队可能还与客户支持、法律和相关人员提出的任何其他问题密切相关。

调和团队

这个团队将研究每笔交易中被盗取的 0.02 美元的影响。你是否需要在数据库中追加记录以适应长期的财务记录?这个团队可能还与财务和法律人员提出的任何其他问题密切相关。

在这种复杂的事件中,通常需要在恢复过程中做出不太理想的选择。例如,考虑需要切断公司电子邮件访问权限的团队。他们可能有一些选择,比如关闭电子邮件系统,锁定所有账户,或者并行启动一个新的电子邮件系统。这些选项都会对组织造成一定影响,可能会提醒攻击者,并且可能并不一定解决攻击者最初获取访问权限的问题。恢复和调查团队需要密切合作,找到适合情况的前进路径。

结论

  1. 恢复是你的事件响应过程中一个独特而关键的步骤,需要一个精通自己领域的专门团队。在规划恢复时需要考虑许多方面:你的攻击者会如何反应?谁执行什么行动,何时执行?你对用户说什么?你的恢复过程应该是并行的,精心计划的,并且与你的事件指挥官和调查团队定期同步。你的恢复工作应该始终以驱逐攻击者、纠正妥协和提高系统整体安全性和可靠性的最终目标为导向。每次妥协都应该产生能够帮助你改善组织长期安全状况的教训。

  2. 从安全事件中恢复的主题在许多资源中都有很好的覆盖,比如NIST SP 800-184,即《网络安全事件恢复指南》。NIST 指南强调了通过提前创建各种类型预期事件的强大恢复检查表来提前计划的必要性。它还提供了关于测试这些计划的说明。这是一个很好的通用参考,但你可能会发现在危机时期实现其严格的建议是具有挑战性的。

  3. 或者三个臭皮匠的小品,如果你没有一个有良好记录的计划!

  4. 错误预算在 SRE 书的第三章中有描述。

  5. 你如何实现“不用键盘”的简报取决于你,但这个练习的主要目的是要得到观众的全神贯注和参与。在事件发生期间,响应者可能会不断地关注运行脚本或状态仪表板,这很诱人。要执行微妙和复杂的恢复,很重要的一点是在简报期间要得到每个人的全神贯注,这样每个人都在同一页面上。

  6. 根据攻击者的复杂程度和攻击的深度,你可能需要考虑重新安装 BIOS 和重新格式化硬盘,甚至更换物理硬件。例如,从 UEFI rootkits 中恢复可能很困难,正如ESET Research 关于 LoJax 的白皮书中所描述的那样。

  7. Pass-the-hash是一种技术,允许攻击者在本地从一台机器上窃取的凭据(NTLM 或 LanMan 哈希)在网络上重播,以登录到其他系统,而无需使用用户的明文密码。

  8. 将云服务密钥存储在本地文件和源代码中并不被认为是最佳实践。有关在 GitHub 中暴露服务密钥的最新系统研究,请参见 Meli, Michael, Matthew R. McNiece, and Bradley Reaves. 2019. “How Bad Can It Git? Characterizing Secret Leakage in Public GitHub Repositories.” Proceedings of the 26th Annual Networked and Distributed Systems Security Symposium. https://oreil.ly/r65fM

  9. 存储密码的最安全方式是使用专门设计用于密码哈希的盐化密钥派生函数,比如scryptArgon2。与仍然常见的盐化 SHA-1 等密码哈希方法不同,这些函数提供了对常见密码恢复攻击的保护,从预先填充的查找表到低成本的专用硬件。即使使用强密码哈希,最佳实践也是确保这些哈希本身在静止状态下使用受保护的密钥进行加密,以增加潜在对手的另一个障碍。

⁹ 有关谷歌为自身和用户实现安全密钥的更多信息,请参阅 Lang, Juan 等人。2016 年。“安全密钥:现代网络的实用密码学第二因素。”第 20 届国际金融密码学和数据安全会议论文集:422-440。https://oreil.ly/bL9Fm

第五部分:组织和文化

原文:Part V. Organization and Culture

译者:飞龙

协议:CC BY-NC-SA 4.0

本书中强调的工程实践将帮助您的组织构建安全可靠的系统,只有在整个组织投入安全可靠文化的情况下,您的努力才会有效。文化是每个组织的强大而独特的定义性组成部分,您不应低估其在推动变革中的作用。

本书的第五部分着重于实现迄今为止所提出方法的文化方面。Chrome 是 Google 旗下首个拥有专门安全团队的产品,积极推动安全为中心的文化。我们从该团队的案例研究开始,重点关注其在 Chrome 的受欢迎和成功中的作用。在第二十章中,我们提出组织中的每个人都对安全和可靠性负有责任。安全专家的角色应该是实现需要专业知识的安全特定技术,并制定最佳实践、政策和培训。第二十一章通过讨论促进健康的安全可靠文化的策略来结束本书。

第十九章:案例研究:Chrome 安全团队

原文:19. Case Study: Chrome Security Team

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Parisa Tabriz

与 Susanne Landers 和 Paul Blankinship

背景和团队演变

2006 年,Google 成立了一个团队,旨在在不到两年的时间内构建一个开源的 Windows 浏览器,该浏览器将比市场上的其他浏览器更安全、更快速和更稳定。这是一个雄心勃勃的目标,并提出了独特的安全挑战:

  • 现代网络浏览器的复杂性与操作系统类似,它们的大部分功能被认为是安全关键的。

  • 客户端和 Windows 软件与 Google 当时大部分现有产品和系统提供有所不同,因此 Google 中央安全团队内部的可转移安全专业知识有限。

  • 由于该项目打算开始并且主要保持开源,因此它具有独特的开发和运营要求,不能依赖于 Google 的企业安全实践或解决方案。

这个浏览器项目最终于 2008 年作为 Google Chrome 推出。自那时起,Chrome 被认为是重新定义了在线安全标准,并成为世界上最受欢迎的浏览器之一。

在过去的十年中,Chrome 的安全组织经历了四个大致的演变阶段:

团队 v0.1

Chrome 在 2008 年正式推出之前并没有正式建立安全团队,而是依赖于工程团队内部的专业知识,以及来自 Google 中央安全团队和第三方安全供应商的咨询。最初的推出并不是没有安全漏洞的——事实上,在公开发布的头两周内发现了许多关键的缓冲区溢出!许多最初的推出漏洞符合开发人员在时间紧迫的情况下试图发布针对性能优化的 C++代码而产生的缺陷模式。浏览器应用程序和 Web 平台的实现也存在漏洞。发现漏洞,修复它们,编写测试以防止回归,并最终设计它们消失是一个成熟团队的正常过程。

团队 v1.0

公开测试版发布一年后,随着浏览器的实际使用开始增长,一个专门的 Chrome 安全团队应运而生。这个最初的安全团队由来自 Google 中央安全团队和新员工的工程师组成,利用了 Google 建立的最佳实践和规范,并从组织外部带来了新的观点和经验。

团队 v2.0

2010 年,Chrome 推出了漏洞奖励计划(VRP)以表彰来自更大安全研究社区的贡献。对 VRP 公告的压倒性回应为安全团队的早期发展提供了一个有用的孵化器。Chrome 最初基于 WebKit——一个之前并没有受到太多安全审查的开源 HTML 渲染引擎,因此团队最初的任务之一是应对大量外部漏洞报告。当时,Chrome 的工程团队非常精简,还不太熟悉整个 WebKit 代码库,因此安全团队发现,解决漏洞的最迅速方法通常是直接着手,建立对代码库的专业知识,并自行修复许多漏洞!

这些早期决定最终对团队未来的文化产生了重大影响。它将安全团队建立为不是孤立的顾问或分析师,而是作为安全专家的混合工程团队。这种混合方法最大的优势之一在于它提供了关于如何将安全开发纳入每个 Chrome 工程师日常流程的独特和实用的见解。

团队 v3.0

到 2012 年,Chrome 的使用量进一步增长,团队的雄心也增长了,攻击者的关注也增加了。为了帮助在不断增长的 Chrome 项目中扩展安全性,核心安全团队建立、社会化并发布了一套核心安全原则。

2013 年,引入了一名工程经理并雇佣了更多致力于安全的工程师之后,团队举行了一次离岸会议,反思他们的工作,定义了团队使命,并就他们想要解决的更大的安全问题以及潜在解决方案进行头脑风暴。这次定义使命的练习导致了一份表述团队共同目标的声明:为 Chrome 用户提供尽可能安全的平台,以浏览网络,并在网络上推进安全性。

在 2013 年的离岸会议上,为了包容地进行头脑风暴,每个人都在便利贴上写下了自己的想法。团队集体整理了这些想法,以确定主题,从而建立了一些持续关注的重点工作领域。这些重点领域包括以下内容:

安全审查

安全团队定期与其他团队协商,帮助设计和评估新项目的安全性,并审查代码库中的安全敏感变更。安全审查是团队的共同责任,并有助于促进知识传递。团队通过撰写文档、举办安全培训,并担任 Chrome 代码的安全关键部分的所有者来扩展这项工作。

发现和修复漏洞

拥有数百万行安全关键代码和数百名全球开发人员不断进行更改,团队投资于一系列方法,以帮助每个人尽快找到和修复漏洞。

架构和利用缓解

团队意识到永远无法预防所有安全漏洞,因此他们投资于安全设计和架构项目,以最小化任何单个漏洞的影响。由于 Chrome 可在流行的桌面和移动操作系统上使用(例如 Microsoft Windows、macOS、Linux、Android 和 iOS),这些操作系统本身不断发展,这需要持续的针对特定操作系统的投资和策略。

可用安全性

无论团队对 Chrome 软件和用于构建它的系统有多么自信,它们仍然需要考虑用户(毕竟是可犯错误的)如何以及何时做出关乎安全的决定。鉴于浏览器用户的数字素养范围广泛,团队投资于帮助用户在浏览网页时做出安全决策,使安全更易用。

Web 平台安全

除了 Chrome 之外,团队还致力于提升为构建 Web 应用程序的开发人员的安全性,以便任何人都能更轻松地在 Web 上构建安全体验。

为每个重点领域确定负责任的领导者,以及后来的专门经理,有助于建立更具可扩展性的团队组织。重点领域的领导者重视灵活性、团队范围的信息共享和项目协作,以确保没有个人或重点领域与其他重点领域隔离。

寻找和留住优秀的人才——那些关心团队使命和核心原则,并且与他人合作良好的人——至关重要。团队中的每个人都参与招聘、面试,并为他们的队友提供持续的成长导向反馈。拥有合适的人才比任何组织细节都更重要。

在实际寻找候选人方面,团队大量利用了个人网络,并不断努力培养和扩大那些来自不同背景的人的网络。我们还将一些实习生转为全职员工。偶尔,我们会冷调那些在会议上发言或其发表的作品显示他们关心网络和构建大规模产品的个人。在公开工作的一个优势是,我们可以将潜在候选人指向Chromium 开发者维基上我们团队努力和最近成就的细节,这样他们可以快速了解团队的工作、挑战和文化。

重要的是,我们追求并考虑了那些对安全感兴趣,但在其他领域具有专业知识或成就的个人。例如,我们雇佣了一名具有丰富 SRE 背景的工程师,他对保护人们安全的使命非常关心,并且对学习安全感兴趣。这种多样化的经验和观点被广泛认为是团队成功的关键因素之一。

在接下来的几节中,我们将分享更多关于 Chrome 核心安全原则如何在实践中应用的见解。这些原则对于 Chrome 来说仍然是相关的(约 2020 年),就像它们在 2012 年首次撰写时一样重要。

安全是团队的责任

Chrome 具有如此强烈的安全重点的一个关键原因是,我们将安全作为核心产品原则,并建立了一个安全被视为团队责任的文化。

尽管 Chrome 安全团队有幸几乎完全专注于安全,团队成员意识到他们永远无法为 Chrome 的所有安全负责。相反,他们努力将安全意识和最佳实践融入到每个参与产品开发的人的日常习惯和流程中。系统设计约定旨在使易用、快速和明晰的路径成为安全路径。这通常需要额外的前期工作,但最终导致了更有效的长期合作。

实践中的一个例子是团队处理安全漏洞的方式。所有工程师,包括安全团队成员,都会修复漏洞并编写代码。如果安全团队只是发现并报告漏洞,他们可能会失去对编写无漏洞代码或修复漏洞有多么困难的感触。这也有助于缓解安全工程师不参与传统工程任务时可能出现的“我们”与“他们”的心态。

随着团队超越了早期的漏洞应急阶段,它开始努力发展更加积极主动的安全方法。这意味着投入时间建立和维护模糊测试基础设施和开发人员工具,使其更快速、更容易地识别引入漏洞的变更并进行撤销或修复。开发人员能够快速识别新漏洞,修复起来就更容易,对最终用户的影响也更小。

除了为开发人员创建有用的工具,团队还为工程团队创造了积极的激励机制来进行模糊测试。例如,组织每年的模糊测试比赛并提供奖品,并创建模糊测试教程来帮助任何工程师学习如何进行模糊测试。组织活动并简化贡献的方式有助于人们意识到他们不需要成为“安全专家”来提高安全性。Chrome 的模糊测试基础设施起步较小,只是一个工程师桌子下的一台电脑。截至 2019 年,它支持了 Google 和全球范围内的模糊测试。除了模糊测试,安全团队还构建和维护安全基础库(例如安全数字库),以便任何人实现变更的默认方式都是安全的方式。

安全团队成员经常向个人或其经理发送同行奖金或积极反馈,当他们注意到有人在模拟强大的安全实践时。由于安全工作有时会被忽视或对最终用户不可见,因此额外努力直接或与职业目标一致地予以认可有助于为更好的安全性设立积极激励。

独立于策略,如果组织尚未认为安全是核心价值和共同责任,就需要更基本的反思和讨论来证明安全和可靠性对组织的核心目标的重要性。

帮助用户安全地浏览网络

有效的安全性不应依赖于任何最终用户的专业知识。任何拥有大规模用户群的产品都需要仔细平衡可用性、功能和其他业务约束。在大多数情况下,Chrome 的目标是使安全性对用户几乎是不可见的:我们进行透明更新,我们倾向于安全默认设置,并且我们不断努力使安全决策成为易于决策的决策,并帮助用户避免不安全的决策。

在团队 v3.0 阶段,我们共同承认我们在可用安全方面存在一系列问题,这些问题源于人类与软件的互动。例如,我们知道用户正在成为社会工程和网络钓鱼攻击的受害者,我们对 Chrome 的安全警告效果表示担忧。我们想解决这些问题,但团队中的人性化软件专业知识有限。我们决定需要有策略地雇佣更多的可用安全专业知识,并与一个对新角色感兴趣的内部候选人偶然取得联系。

当时,这位候选人处于研究科学家的职业阶梯上,Chrome 在招聘方面没有先例。尽管最初有所保留,但我们说服了领导层雇佣这位候选人,强调候选人的学术专长和多元化观点实际上是团队的资产,并且对于增强其现有技能组是必要的。与用户体验(UX)团队密切合作,这位新加入我们团队的成员继续建立了 Chrome 可用安全的重点领域。最终,我们雇佣了更多的 UX 设计师和研究人员,帮助我们更深入地了解用户的安全和隐私需求。我们了解到,安全专家,由于他们对计算机系统和网络运作方式的高度理解,往往对用户面临的许多挑战视而不见。

速度至关重要

用户安全取决于快速检测安全漏洞并在攻击者利用之前向用户提供修复程序。Chrome 最重要的安全功能之一是快速自动更新。从早期开始,安全团队与技术项目经理(TPM)密切合作,他们建立了 Chrome 的发布流程并管理了每个新版本的质量和可靠性。发布 TPM 测量崩溃率,确保高优先级错误的及时修复,小心谨慎地推进发布,当工程师们进展太快时进行反对,并努力以合理的速度将可靠性或安全性改进的发布推送给用户。

早期,我们利用 Pwn2Own 和后来的 Pwnium 黑客大赛作为强制性手段,以查看我们是否能在 24 小时内发布和部署关键的安全修复。(我们可以。)这需要与发布 TPM 团队的强大合作伙伴关系和大量的帮助和支持,尽管我们证明了这种能力,但由于 Chrome 在深度防御方面的投资,我们很少需要使用它。

深度防御设计

无论团队有多快能够检测和修复 Chrome 中的任何单个安全漏洞,这些漏洞都可能发生,特别是考虑到 C++的安全缺陷和浏览器的复杂性。由于攻击者不断提升他们的能力,Chrome 不断投资于开发利用缓解技术和帮助避免单点故障的架构。团队创建了一个活跃的按风险分级的组件图,以便任何人都可以推理出 Chrome 的安全架构和各种防御层,以指导他们的工作。

实践中深度防御的最好例子之一是持续投资于沙盒能力。Chrome 最初采用了多进程架构和沙盒化的渲染器进程。这阻止了恶意网站接管用户整个计算机,这对于当时的浏览器架构来说是一个重大进步。在 2008 年,来自网络的最大威胁是恶意网页利用浏览器妥协在用户的机器上安装恶意软件,而 Chrome 的架构成功地解决了这个问题。

但是,随着计算机使用的发展,随着云计算和网络服务的普及,越来越多的敏感数据已经转移到了在线。这意味着跨网站数据窃取可能与本地机器的妥协一样重要。攻击重点从“渲染器妥协安装恶意软件”转移到“渲染器妥协窃取跨站点数据”并不清楚,但团队知道这样做的动机是存在的,使这种转变是不可避免的。有了这一认识,2012 年,团队启动了站点隔离项目,以提高沙盒化的状态以隔离各个站点。

最初,团队预测站点隔离项目需要一年才能完成,但我们的估计偏差超过了五倍!这样的估计错误往往会引起高层管理对项目的关注,这是有充分理由的。团队定期向领导和各利益相关者阐明了项目的深度防御动机、进展情况以及为什么比最初预期的工作量更大的原因。团队成员还展示了对整体 Chrome 代码健康的积极影响,这有利于 Chrome 的其他部分。所有这些都使团队有额外的保护来捍卫项目,并在多年内向高级利益相关者传达其价值,直到最终公开发布。(巧合的是,站点隔离部分缓解了推测执行漏洞,这些漏洞是在 2018 年发现的。)

由于深度防御工作不太可能导致用户可见的变化(如果做得正确),因此领导层更需要积极管理、认可和投资这些项目。

保持透明并与社区互动

透明度自一开始就是 Chrome 团队的核心价值观。我们不会淡化安全影响或用悄无声息的修复来掩盖漏洞,因为这样做对用户毫无益处。相反,我们为用户和管理员提供了他们需要准确评估风险的信息。安全团队发布了处理安全问题的方式,披露了 Chrome 及其依赖项中修复的所有漏洞,无论是内部发现还是外部发现,并且在可能的情况下,在发布说明中列出了每个已修复的安全问题。

除了漏洞,我们还与公众分享季度总结,并通过外部讨论邮件列表(security-dev@chromium.org)与用户互动,以便任何人都可以向我们发送想法、问题或参与正在进行的讨论。我们积极鼓励个人在会议或安全聚会上分享他们的工作,或通过他们的社交网络参与。我们还通过 Chrome 的漏洞奖励计划和安全会议赞助与更大的安全社区互动。Chrome 之所以更安全,要归功于许多不认为自己是团队一部分的人的贡献,我们尽力承认和奖励这些贡献,确保适当的归因并支付货币奖励。

在谷歌内部,我们组织了一年一度的团队外出会议,将 Chrome 安全团队与中央安全团队和其他嵌入式团队(例如,Android 的安全团队)联系起来。我们还鼓励谷歌内的安全爱好者在 Chrome 上做 20%的工作(反之亦然),或者找到与 Chromium 项目上的学术研究人员合作的机会。

在开放环境中工作使团队能够分享其工作、成就和想法,并获得反馈或在谷歌之外追求合作。所有这些有助于推动对浏览器和网络安全的共同理解。团队多年来努力增加 HTTPS 采用率的努力是如何沟通变化并与更大的社区合作可以导致生态系统变化的一个例子。

结论

Chrome 团队在项目的早期阶段确定安全性是核心原则,并随着团队和用户基数的增长扩大了投资和策略。Chrome 安全团队在浏览器推出仅一年后正式成立,随着时间的推移,其角色和责任变得更加明确定义。团队阐明了使命和一套核心安全原则,并为其工作建立了关键的重点领域。

将安全性作为团队责任,拥抱透明度,并与 Chrome 之外的社区互动有助于创建和推进以安全为中心的文化。追求快速创新导致了一个动态的产品,并有能力灵活应对不断变化的环境。深度防御设计有助于保护用户免受一次性错误和新型攻击。考虑安全的人类因素,从最终用户体验到招聘流程,帮助团队扩大对安全的理解并解决更复杂的挑战。愿意直面挑战并从错误中学习使团队能够努力使默认路径也成为安全路径。

¹ 谷歌在 2019 年初开源了其OSS-Fuzz 服务的模糊后端ClusterFuzz

第二十章:理解角色和责任

原文:20. Understanding Roles and Responsibilities

译者:飞龙

协议:CC BY-NC-SA 4.0

由 Heather Adkins,Cyrus Vesuna,Hunter King,Felix Gröbert 和 David Challoner 撰写

与 Susanne Landers,Steven Roddis,Sergey Simakov,Shylaja Nukala,Janet Vong,Douglas Colish,Betsy Beyer 和 Paul Blankinship 合作

正如本书多次强调的那样,构建系统是一个过程,而改进安全性和可靠性的过程依赖于人。这意味着构建安全可靠的系统涉及解决两个重要问题:

  • 谁负责组织的安全和可靠性?

  • 安全和可靠性工作如何整合到组织中?

这些问题的答案高度依赖于您组织的目标和文化(下一章的主题)。以下部分概述了如何思考这些问题的一些高层指导,并提供了谷歌多年来如何处理这些问题的见解。

谁负责安全和可靠性?

在一个组织中谁负责安全和可靠性?我们认为安全和可靠性应该整合到系统的生命周期中;因此,它们是每个人的责任。我们希望挑战这样一个神话,即组织应该把这些问题的负担完全放在专门的专家身上。

如果可靠性和安全性被委托给一个无法要求其他团队进行安全相关更改的孤立团队,同样的失败将反复发生。他们的任务可能开始感到像西西弗斯一样——重复而无效。

我们鼓励组织将可靠性和安全性作为每个人的责任:开发人员,SRE,安全工程师,测试工程师,技术负责人,经理,项目经理,技术作家,高管等等。这样,第四章中描述的非功能性需求将成为整个组织在整个系统生命周期中的关注焦点。

专家的角色

如果每个人都对安全和可靠性负责,那么您可能会想:安全专家或可靠性专家的角色究竟是什么?根据一种观点,构建特定系统的工程师应该主要关注其核心功能。例如,开发人员可能会专注于为基于手机的应用程序构建一组关键用户体验。在开发团队的工作之外,一个以安全为重点的工程师将从攻击者的角度审视该应用程序,以破坏其安全性。一个以可靠性为重点的工程师可以帮助理解依赖链,并根据这些依赖链确定应该测量哪些指标,这些指标将导致客户满意和符合 SLA 的系统。这种分工在许多开发环境中很常见,但重要的是这些角色应该共同合作,而不是孤立工作。

为了进一步扩展这个想法,根据系统的复杂性,组织可能需要具有专业经验的人员来做细微的判断。由于不可能构建一个绝对安全、能够抵御所有攻击的系统,或者一个完全可靠的系统,专家的建议可以帮助引导开发团队。理想情况下,这些指导应该整合到开发生命周期中。这种整合可以采取多种形式,安全和可靠性专家应该直接与开发人员或其他专家合作,以改进系统的每个阶段。¹ 例如,安全咨询可以在多个阶段进行:

  • *项目开始时的安全设计审查,以确定如何整合安全性

  • 持续的安全审计以确保产品按照安全规范正确构建

  • 测试以查看独立人员可以发现的漏洞

安全专家应负责实现需要专业知识的安全特定技术。加密是一个典型的例子:“不要自己编写加密”是一个常见的行业口头禅,旨在阻止有抱负的开发人员实现他们自己的解决方案。加密实现,无论是在库中还是在硬件中,都应该留给专家。如果你的组织需要提供安全服务(例如通过 HTTPS 的网络服务),请使用业界公认和经过验证的解决方案,而不是尝试编写自己的加密算法。专业的安全知识也可能需要实现其他类型的高度复杂的安全基础设施,例如自定义身份验证、授权和审计(AAA)系统,或者新的安全框架以防止常见的安全漏洞。

可靠性工程师(如 SRE)最适合开发集中式基础设施和全公司自动化。SRE 书的第七章讨论了水平解决方案的价值和演变,并展示了能够促进产品开发和发布的关键软件如何演变成平台。

最后,安全和可靠性专家可以制定适合你组织工作流程的最佳实践、政策和培训。这些工具应该赋予开发人员采用最佳实践和实现有效的安全和可靠性实践的能力。专家应该努力建立组织的知识智囊团,不断学习行业发展并提高更广泛的意识(参见“意识文化”)。通过创造意识,专家可以帮助组织以迭代的方式变得更加安全和可靠。例如,谷歌有针对 SRE 和安全的教育计划,为这些特定角色的新员工提供基本的知识水平。除了向全公司提供课程材料外,我们还为员工提供许多关于这些主题的自学课程。

了解安全专业知识

任何试图在组织中雇佣安全专业人员的人都知道这项任务可能具有挑战性。如果你自己不是安全专家,那么在雇佣安全专家时应该寻找什么?医学专业人员提供了一个很好的类比:大多数人对人类健康的基本原理有一般了解,但许多人在某个时候会专攻某个领域。在医学领域,家庭医生或全科医生通常负责初级护理,但更严重的疾病可能需要神经学、心脏病学或其他领域的专家。同样,所有安全专业人员通常掌握一般知识,但他们也倾向于在一些特定领域专攻。

在雇佣安全专家之前,了解组织需要的技能类型非常重要。如果你的组织很小,例如,如果你是一家初创公司或一个开源项目,一名综合性专家可能可以满足你的许多需求。随着组织的成长和发展,其安全挑战可能变得更加复杂,需要增加专业化。表 20-1 介绍了谷歌早期历史上需要相应安全专业知识的一些关键时刻。

表 20-1. Google 历史上关键时刻需要的安全专业知识

公司里程碑 需要的专业知识 安全挑战
Google 搜索(1998)Google 搜索为用户提供查找公开信息的能力。 一般 搜索查询日志数据保护
防止拒绝服务攻击
网络和系统安全
Google AdWords(2000)Google AdWords(Google Ads)使广告商能够在 Google 搜索和其他产品上展示广告。 一般
数据安全 金融数据保护
网络安全 法规合规
系统安全 复杂的网络应用程序
应用安全 身份
合规和审计 账户滥用
反欺诈 欺诈和内部滥用
隐私
拒绝服务
内部风险
博客(2003 年)博客是一个允许用户托管自己的网页的平台。 通用数据安全
网络安全 拒绝服务
系统安全 平台滥用
应用程序安全 复杂的网络应用程序
内容滥用
谷歌邮箱(Gmail)(2004 年)Gmail 是谷歌的免费网络邮件系统,高级功能可通过付费的 GSuite 账户获得。 通用隐私
数据安全 保护静态和传输中的高度敏感用户内容
网络安全 涉及高能力外部攻击者的威胁模型
系统安全 复杂的网络应用程序
应用程序安全 身份系统
密码学 账户滥用
反垃圾邮件 电子邮件垃圾邮件和滥用
反滥用 拒绝服务
事件响应 内部滥用
内部风险 企业需求
企业安全

认证和学术

一些安全专家寻求获得他们感兴趣领域的认证。全球各地的机构提供面向安全的行业认证,这些认证可以很好地表明一个人对于发展相关职业技能和学习关键概念的兴趣。这些认证通常涉及标准化的基于知识的测试。一些认证需要最少一定数量的课堂、会议或工作经验。几乎所有认证在一定时间后会过期,或要求持有人刷新最低要求。

这些标准化的测试机制不一定能证明某人在您组织的安全角色中取得成功的能力,因此我们建议采取一种平衡的方法来评估安全专家,综合考虑他们的所有资格:他们的实际经验、认证和个人兴趣。虽然认证可能表明某人通过考试的能力,但我们见过持有证书的专业人士在应用知识解决问题时遇到困难。与此同时,早期职业候选人或从其他专业角色转入该领域的候选人可能会利用认证快速提升他们的知识。对于对该领域有浓厚兴趣或在开源项目中有实际经验(而不是工作经验)的早期职业候选人来说,他们可能能够迅速增加价值。

由于安全专家的需求越来越大,许多行业和大学一直在开发和完善以安全为重点的学术项目。一些机构提供涵盖多个安全领域的通用安全学位。其他学位项目专注于特定的安全领域(这对博士生来说很常见),还有一些提供了关于网络安全问题和公共政策、法律和隐私等领域重叠课程的混合课程。与认证一样,我们建议在考虑候选人的学术成就时,要考虑他们的实际经验和您组织的需求。

例如,您可能希望首先聘请一名经验丰富的专业人士作为您的第一位安全招聘,并在团队建立并能够提供指导时聘请早期职业人才。另外,如果您的组织正在解决一个小众的技术问题(比如保护自动驾驶汽车),一位在特定研究领域有深入知识但工作经验较少的新博士毕业生可能会很好地适应这个角色。

将安全整合到组织中

知道何时开始从事安全工作更多的是一门艺术而不是科学。关于这个话题的意见很多,也各不相同。然而,可以肯定的是,你越早开始考虑安全,你就会越好。更具体地说,多年来我们观察到一些情况很可能会促使组织(包括我们自己)开始建立安全计划:

  • 当一个组织开始处理个人敏感数据,比如敏感用户活动日志、财务信息、健康记录或电子邮件时

  • 当一个组织需要建立高度安全的环境或定制技术,比如在网页浏览器中定制安全功能时

  • 当法规要求遵守标准(比如萨班斯-奥克斯法案、PCI DSS 或 GDPR)或相关审计时

  • 当一个组织与客户有合同要求,尤其是关于泄露通知或最低安全标准的要求时

  • 在妥协或数据泄露期间或之后(理想情况下是之前)

  • 作为对同行业内同行妥协的反应

总的来说,你会希望在满足这些条件之前,尤其是在数据泄露之前,就开始着手处理安全问题!在这样的事件发生之前实现安全措施要比之后简单得多。例如,如果你的公司计划推出一个接受在线支付的新产品,你可能需要考虑为该功能选择一个专业的供应商。审查供应商并确保他们有良好的数据处理实践将需要时间。

想象一下,你选择了一个集成在线支付系统不安全的供应商。数据泄露可能会导致法规罚款、客户信任的丧失,以及工程师重新实现系统的生产力损失。许多组织在此类事件后停止存在。

同样,想象一下,你的公司正在与一个有额外数据处理要求的合作伙伴签订新合同。假设,你的法律团队可能会建议你在签署合同之前实现这些要求。如果你延迟了额外的保护措施,并因此遭受了泄露,会发生什么?

在考虑安全计划的成本和公司可以投入该计划的资源时,通常会出现相关问题:实现安全的成本有多高,公司是否负担得起?虽然本章无法深入探讨这个非常复杂的话题,但我们将强调两个主要的要点。

首先,为了使安全有效,必须仔细平衡组织的其他要求。为了让这一准则更具体化,我们可以通过关闭数据中心、网络、计算设备等来使谷歌几乎 100%免受恶意行为的影响。虽然这样做可以实现高水平的安全性,但谷歌将不再拥有客户,并将消失在失败公司的历史中。可用性是安全的核心原则!为了制定合理的安全策略,你需要了解业务运营所需的内容(对于大多数公司来说,赚取利润所需的内容)。找到业务需求和充分的安全控制之间的平衡。

其次,安全是每个人的责任。你可以通过将一些安全流程分配给受影响最严重的团队来降低一些安全流程的成本。例如,考虑一个公司有六个产品,每个产品都有一个产品团队,并由 20 个防火墙保护。在这种情况下,一个常见的方法是让一个中央安全团队维护所有 120 个防火墙的配置。这种设置要求安全团队对六种不同产品有广泛的了解——这是导致最终可靠性问题或系统变更延迟的原因,所有这些都会增加你的安全计划的成本。另一种方法是将操作自动配置系统的责任分配给安全团队,该系统接受、验证、批准并推送六个产品团队提出的防火墙变更。这样,每个产品团队都可以有效地提出小的变更供审查,并扩展配置过程。这种优化可以节省时间,甚至通过在人为参与之前及时发现错误来提高系统的可靠性。

由于安全是组织生命周期的一个重要组成部分,组织的非技术领域也需要早期考虑安全。例如,董事会经常审查他们监督的实体的安全和隐私实践。在数据泄露后的诉讼中,比如 2017 年对雅虎的股东诉讼,正在推动这一趋势。在准备安全路线图时,一定要考虑这些类型的利益相关者在您的流程中。

最后,重要的是建立维持对当前问题及其优先级的持续理解的流程。当安全被视为一个持续的过程时,就需要对企业面临的风险进行持续评估。为了随着时间的推移迭代深度防御安全控制,您需要将风险评估纳入软件开发生命周期和安全实践中。接下来的部分将讨论一些实际的策略。

嵌入安全专家和安全团队

多年来,我们看到许多公司在组织内部的哪里嵌入安全专家和安全团队进行了实验。这些配置范围从完全嵌入产品团队内的安全专家(参见第十九章)到完全集中的安全团队。谷歌的中央安全团队在组织上配置为这两种选项的混合体。

许多公司在决策方面也有不同的问责安排。我们看到首席信息安全官(CISO)和其他负责安全的领导角色向几乎每个 C 级高管汇报:CEO、CFO、CIO 或 COO、公司的总法律顾问、工程副总裁,甚至 CSO(首席安全官,通常也负责物理安全)。没有正确或错误的配置,您的组织所做的选择将高度依赖于对您的安全工作最有效的因素。

本节的其余部分提供了多年来我们成功使用的配置选项的一些细节。虽然我们是一家大型科技公司,但其中许多组件在中小型组织中也同样有效。但是,我们想象这种配置可能不适用于金融公司或公用事业公司,那里对安全的问责可能有不同的利益相关者和驱动因素。您的情况可能有所不同。

示例:在谷歌嵌入安全

在谷歌,我们首先建立了一个作为产品工程同行的中央安全组织。该组织的负责人是工程部门的高级领导(副总裁)。这创造了一个报告结构,其中安全被视为工程的盟友,但也允许安全团队有足够的独立性来提出问题并解决冲突,而不会受到其他领导可能存在的利益冲突的影响。这类似于谷歌的 SRE 组织与产品开发团队保持独立的报告链的方式。通过这种方式,我们建立了一个专注于改进的开放和透明的参与模型。否则,您可能会面临以下特点的团队:

  • 由于发布被过度优先考虑,无法提出严重问题

  • 被视为需要通过沉默发布来规避的阻碍门

  • 由于文档不足或代码访问受限而放缓

谷歌的中央安全团队依赖于标准流程,如工单系统,与组织的其他部门进行交互,当团队需要请求设计审查、访问控制变更等。要了解这个工作流程的运作方式,请参见“谷歌的智能引入系统”。

随着谷歌的发展,将“安全冠军”嵌入到各个产品工程对等组中也变得很有用。安全冠军成为促进中央安全团队和产品团队合作的门户。在开始时,这个角色非常适合在组织中地位良好并对安全感兴趣或有安全背景的高级工程师。这些工程师还成为产品安全倡议的技术负责人。随着产品团队变得更加复杂,这个角色被分配给高级决策者,如董事或副总裁,这个人可以做出艰难的决定(比如在发布和安全修复之间取得平衡),获取资源并解决冲突。

在安全冠军模型中,建立和达成参与流程和责任非常重要。例如,中央团队可能会继续进行设计审查和审核,制定组织范围的安全政策和标准,构建安全的应用程序框架(见第十三章),并设计最小权限方法等共同基础设施(见第五章)。分布式安全冠军是这些活动的关键利益相关者,并应帮助决定这些控制在其产品团队中的工作方式。安全冠军还推动其各自产品团队内的政策、框架、基础设施和方法的实现。这种组织配置需要通过团队宪章、跨职能会议、邮件列表、聊天频道等建立紧密的沟通循环。

由于谷歌和 Alphabet 规模庞大,除了中央安全团队和分布式安全冠军外,我们还为更复杂的产品设立了特殊的分散安全团队。例如,Android 安全团队位于 Android 工程组织内。Chrome 也有类似的模式(见第十九章)。这意味着 Android 和 Chrome 安全团队负责其各自产品的端到端安全,包括决定产品特定的标准、框架、基础设施和方法。这些专门的安全团队运行产品安全审查流程,并有特殊的程序来加固产品。例如,Android 安全团队已经努力加固了媒体堆栈,并从集成的安全和工程方法中受益。

在所有这些模型中,安全团队的开放和可接近性非常重要。除了安全审查流程外,在项目生命周期中,开发人员需要及时和一致的安全相关问题反馈,以便从专业人员那里获得帮助。我们在第二十一章中解决了许多关于这些互动的文化问题。

特殊团队:蓝队和红队

安全团队通常使用颜色来标记他们在保护组织中的角色。所有这些以颜色编码的团队都致力于共同的目标,即改善公司的安全姿态。

蓝队主要负责评估和加固软件和基础设施。他们还负责在成功攻击发生时进行检测、封锁和恢复。蓝队成员可以是组织中任何一个致力于保卫组织的人,包括构建安全可靠系统的人。

红队进行攻击性安全演练:模拟真实对手的端到端攻击。这些演练揭示了组织防御的弱点,并测试其检测和防御攻击的能力。

通常,红队关注以下内容:

特定目标

例如,红队可能试图利用客户账户数据(或更具体地说,找到并将客户账户数据外泄到一个安全的目的地)。这些练习与对手的操作方式非常相似。

监视

目的是确定你的检测方法是否能够检测到对手的侦察行为。监视也可以作为未来目标导向参与的地图。

定向攻击

目的是证明那些据称是理论的、极不可能被利用的安全问题的利用是可行的。因此,你可以确定哪些问题值得建立防御。

在开始红队计划之前,一定要确保获得可能受到这些练习影响的组织部分的支持,包括法律和高管。这也是定义边界的好时机——例如,红队不应访问客户数据或干扰生产服务,并且他们应该使用数据窃取和服务中断的近似值(例如,只通过破坏测试账户的数据)。这些边界需要在进行真实练习和建立合作团队可以接受的时间和范围之间取得平衡。当然,你的对手不会尊重这些边界,所以红队应该特别注意那些没有得到很好保护的关键领域。

一些红队会与蓝队分享他们的攻击计划,并与他们密切合作,以快速全面地了解检测情况。这种关系甚至可以通过一个紫队来正式化,桥接这两者。如果你正在进行许多练习并希望快速行动,或者想要将红队活动分配给产品工程师,这可能会很有用。这种配置也可以激发红队去寻找他们可能不会考虑的地方。设计、实现和维护系统的工程师最了解系统,并且通常对系统的弱点有直观的认识。

红队不是漏洞扫描或渗透测试团队。漏洞扫描团队寻找软件和配置中可预测和已知的弱点,可以自动扫描。渗透测试团队更专注于发现大量的漏洞和测试人员试图利用它们。他们的范围更窄,专注于特定产品、基础设施组件或流程。由于这些团队主要测试组织安全防御的预防和一些检测方面,他们的典型参与持续几天。

相比之下,红队的参与是目标导向的,通常持续几周。他们的目标是特定的目标,比如知识产权或客户数据的外泄。他们的范围广泛,使用任何必要手段来实现他们的目标(在安全限制内),穿越产品、基础设施和内部/外部边界。

在一定时间内,优秀的红队可以实现他们的目标,通常不会被发现。与其将成功的红队攻击视为对糟糕或无效的业务部门的评判,不如利用这些信息以无过失的方式更好地了解一些更复杂的系统。利用红队练习的机会更好地了解这些系统是如何相互连接和共享信任边界的。红队旨在帮助加强威胁模型并建立防御。

注意

由于他们并不完全模仿外部攻击者的行为,红队攻击并不是对你的检测和响应能力的完美测试。特别是如果红队由对系统已有知识的内部工程师组成,这一点尤为真实,因为他们试图渗透的系统。

您也不可能频繁地进行红队攻击,以提供对攻击的实时视图或对检测和响应团队的统计显著指标。红队旨在发现正常测试无法发现的罕见边缘情况。尽管存在所有的警告,定期进行红队演习是了解您的安全姿态的一种好方法。

您还可以利用红队来教导设计、实现和维护系统的人员对抗敌对心态。直接将这些人员嵌入攻击团队,例如通过一个小范围的项目,将使他们第一手了解攻击者如何审查系统可能存在的漏洞并绕过防御。他们可以在以后将这些知识注入团队的开发过程中。

与红队合作有助于更好地了解您的组织安全姿态,并制定实现有意义的风险降低项目的路线图。通过了解您当前的风险容忍度的影响,您可以确定是否需要进行调整。

外部研究人员

检查和改进您的安全姿态的另一种方法是与发现系统漏洞的外部研究人员和爱好者密切合作。正如我们在第二章中提到的,这是了解您的系统的一种有用方式。

许多公司通过建立“漏洞奖励计划(VRPs)”与外部研究人员合作,也俗称为“漏洞赏金计划”。这些计划提供奖励,以换取对系统漏洞的负责披露,这些奖励可能是现金形式,也可能不是。⁹ Google 的第一个 VRP 始于 2006 年,提供了一件 T 恤和一个简单的感谢信息放在我们的公共网页上。通过奖励计划,您可以扩大对安全相关漏洞的搜索范围,超出您的直接组织,并与更多的安全研究人员合作。

在开始 VRP 之前,首先了解找到和解决常规安全问题的基础知识,通过彻底的审查和基本的漏洞扫描可以找到。否则,您最终会支付外部人员来发现您自己的团队本来可以轻松发现的漏洞。这不是 VRP 的预期目的。它还有一个缺点,即可能有多个研究人员向您报告同一个问题。

了解如何建立漏洞赏金计划需要一些前期工作。如果选择运行漏洞赏金计划,可以按照以下基本步骤进行:

  1. 确定您的组织是否准备好进行这项计划。

  2. 确定系统的目标范围。例如,您可能无法针对公司系统。

  3. 确定支付水平并留出资金用于支付。¹⁰

  4. 考虑是否要运行内部漏洞赏金计划或者雇佣专门从事这些计划的组织。

  5. 如果要自己运行,建立漏洞接收、分类、调查、验证、跟进和修复的流程。根据我们的经验,您可以估计这个过程大约需要 40 个小时来处理每个严重的问题,不包括修复。

  6. 制定付款流程。记住,报告者可能来自世界各地,而不仅仅是您的本国。您需要与您的法律和财务团队合作,了解您的组织可能存在的任何限制。

  7. 启动,学习和迭代。

每个漏洞赏金计划都面临一些可能的挑战,包括以下内容:

需要调整被报告问题的火管

根据您的行业声誉、攻击面、支付金额和发现漏洞的难易程度,您可能会收到大量报告。事先了解您的组织需要做出怎样的响应。

报告质量差

如果大多数工程师都在追踪基本问题或非问题,那么赏金计划可能会变得繁重。我们发现这对于 Web 服务尤其如此,因为许多用户配置错误的浏览器并且“发现”了实际上并不是 bug 的 bug。安全研究人员不太可能出现在这个范围内,但有时很难事先辨别 bug 报告者的资格。

语言障碍

漏洞研究人员不一定会用您的母语向您报告 bug。语言翻译工具在这里可能会有所帮助,或者您的组织可能有人了解报告者使用的语言。

漏洞披露指南

披露漏洞的规则并没有得到普遍认同。研究人员应该公开他们所知道的吗,如果是的话,何时?研究人员应该给您的组织多长时间来修复 bug?哪些类型的发现会得到奖励,哪些类型不会?在这里有许多不同意见的“正确”方法。以下是一些进一步阅读的建议:

  • 谷歌安全团队已经撰写了一篇关于负责任披露的博客文章。¹¹

  • Project Zero 是谷歌内部的漏洞研究团队,他们也撰写了一篇关于数据驱动披露政策更新的博客文章

  • 奥卢大学安全编程小组提供了一系列有用的漏洞披露出版物

  • 国际标准化组织(ISO)提供了漏洞披露的建议。

准备好及时解决研究人员向您报告的问题。还要意识到他们可能发现已被恶意行为者积极利用了一段时间的问题,在这种情况下,您可能还需要解决安全漏洞。

结论

安全和可靠性是由流程和实践的质量或缺失所创造的。人是这些流程和实践最重要的推动者。有效的员工能够跨角色、部门和文化边界进行协作。我们生活在一个未知未来且对手不可预测的世界。在一天结束时,确保您组织中的每个人都对安全和可靠性负责是最好的防御!

¹ 在 SRE 工作手册的第十八章中描述的发展轨迹展示了经验丰富的 SRE 在整个产品生命周期中提供的价值。

² 2002 年的萨班斯-奥克斯法案(公众公司会计改革和投资者保护法)为美国上市公司设定了有关其会计实践的标准,并包括信息安全主题。支付卡行业数据安全标准为保护信用卡信息设定了最低指导方针;任何进行此类支付处理的人都需要遵守合规要求。《通用数据保护条例》是一项关于个人数据处理的欧盟法规。

³ 一些知名的组织在遭受攻击后破产或申请破产,比如 Code Spaces 和美国医疗收费机构。

⁴ 在美国,高管和董事会越来越被迫对其组织的安全负责。普渡大学康科德法学院撰写了一篇关于这一趋势的好文章

⁵ 参见 SRE 书中的第 31 章。

⁶ 这种颜色方案源自美国军方。

⁷ 欲了解更多有关紫队的信息,请参阅 Brotherston, Lee, 和 Amanda Berlin 所著的 2017 年作品Defensive Security Handbook: Best Practices for Securing Infrastructure。Sebastopol, CA: O’Reilly Media。

⁸ 通过在无过失的事后分析文化基础上建立,如SRE 书籍第十五章所述。

⁹ 请参阅谷歌研究员 sirdarckcat 的有关奖励的博客文章,以获得更多哲学观点。

¹⁰ 欲进一步阅读,请参阅 sirdarckcat 关于漏洞定价的文章。

¹¹ sirdarckcat 还撰写了一篇关于漏洞披露的文章。

第二十一章:建立安全和可靠性文化

原文:21. Building a Culture of Security and Reliability

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Heather Adkins

与 Peter Valchev,Felix Gröbert,Ana Oprea,Sergey Simakov,Douglas Colish 和 Betsy Beyer 一起

当组织围绕这些基本原则建立文化时,有效的安全和可靠性就会蓬勃发展。明确设计、实现和维护他们希望体现的文化的组织通过使文化成为团队努力的一部分——从 CEO 及其领导团队,到技术领导和经理,再到设计、实现和维护系统的人员,取得成功。

想象一下这种情况:就在上周,CEO 告诉整个组织,获得下一个重大交易对公司的未来至关重要。今天下午,你发现公司系统中有攻击者的证据,你知道这些系统将不得不被下线。客户将会生气,重大交易可能会受到威胁。你也知道你的团队可能会因为上个月没有应用安全补丁而受到责备,但很多人都在度假,每个人都在为重大交易的紧迫期限而努力。在这种情况下,你的公司文化支持员工做出什么样的决定?一个具有强大安全文化的健康组织会鼓励员工立即报告事故,尽管这可能会延误重大交易。

假设与此同时,你正在调查恶意闯入者,前端开发团队意外地将一个重大更改推送到了实时生产系统。这个错误导致公司的收入流下线了一个多小时,客户不断打爆了支持热线。你的客户基础的信任正在迅速侵蚀。一个可靠性文化会鼓励员工重新设计允许意外前端推送的流程,以便团队可以管理客户的需求和错过或延误重大交易的风险。

在这些情况下,文化规范应该鼓励无过失的事后分析,以发现可以修复的失败模式,从而避免将来出现有害条件。健康文化的公司知道,被黑客入侵一次是非常痛苦的,但被黑客入侵两次甚至更糟。同样,他们知道 100%永远不是正确的可靠性目标;使用诸如错误预算之类的工具以及围绕安全代码推送的控制可以通过在可靠性和速度之间取得正确的平衡来让用户满意。最后,具有健康安全和可靠性文化的公司知道,从长远来看,客户在不可避免地发生事故时会欣赏透明度,并且隐藏此类事故可能会侵蚀用户的信任。

本章描述了建立安全和可靠性文化的一些模式和反模式。虽然我们希望这些信息对各种规模和形式的组织都有所帮助,但文化是组织的独特元素,在其特定挑战和特征的背景下塑造。没有两个组织会有相同的文化,我们在这里提供的建议也不一定适用于所有人。这一章旨在提供关于文化主题的一系列想法,但在现实世界的情况下,我们提出的有些理想化观点可能并不完全实用。即使在谷歌,我们也并不总是能够做到文化正确,我们不断努力改进现状。我们希望在这里提出的广泛观点和选择中,你能找到一些适合你的环境的观点。

定义健康的安全和可靠性文化

与健康的系统一样,健康的团队文化可以被明确设计、实现和维护。在本书中,我们专注于构建健康系统的技术和流程组件。构建健康文化的设计原则也存在。事实上,文化是设计、实现和维护安全和可靠系统的核心组成部分。

默认安全和可靠性文化

正如我们在第四章中讨论的,往往会有延迟考虑安全性和可靠性直到项目生命周期的后期阶段。这种推迟似乎会加速初始速度,但却以持续速度和可能增加的改装成本为代价。随着时间的推移,这些改装可能会增加技术债务或应用不一致,导致失败。为了说明这一点,想象一下购买汽车时,您必须单独寻找安全带供应商、汽车挡风玻璃安全性评估人员和验证气囊的检查员。只有在制造汽车之后才解决安全和可靠性问题会给消费者带来很大负担,消费者可能无法评估实现的解决方案是否足够。这也可能导致任何两辆制造的汽车之间的不一致做法。

这个类比反映了系统需要默认安全和可靠。当安全和可靠性选择贯穿项目的整个生命周期时,更容易保持一致性。此外,当这些成为系统的一部分时,它们对消费者来说可以变得不可见。回到汽车的类比:消费者不需要过多考虑安全机制,如安全带、挡风玻璃或后视摄像头,就可以相信它们会做正确的事情。

拥有健康的默认安全和可靠性文化的组织鼓励员工在项目生命周期的早期阶段(例如,在设计阶段)和每个实现迭代期间讨论这些话题。随着产品的成熟,它们的安全性和可靠性也将自然成熟;这已被纳入软件开发生命周期中。

这种文化使得设计、维护和实现系统自动和透明地融入安全和可靠性主题变得更容易。例如,您可以引入连续构建、消毒剂、漏洞发现和测试的自动化。应用程序框架和通用库可以帮助开发人员避免常见的跨站脚本(XSS)和 SQL 注入等漏洞。关于选择适当的编程语言或编程语言特性的指导可以帮助避免内存损坏错误。这种自动安全旨在减少摩擦(例如缓慢的代码审查)和错误(审查时未发现的错误),并且对开发人员来说应该相对透明。随着系统在这些安全和可靠性结构中的成熟,理想情况下,员工将越来越信任这些实现。

我们在第 12、13 和 19 章提供了一些关于在谷歌创建默认安全和可靠性文化的见解。

审查文化

当一个强大的审查文化存在时,每个人都被鼓励提前考虑他们在批准变更中的角色。这可以通过确保变更考虑到这些特殊情况来增强系统的持续安全和可靠性属性。同行审查可确保系统的安全和可靠性特性适用于各种变更场景,例如:

  • 多方授权审查以维护最小特权(见第五章)

  • 同行审查以确保代码更改是适当的和高质量的,包括安全性和可靠性考虑

  • 在将配置更改推送到生产系统之前进行同行审查

建立这样的文化需要组织内对审查的价值以及如何进行审查有广泛的理解。

变更审查实践应该被记录下来,以明确期望在审查期间会发生什么。例如,对于同行代码审查,您可以记录组织与代码审查相关的工程实践,并教育所有新开发人员了解这些期望。当要求同行审查多方授权方案时,记录何时将授予访问权限以及在什么条件下可能会被拒绝。这在组织内建立了一套共同的文化期望,因此只有有效的批准请求才会成功。同样,您应该设定期望,如果批准者拒绝请求,原因是可以理解的,基于记录的政策,以避免人与人之间的矛盾和产生“我们与他们”的心态。

与文档的相关性是教育审查者,使他们了解成为审查者的基本期望。这种教育可以在组织或项目的入职期间早期进行。考虑通过见习计划来培养新的审查者,其中更有经验或校准的审查者也会审查变更。

审查文化要求每个人都参与审查流程。虽然所有者负责确保其各自领域的整体方向和标准,但他们也应该个别对他们发起的变更负责。没有人应该能够选择退出审查,仅仅因为他们担任高级职位或不想参与。代码树所有者不免于对其代码和配置更改进行审查。同样,系统所有者也不能豁免参与多方登录授权的审批。

确保审查者具有做出决策所需的上下文,并且可以拒绝或重定向审查,如果他们缺乏足够的上下文来准确评估变更是否安全。当审查安全性和可靠性属性时,这一点尤为重要,例如多方授权的访问请求,或者具有安全影响的代码片段的更改。如果审查者不熟悉要注意的陷阱,那么审查将不起到足够的控制作用。自动化检查可以帮助建立这种上下文。例如,在第十三章中,我们讨论了 Google 如何在代码更改的预提交阶段使用 Tricorder 自动提出开发人员和审查者的安全问题。

意识文化

当组织成员意识到他们有安全和可靠性责任,并知道如何履行这些责任时,他们可以有效地实现良好的结果。例如,工程师可能需要采取额外措施来保护他们的帐户安全,因为他们访问敏感系统。那些工作需要频繁与外部方进行沟通的人可能会收到更多的钓鱼邮件。高管在前往世界某些地区时可能面临更高的风险。具有健康文化的组织会增强对这些条件的意识,并通过教育项目加以强化。

意识和教育策略对于建立强大的安全文化至关重要。这些倡议应该力求轻松愉快,以便学习者对内容感兴趣。人们对不同类型的信息的保留速度不同,这取决于信息的传达方式、他们对材料的现有熟悉程度,甚至个人因素,如年龄和背景。根据我们的经验,许多学习者通过动手实验等互动学习方法的保留率要高于通过观看视频等被动学习方法。在建立意识时,通过仔细考虑你希望人们保留的信息类型以及你希望他们如何学习,来优化最佳学习体验。

谷歌采取了多种方法来教育员工有关安全和可靠性。在广泛范围内,我们为所有员工提供强制性的年度培训。然后,我们通过专门为某些角色设计的专业计划来强化这些信息。以下是我们多年来在谷歌实现这些计划时发现有用的一些策略:

互动讲话

鼓励观众参与的讲话可以是以引人入胜的方式传达复杂信息的一种方式。例如,在谷歌,分享重大安全和可靠性事件的主要根本原因和缓解措施有助于我们的员工更好地理解为什么我们关注这些主题。我们发现这些类型的互动讨论也鼓励人们提出他们发现的问题,从工作站上的可疑活动到可能使系统崩溃的错误代码。这种做法有助于让人们感到自己是使组织更加可靠和安全的团队的一部分。

游戏

将安全和可靠性变成游戏是建立意识的另一种方式。这些方法往往更有效地适用于较大的组织,这些组织可能更能够在何时接受培训和是否可以重新接受培训方面给予玩家灵活性。我们的XSS 游戏(如图 21-1 所示)在教导开发人员有关这种常见的 Web 应用程序漏洞方面取得了相当大的成功。

安全培训游戏

图 21-1:安全培训游戏

参考文档

阅读文档的保留率可能比动手练习等方法低,但我们发现为开发人员提供强大的可供参考的文档非常重要。正如我们在第十二章中所指出的,参考文档很重要,因为很难同时记住安全和可靠性的许多微妙之处。对于常见的安全问题的指导,谷歌维护了一套内部安全最佳实践,工程师可以搜索答案以解决问题。⁵ 所有文档都应该有明确的所有权,并在不再相关时进行更新或废弃。

意识宣传

通知开发人员有关最近的安全和可靠性问题和发展可能是困难的。为了解决这个问题,谷歌每周以一页的格式发布工程建议(图 21-2 显示了一个例子)。这些“测试卫生间”剧集分发到谷歌所有办公室的洗手间。虽然最初旨在改善测试,但该计划偶尔也涉及安全和可靠性主题。在一个无法忽视的位置张贴传单是提供提示和提供灵感的好方法。⁶

一个测试卫生间的剧集

图 21-2:一个测试卫生间的剧集

及时通知

特别是在程序步骤中(比如检查代码或升级软件),您可能希望提醒人们遵循良好的安全和可靠性实践。在这些情况下向人们显示及时通知可以帮助他们做出更好的风险决策。在谷歌,我们已经尝试在关键时刻向员工显示弹出窗口和提示,例如当员工从不受信任的存储库升级软件或尝试将敏感数据上传到未经批准的云存储系统时。当用户在关键时刻看到警报时,他们可以为自己做出更好的决策,避免错误。另外,正如我们在第十三章中讨论的那样,在开发代码时向开发人员提供预提交的安全和可靠性提示有助于他们做出更好的选择。

肯定文化

随着时间的推移,组织可能会形成保守的风险文化,特别是如果安全漏洞或可靠性问题导致收入损失或其他不良结果。在极端情况下,这种心态可能导致“否定文化”:倾向于避免风险变化和可能带来的负面后果。当以安全或可靠性的名义延续时,“否定文化”可能导致组织停滞甚至无法创新。我们发现健康的组织有一种方式来应对在利用机会时需要一定风险的挑战,即有意识地冒险。要以这种方式拥抱风险,通常需要能够评估和衡量风险。

作为一个具体的例子,在第八章中,我们描述了用于保护谷歌应用引擎的方法,这是一个提议运行第三方未经验证代码的平台。在这种情况下,谷歌的安全团队可能会认为这次发布过于冒险。毕竟,运行任意不受信任的代码是一个相当众所周知的安全风险。例如,您不知道管理代码的第三方是否可能是恶意的,并尝试逃离平台的执行环境并危害您的基础设施。为了解决这个风险,我们展开了一项雄心勃勃的产品和安全团队之间的合作,制定了一个分层、加固的系统,这使我们能够推出一个在其他情况下看起来太危险的产品。这种合作使得随着时间的推移更容易在平台中构建额外的安全性,因为团队之间建立了信任基础。

另一种拥抱风险的方法是使用错误预算,它允许在一定限制内发生故障。一旦组织达到预定的最大限制,团队就应该合作减少风险至正常水平。由于错误预算在产品的整个生命周期中始终强调安全和可靠性,创新者有自由引入一定数量的风险变化。

不可避免的文化

没有系统是完美的,任何系统最终都可能失败。在某个时候,您的组织可能会经历服务中断或安全事件。接受这种不可避免性可以帮助团队有适当的心态来构建安全可靠的系统并应对失败。在谷歌,我们假设失败可能随时发生,这不是因为我们不积极采取预防措施或因为我们对系统缺乏信心,而是因为我们知道现实世界的系统永远无法百分之百安全可靠。

第十六章讨论了为不可避免的情况做准备的必要性。接受不可避免文化的团队会花时间准备灾难,以便能够有效地做出反应。他们公开讨论可能的失败,并留出时间来模拟这些情景。只有经常使用事故应对技能才有效,因此使用桌面练习、红队攻击、实际恢复测试和灾难角色扮演等练习来测试和完善组织的流程是个好主意。接受不可避免的组织也研究任何发生的失败,包括在同行群体内部。在内部,他们使用无过失的事后分析来减少失败的恐惧,并建立重复事件不太可能发生的信心。他们还利用其他组织发布的事后行动报告,无论是在其行业内部还是外部,这些报告提供了对可能与组织相关的失败情景的更广泛理解。

可持续文化

为了长期保持系统的可靠性和安全性特性,您的组织必须确保持续努力改进,并投入足够的资源(人员和时间)来完成这项任务。可持续性要求建立处理中断、安全事件和其他紧急情况的手段,并使用明确定义的流程。

为了保持这种努力,团队必须能够平衡在应对性工作和长期投资之间所花费的时间。回想一下我们在第十七章中提到的加利福尼亚州林业和消防局的例子,有效的团队将艰苦的工作分摊到许多人的肩上,以免任何一个人承担过多的责任。

拥有“可持续文化”的组织衡量处理运营工作(例如事故应对)所需的工作量,以及随着时间推移所需的改进投资。他们在规划中考虑压力、倦怠和士气,通过优先级确定足够的资源来支持长期努力或推迟必要的工作。他们通过建立可重复和可预测的应对紧急情况的流程,并定期轮换处理紧急情况的员工,避免了英雄主义的需要。他们还通过提出个人关注并持续激励人们来主动处理士气问题。

拥有可持续文化的组织也意味着知道有时特殊情况可能会导致暂时偏离预期工作量,并具有处理这些偏差的良好流程。例如,如果多个业务关键系统长时间未达到其 SLO,或者严重的安全漏洞导致了非同寻常的应对工作,您可能需要“全员上阵”来使事情重新回到正轨。在这段时间内,团队可能完全致力于运营工作和提高安全性或可靠性。虽然您可能不得不推迟所有其他组织工作,但您也可能不得不偏离最佳实践。

在健康的组织中,这种正常业务运营的特殊干扰应该是罕见的。在处理这些情况时,以下考虑因素可以帮助在情况解决后保持可持续文化:

  • 在正常运营之外操作时,一定要澄清情况是暂时的。例如,如果你要求所有开发人员手动监督他们推送到生产环境的更改(而不仅仅使用自动化系统),这可能会在长期内造成很多辛苦和不快乐。明确表示你期望情况很快恢复正常,并给出何时会发生的想法。

  • 有一个专门的待命小组,他们了解风险情况并有权迅速做出决策。例如,这个小组可能会对标准的安全和可靠性程序进行例外授权。这将减少执行中的摩擦,同时给组织一些保证,即安全机制仍然存在。有一种方法来标记你不得不绕过或推翻最佳实践的时候,并确保以后解决这些一次性事件。

  • 活动结束后,一定要确保你的事后审查了可能导致紧急情况的奖励制度。有时,像优先考虑功能发布而不是可靠性或安全功能的文化问题可能会导致技术债务的积累。积极解决这些问题将有助于组织恢复到可持续的节奏。

通过良好实践改变文化

影响组织文化可能很困难,特别是如果你正在处理的团队或项目已经很成熟。组织通常希望进行安全和可靠性改进,但发现文化障碍阻碍了前进。文化的反生产存在很多原因:领导方法、资源匮乏等。

对于改变的抵制的一个常见因素——安全和可靠性改进所必需的改变——是恐惧。改变可能会引发混乱、更大的摩擦、生产力和控制的丧失,以及风险的增加。特别是,摩擦的话题经常与新的可靠性和安全控制相关联。新的访问检查、流程和程序可能被解释为干扰开发人员或运营生产力。当组织面临紧迫的截止日期和高期望交付,无论是自我设定的还是由管理层推动的,对这些新控制的恐惧可能加剧担忧。然而,我们认为,安全和可靠性改进必须造成摩擦的信念是一个谬论。如果你在实现改变时考虑了某些文化因素,我们相信这些改变实际上可以改善每个人的体验。

本节讨论了一些技术策略,介绍了一些即使在最困难的文化中也可能有用的改变。你可能不是你组织中的 CEO 或领导,但每个开发人员、SRE 和安全专业人员都是他们自己影响领域中的变革工具。通过有意识地选择你设计、实现和维护系统的方式,有可能对你组织的文化产生积极影响;通过选择某些策略,你可能会发现随着时间的推移,你可以通过建立信任和善意来扭转局势。

我们在本节中给出的建议是基于这个目标的——但文化是经过长时间发展的,并且高度依赖于涉及的人和情况。当你在你的组织中尝试这里概述的一些策略时,你可能会发现它们只能取得有限的成功,有些策略可能根本行不通。有些文化是静态的,抵制改变。然而,拥有一个重视安全和可靠性的健康文化和你设计、实现和维护系统一样重要,所以你的努力可能并不总是奏效不应该阻止你尝试。

对齐项目目标和参与者激励

建立信任需要艰苦的工作,但失去信任却很容易。为了让设计、实现和维护系统的人能够跨越多个角色进行合作,他们需要共享一个共同的奖励体系。

在技术层面上,项目的可靠性和安全性可以通过可观察的指标(如SLOs和威胁建模)定期评估(例如,参见第二章和第十四章)。在流程和人员层面上,你应该确保职业晋升机会奖励安全性和可靠性。理想情况下,个人应该根据高层次的文件化期望进行评估。这些不应该只是一组要勾选的复选框,它们应该突出个人应该努力达到的主题和目标。例如,谷歌的入门级软件工程师职位阶梯规定工程师应该掌握至少一项核心编码之外的常见技能,比如为他们的服务添加监控或编写安全测试。

将项目目标与组织战略保持一致,而不调整参与者的激励可能会导致一种不友好的文化,即致力于改善产品安全性和可靠性的人并不是那些往往会得到晋升的人。由于财务奖励通常与资历相关,通过将项目激励与奖励制度保持一致,可以让为用户带来快乐的员工保持快乐。

通过风险减少机制减少恐惧

你是否曾经发现自己想要进行重大改变,比如推出新软件或新控制,却发现组织因为感知到的风险而反对?通过做出良好的部署选择,你可以激发组织的信心。我们在第七章中讨论了许多这些概念,但在这里特别值得注意的是文化的影响。以下是一些你可能想尝试的策略:

金丝雀和分阶段的推出

通过小金丝雀用户组或系统缓慢推出重大改变,可以减少恐惧。这样,如果出了什么问题,一个不幸的改变的影响范围就会很小。还要考虑更进一步,通过分阶段的推出和金丝雀来实现所有改变(参见SRE 工作手册中的第十六章)。在实践中,这种方法有许多好处。例如,在第十九章中,我们讨论了 Chrome 的分阶段发布周期如何平衡快速更新和可靠性的竞争需求。随着时间的推移,Chrome 的分阶段发布已经培养了它作为一个安全浏览器的声誉。我们还发现,通过将分阶段的推出作为例行改变流程的一部分,随着时间的推移,组织会期望对所有改变都应用谨慎和细心,这增强了对改变的信心并减少了恐惧。

自用

通过向用户展示你不害怕自己的改变,你可以增强对任何特定改变的稳定性和生产力影响的信心。自用(或“吃自己的狗粮”)意味着在改变影响他人之前先采用改变。如果你正在影响影响人们日常生活的系统和流程,这一点尤为重要。例如,如果你正在推出新的最低特权机制,比如多因素授权,先在自己的团队内采用更严格的控制,然后再要求所有员工实现这一改变。在谷歌,我们在推出新的端点安全软件之前,首先在一些最具洞察力的用户(安全团队)上进行测试。

受信任的测试者

在项目生命周期的早期邀请组织内的人帮助测试变更可以减少对未来变更的恐惧。这种方法让利益相关者在最终确定之前看到变更,这使他们能够及早提出关注。这些新开放的沟通渠道让他们有直接途径在出现问题时提供反馈。在测试阶段表现出收集反馈的意愿可以减少组织内部的隔阂。重要的是要向测试人员明确表示您信任他们提供的反馈,并利用他们的反馈,以便他们知道自己被听到。您并不能总是解决每一条反馈意见,因为并非所有反馈都是有效的或可操作的,但通过向测试人员解释您的决定,您可以建立一个强大的信任联盟。

强制前选择

与狗粮和信任的测试者策略相辅相成的是,在变得强制性之前将新控制变为可选。这使团队有机会按照自己的时间表采纳变化。复杂的变化,如新的授权控制或测试框架,是有成本的;组织需要时间来完全采纳这些变化,而您通常需要权衡这些变化与其他优先事项。如果团队知道他们有时间以自己的步调实现变化,他们可能会对这样做的抵抗性较小。

渐进严格性

如果您必须实现严格的可靠性或安全性新政策,请考虑是否可以随着时间的推移加强严格性:也许您可以首先引入一个影响较小的较低级别控制,然后团队完全采用更严格的控制,负担更重。例如,假设您想添加最低特权控制,要求员工证明其对某些数据的访问。未能适当证明访问权限的用户将被系统锁定。在这种情况下,您可以首先让开发团队将证明框架(如库)集成到系统中,但保持最终用户的证明是可选的。一旦您认为系统性能良好且安全,您可以要求证明访问数据而不锁定未能满足既定标准的用户。相反,当用户输入不准确的证明时,系统可以提供详细的错误消息,提供反馈循环以培训用户并改进系统的使用。一段时间后,当指标显示适当证明的成功率很高时,您可以使锁定用户系统的严格控制成为强制性的。

使安全网成为常态

可靠性和安全性改进通常需要您删除长期依赖的资源,这些资源无法满足您引入的新安全标准。例如,想象一下,您想要改变组织中人员如何使用 Unix 根权限(或类似的高度特权访问),也许通过实现新的代理系统(参见第三章)。对于这样的重大变化的恐惧是自然的。毕竟,如果一个团队突然失去了关键任务的资源访问权限会怎么样?如果变更导致停机时间呢?

您可以通过提供像紧急程序这样的安全网来减少对变革的恐惧(在第五章中讨论),这些程序允许用户绕过新的严格控制。然而,应该谨慎使用这些紧急程序,并且要经受高水平的审计;它们应该被视为最后的手段,而不是方便的替代方案。当正确实现时,紧急程序可以让紧张的团队确信他们可以接受变化或对事件做出反应,而不会完全失去控制或生产力。例如,假设您有一个分阶段的推出程序,需要一个长时间的金丝雀过程,您已经将其实现为一个安全机制,以防止可靠性问题。如果绝对必要,您可以提供一个紧急程序绕过机制,使推送立即发生。我们在第十四章中讨论了这些类型的情况。

提高生产力和可用性

对于与安全和可靠性相关的组织变化,对增加的摩擦的恐惧可能会使其变得困难。如果人们认为减缓开发和创新的新控制是适得其反的,他们可能会认为其采用会对组织产生负面影响。因此,对于新倡议的采用策略通常很重要:考虑整合变化所需的时间,变化是否可能减缓生产力,以及收益是否超过了变化的成本。我们发现以下技术有助于减少摩擦:

构建透明功能

在第六章和第十二章中,我们讨论了通过使用安全构造 API、框架和库来解除开发人员对安全和可靠性的责任。使安全选择成为默认选择有助于开发人员在不给他们带来沉重负担的情况下做出正确的选择。这种方法随着时间的推移减少了摩擦,因为开发人员不仅看到了拥有安全可靠系统的好处,而且也认识到您保持这些倡议简单易行的意图。我们发现这可以随着时间的推移建立团队之间的信任。

专注于可用性

专注于可用性对安全和可靠性文化产生积极影响。¹² 如果新的控制比替代的控制更容易使用,它可以为变革创造积极的激励。

在第七章中,我们谈到在推出双因素身份验证的安全密钥时,我们专注于可用性。用户发现触摸安全密钥进行身份验证比使用硬件令牌生成的一次性密码要容易得多。

作为额外的奖励,这些密钥的增强安全性使我们能够要求更少的密码更改。¹³ 我们对这个问题进行了风险分析,考虑了可用性、安全性和可审计性的权衡。我们发现,安全密钥消除了远程攻击者盗取密码的有效性。当与监控结合使用以检测密码被怀疑被篡改的情况,并在这种情况下强制更改密码时,我们能够平衡安全性和可用性。

有其他机会可以通过安全性和可靠性功能淘汰旧的或不需要的流程,并提高可用性。利用这些机会可以建立用户对安全性和可靠性解决方案的信心和信任。

自注册和自解决

自注册和自解决门户使开发人员和最终用户能够直接解决安全性和可靠性问题,而不需要依赖可能过载或缓慢的中央团队。例如,谷歌使用拒绝和允许列表来控制员工使用的系统上可以运行哪些应用程序。这项技术在防止恶意软件(如病毒)执行方面非常有效。

缺点是,如果员工想要运行不在允许列表上的软件,他们需要寻求批准。为了减少例外请求的摩擦,我们开发了一个名为Upvote的自助门户,使用户能够快速获得可接受软件的批准。在某些情况下,我们可以自动确定某个软件是安全的并批准它。如果我们无法自动批准软件,我们会给用户一个选项,让一定数量的同行批准它。

我们发现社交投票是一种令人满意的控制方式。它并不完美——有时员工会批准一些并非业务相关的软件,比如视频游戏——但这种方法在防止恶意软件在我们的系统上执行方面效果很好。而且由于它不依赖于一个中央团队,控制的摩擦非常小。

过度沟通和透明

在倡导变革时,沟通方式可以影响结果。正如我们在第 7 章和第十九章中所讨论的,良好的沟通是建立支持和成功信心的关键。向人们提供信息和清晰的洞察力,以了解变化是如何发生的,可以减少恐惧并建立信任。我们发现以下策略是成功的:

记录决策

在进行变化时,清楚地记录为什么会发生变化,成功的标志是什么,如果运营条件恶化,如何回滚变化,以及在出现问题时应该与谁联系。确保清楚地传达为什么要进行变化,特别是如果它直接影响员工。例如,谷歌的每个生产卓越 SLO 都需要有明确的理由。由于 SRE 组织是根据这些 SLO 进行衡量的,SRE 们理解其背后含义是很重要的。¹⁵

创建反馈渠道

通过创建反馈渠道,使沟通双向化,人们可以提出关注。这可以是一个反馈表单,一个链接到您的 bug 跟踪系统,甚至是一个简单的电子邮件地址。正如我们在受信任的测试人员讨论中提到的(见“通过风险减少机制减少恐惧”),让合作伙伴和利益相关者更直接地参与变化可以减少恐惧。

使用仪表板

如果你正在跨多个团队或基础设施的不同部分进行复杂的变化,使用仪表板清晰地展示你需要人们做什么,并提供他们的表现反馈。仪表板还有助于展示推出的整体情况,并使组织在进展上保持同步。

撰写频繁的更新

如果一个变化需要很长时间(谷歌的一些变化已经持续了几年),那么就指定某人定期(例如每月)撰写利益相关者更新,概述进展情况。这将建立信心——特别是在领导层——表明项目正在进展,并且有人在关注项目的健康状况。

建立共鸣

在了解别人之前,你不能理解他们的感受。

——未知

当人们了解如何执行自己的角色时,他们开始理解其他人面临的挑战。跨团队的同理心在系统的可靠性和安全性方面尤为重要,因为(如第二十章中所讨论的)这些责任应该在整个组织中共享。建立同理心和理解可以帮助减少对必要变化的恐惧。

在第十九章中,我们概述了一些建立跨团队同理心的技巧——特别是团队如何共享编写、调试和修复代码的责任。同样,Chrome 安全团队不仅进行修复来提高产品的安全性,还作为一个跨组织的团队建设活动。理想情况下,团队应该从一开始就始终共享责任。

工作跟踪或工作交换是另一种建立同理心的方法,不需要永久性的组织自上而下的变化。这些参与可以从几个小时(往往是较少正式的练习)到几个月(可能需要管理层的支持)不等。通过邀请他人体验你团队的工作,你可以表明你愿意打破组织的壁垒,建立共同的理解。

谷歌的 SRE 安全交流计划允许 SRE 在一周内跟随另一个 SRE 或安全工程师。交流结束时,SRE 会为他们的团队和接待团队撰写一份改进建议报告。当在同一办公室进行时,这个计划需要非常低的投资,但在整个组织中提供了许多知识共享方面的好处。谷歌的任务控制计划鼓励人们加入 SRE 组织六个月,期间他们学习如何像 SRE 一样思考和应对紧急情况。通过这样做,他们直接看到了合作组织发起的软件变更的影响。一个名为黑客营的并行计划鼓励人们加入安全团队六个月,他们可以在安全审查和应对工作中工作。

这些计划可能从一两名工程师的小型实验开始,并随着时间的推移而增长。我们发现,这种类型的工作交换既建立了同理心,又激发了解决挑战的新想法。引入这些新的观点并在团队之间建立善意有助于推动变革的齿轮。

最后,建立感谢机制——从简单的电子邮件到更复杂的形式——强化了人们对彼此的积极影响,并设定了正确的激励措施。在谷歌,我们长期以来一直有同事奖金的文化——这是一些不花费公司很多钱的小额现金,但却建立了大量的善意。一个无现金版本的 Kudos 允许谷歌员工以数字形式正式认可彼此。我们的一些办公室也尝试过感谢明信片。

说服领导

如果你在一个大型组织中工作,获得对你想要进行的可靠性和安全性变更的支持可能是一个挑战。由于许多组织受到激励,要把有限的资源花在产生收入或推动使命的努力上,很难获得对被视为发生在幕后的改进的支持。

本节探讨了我们在谷歌使用过或在其他地方看到的一些策略,以获得领导层对安全和可靠性变革的支持。与本章其他地方给出的指导一样,你的效果可能会有所不同。其中一些策略会有效,而其他策略则不会。正如每个组织的文化是独特的一样,每个领导者和领导团队也是如此。值得重申我们之前的建议:仅仅因为你认为其中一种策略行不通并不一定意味着你不应该尝试。结果可能会让你感到惊讶。

了解决策过程

假设你想对你组织的自定义前端 Web 服务器基础架构进行相当大的改变,以包括 DDoS 防护;例如,参考第十章中概述的好处。你知道这将极大地提高系统的可靠性和安全性,但这也需要多个团队来整合新的库或重构代码。正确整合和测试这个改变可能需要几个月的时间。考虑到高成本但积极影响,你组织中的谁会决定继续前进,他们将如何做出这个决定?了解这些问题的答案对于知道如何影响领导至关重要。

在这里,“领导力”一词宽泛地适用于做出决策的人,无论这些决策是关于方向的设定、资源分配还是解决冲突。简而言之,这些人被认为拥有权威和责任。他们是你想要影响的人,所以你需要弄清楚他们是谁。如果你在一家大公司工作,他们可能是副总裁或其他高级管理人员。像初创公司和非营利组织这样的小型组织通常认为 CEO 是高级决策者。在开源项目中,这可能是项目的创始人或最重要的贡献者。

回答“谁是这个变革的决策者?”这个问题可能很难确定。做出决策的权威可能确实属于通常被认为处于领导层层级最高位置的人,或者处于明显的守门人角色,比如律师或风险官员。但根据你提出的变革的性质,决定也可能在技术领导者,甚至在你身上。决策者可能不是一个人,可能是来自组织不同部门的一组利益相关者,比如法律、新闻关系、工程和产品开发。

有时做出决定的权威可能松散地存在于一群人之中,或者在极端情况下,存在于整个社区之中。例如,在第七章中,我们描述了 Chrome 团队参与增加互联网上的 HTTPS 使用。在这种情况下,决定做出方向性改变是在社区内做出的,并且需要建立行业范围内的共识。

确定谁是变革的决策者可能需要一些侦探工作,特别是如果你是新来到一个组织,或者没有现有的流程告诉你如何完成某件事。然而,你不能跳过这一步。一旦你了解了决策者是谁,你就应该努力了解他们面临的压力和需求。这些压力可能来自他们自己的管理层、董事会或股东,甚至可能来自客户的期望等外部影响。了解这些压力很重要,这样你就能了解你提出的变革在哪里。

为变革建立案例。

正如本章已经提到的,对变化的抵制可能源于恐惧或摩擦感的认知,但在许多情况下,它也可能源于不理解变化的原因。当面临许多优先事项时,决策者和利益相关者面临着选择不同目标的困难任务。他们如何知道你的变化是有价值的?了解决策者在为你的变革建立案例时面临的挑战是很重要的。这是成功案例建立过程中的一些步骤:¹⁶

收集数据

你知道需要做出改变。你是如何得出这个结论的?有数据支持你提出的变化是很重要的。例如,如果你知道将自动化测试框架构建到构建过程中会节省开发人员的时间,你能展示这种变化将节省多少时间吗?如果你主张持续构建,因为这种做法会激励开发人员修复错误,你能展示持续构建如何在发布过程中节省时间吗?进行研究和用户研究,制作数据丰富的报告,其中包括图表和用户的轶事,然后以决策者能够消化的方式总结这些数据。例如,如果你想缩短团队修补安全漏洞或解决可靠性配置问题所需的时间,考虑创建一个跟踪每个工程团队进展的仪表板。向这些领域的领导展示这些仪表板可以鼓励各个团队达到目标。要注意你需要做出的投资,以收集高质量、相关的数据。

教育他人

安全性和可靠性问题可能很难理解,除非你每天都与它们联系在一起。通过讲话和信息会议传播消息。在谷歌,我们使用红队事后总结(见第二十章)向高层领导教育我们所面临的风险。虽然红队最初并不是为了教育而创建的,但它可以在公司的各个层面提高意识。这对说服团队维持其修复漏洞的 SLOs 是有益的。

调整激励措施

利用你收集的数据和对决策者面临的压力的了解,你可能能够解决他们影响范围内的其他问题。在我们之前的 DDoS 示例中,对框架提出的改变将提供安全性好处,但更可靠的网站也可能有助于增加销售额。这可能是向公司领导提出的一个有力论据。以 Chrome 安全团队为例,第十九章讨论了 Chrome 的快速发布如何更快地为用户提供安全修复,同时还能快速部署可靠性修复和新功能。这对用户和产品开发利益相关者都是很好的。不要忘记讨论你是如何减少可能伴随变化的恐惧和摩擦的——正如本章前面提到的,谷歌的安全密钥推出使我们能够取消不受欢迎的密码更改政策,并减少双因素身份验证的最终用户摩擦,这是变革的有力论据。

寻找盟友

很可能,你并不是唯一知道你提出的变化会有益的人。寻找盟友并说服他们支持你的变革可以增加你的论据的说服力,尤其是如果这些人在组织上与决策者密切相关。盟友还可以测试你对变化的假设。也许他们知道不同的基于数据的论据,或者以你不了解的方式了解组织。这种同行评审可以增强你的论据的力量。

观察行业趋势

如果你正在采纳其他组织已经采纳的变革,你可能可以依靠他们的经验来说服你的领导层。做好你的研究——文章、书籍、会议上的公开演讲以及其他材料可能会展示一个组织是如何以及为什么采纳了变革。你可能还可以直接使用其他数据点来建立你变革的案例。你甚至可以考虑邀请专家演讲者就特定主题和行业趋势向你的领导层发表演讲。

改变时代精神

如果你能够随着时间改变人们对你的问题的看法,也许以后说服决策者会更容易。当你需要广泛共识支持变革时,这一点尤为重要。我们在第七章中简要讨论了这种动态,在那里,Chrome 团队和行业中的其他人在很长一段时间内改变了开发者的行为,直到 HTTPS 成为默认情况。

抉择你的战斗

如果你的组织面临许多可靠性和安全挑战,不断的倡导可能会导致疲劳和对额外变革的抵制。重要的是要谨慎选择你的战斗:优先考虑有可能成功的倡议,并知道何时停止为已经失败的事业倡导。这显示了领导和决策者们你正在解决最重要的问题。

失败的事业——也就是你不得不搁置的提案——也有价值。即使你不能成功地倡导变革,拥有支持你的想法的数据和盟友,以及向人们传达问题的教育,都是有价值的。在某个时候,你的组织可能已经准备好解决你已经研究过的挑战。如果你已经有一个等待的计划,团队可以更快地行动。

升级和问题解决

尽管尽了最大努力,有时对安全或可靠性变革的决策需求可能会以爆炸性的方式浮出水面。也许严重的故障或安全漏洞意味着你迅速需要更多资源和人员。或者也许两个团队对如何解决问题有不同的意见,而决策的自然过程并不奏效。在这些类型的情况下,你可能需要从管理层寻求解决方案。在处理升级时,我们建议遵循以下准则:

  • 组建一个由同事、导师、技术负责人或经理组成的小组,从两方面提供对情况的意见。在决定升级之前,通常最好与一个持中立观点的人一起讨论情况。

  • 让团队总结情况并提出管理层的决策选项。尽可能保持这个总结简洁。保持严格的事实陈述,包括任何相关的支持数据、对话、错误、设计等的链接。尽可能清楚地说明每个选项的潜在影响。

  • 与你自己团队的领导分享总结,以确保对可能解决方案的进一步一致性。例如,多个问题可能需要同时升级。你可能想要合并升级或强调相应情况的其他方面。

  • 安排一个会议向所有受影响的管理层介绍情况,并在每个管理层中指定适当的决策者。然后决策者们应该做出正式决定或单独会议讨论这个问题。

作为一个具体的例子,有时在谷歌,当产品团队和安全审查人员在采取行动的最佳方式上出现无法解决的分歧时,安全问题需要升级。在这种情况下,安全团队内部发起了升级。此时,组织内的两位高级领导者协商妥协或决定执行安全团队或产品团队提出的选项之一。因为我们将这些升级融入了我们正常的公司文化中,升级并不被视为对抗性的。

结论

正如您设计和管理系统一样,您可以随着时间的推移设计、实现和维护组织文化,以支持安全性和可靠性目标。可靠性和安全性工作应该像工程工作一样仔细考虑。工程中有重要的文化元素,当综合考虑时,甚至单独考虑时,可以促进更健壮的系统。

安全性和可靠性的改进可能会引发对增加摩擦的恐惧或担忧。有策略可以解决这些恐惧,并帮助获得受到这些变化影响的人的支持。确保您的目标与利益相关者(包括领导层)的利益保持良好的一致性至关重要。专注于可用性并表现出对用户的同理心可以鼓励人们更愿意接受变化。对于思考他人如何看待变化进行一点投资可能会导致更大的成功,使他们相信您的变化是合理的。

正如我们在本章开头所述,没有两种文化是相同的,您需要根据自己的组织来调整我们提出的策略。这样做,您还会发现您可能无法实现所有这些策略。挑选并选择您的组织最需要解决的领域,并随着时间的推移改进这些领域——这是谷歌长期不断改进的方式。

请参见 SRE 书的第十五章。

这在 SRE 书的第三章中有所讨论。

谷歌的代码审查实践在《代码审查开发人员指南》中有记录。有关谷歌的代码审查流程和文化的更多背景信息,请参见 Sadowski, Caitlin 等人 2018 年的“现代代码审查:谷歌的案例研究”。

一项 2018 年对谷歌现代代码审查流程的研究发现,开发人员重视其工具提供的低摩擦工作流程。

例如,谷歌保持了一套关于跨站脚本的最佳实践。

最近一项关于谷歌“厕所测试”计划的使用研究表明,该计划提高了开发人员的意识。

这个话题与“推动”密切相关,这是一种通过微妙地鼓励人们做正确的事情来改变行为的方法。推动理论是由理查德·塞勒斯坦和卡斯·桑斯坦开发的,他们因对行为经济学的贡献而获得了诺贝尔经济学奖。有关更多信息,请参见理查德·H·塞勒和卡斯·R·桑斯坦。2008 年的《推动:改善健康、财富和幸福的决策》。

谷歌的客户可靠性工程总监戴夫·伦辛在他的演讲“通过更大的人性减少风险”中更详细地讨论了这个话题。

哥伦比亚灾难调查委员会的最终报告被保存在 NASA 网站上,供普通公众阅读。特别是第七章着重于 NASA 的安全文化及其对灾难的影响。我们发现该报告的调查结果通常可以推广到其他类型的工程组织的组织文化中。

¹⁰ 请参见SRE 书籍的第 29 章

¹¹ 请参见SRE 书籍的第 32 章

¹² 人们早就认识到安全和隐私的可用解决方案对于成功部署技术控制至关重要。如果你想了解这些对话的风采,你可能会对探索SOUPS的会议记录感兴趣,这是一场专门讨论可用安全和隐私的会议。

¹³ 研究表明,用户做出了使其密码面临风险的不良选择。有关用户密码选择不利影响的更多信息,请参见张引谦、法比安·蒙罗斯和迈克尔·K·赖特的文章。2010 年。“现代密码过期的安全性:算法框架和实证分析。”第 17 届 ACM 计算机与通信安全会议论文集:176-186。https://oreil.ly/NbfFj。标准和合规制度也在考虑这些影响。例如,NIST 800-63已经更新,要求只有在怀疑密码已被破坏时才需要更改密码。

¹⁴ Password Alert 是 Chrome 浏览器的一个扩展程序,当用户在恶意网站上输入其 Google 或 GSuite 密码时会发出警报。

¹⁵ 由高级 SRE 领导定期对 SRE 团队进行生产卓越性审查,评估他们在许多标准措施上的表现,并提供反馈和鼓励。例如,99.95%的 SLO 可能会伴随着这样的理由:“我们之前希望达到 99.99%的成功率,但发现这个目标在实践中是不现实的。在 99.95%的情况下,我们没有发现对开发人员生产力的负面影响。”

¹⁶ 有关我们如何成功建立变革案例的实际例子,请参见“示例:增加 HTTPS 使用率”

总结

原文:Conclusion

译者:飞龙

协议:CC BY-NC-SA 4.0

我们写这本书是因为我们相信在保护系统和保持其可靠性的技术和实践中存在重要的重叠,并且组织应该在设计、实现和维护系统的过程中整合安全和可靠性的概念。传统上,它们被视为独立的学科,但我们认为安全和可靠性是系统的固有属性,因此是项目生命周期中所有参与者的责任。许多技术变革已经在进行中,我们相信这将激励组织采取这种观点。

这种技术革命——有些人称之为第四次工业革命——正在改变我们所知道的世界。这种转变不仅被消费者所感受到,以更复杂的产品形式,而且也被生产这些产品的开发人员所感受到。组织越来越依赖技术,即使它不是他们业务的核心。例如,我们看到了让外科医生在世界另一端的患者上进行手术的系统。科学家正在使用自主飞行器勘测考古遗址,研究土壤侵蚀的影响,并保护濒危物种。机器人被部署进行太空和核灾难现场的危险工作。

科技的不断连接意味着我们越来越依赖这些解决方案的可靠性。当我们将数据表面扩展到第三方时,我们需要确信这样做是安全的。我们与人们建立的信任是基于我们选择运行基础设施的技术的可靠性和安全性。这对于任何规模的组织都是如此,从一个三人的开源项目,成千上万其他项目依赖它,到一个向全球用户销售产品的大型跨国公司。

现代系统的复杂性和它们的开发速度意味着安全性和可靠性需要从产品的开始阶段进行整合,以实现最大的效果。将安全性和可靠性视为系统的固有属性不仅是自然的,而且在今天的自动化、连接和复杂的技术环境中至关重要。

因此,在更广泛的社区中,DevOps 和 DevSecOps 正在推动关于系统可持续性的讨论。然而,一个集成的安全和可靠性模型的概念将需要时间来发展,并成为生态系统的自然组成部分。许多开发生命周期和组织在功能上都围绕着负责系统开发、测试、安全、可靠性和运营的团队之间的分工。这种模式需要转变以满足我们所看到的技术变革的需求。

在撰写本书时,我们邀请了来自谷歌各个团队的成员——从开发人员到 SREs 再到安全工程师。这种合作反映了谷歌依赖于保护其系统并使其越来越可靠的互动精神。在谷歌,我们将安全性和可靠性问题纳入产品开发过程,并鼓励具有不同经验和技能的人们共同合作,并倾听其他人带来的观点。因为人与系统本身一样重要,我们建议在设计团队和构建其责任和激励结构时进行深思熟虑的投资。人们需要在讨论技术解决方案之前就共同的要求达成一致意见,而这可能很难实现。不要低估建立信任和确保大家都在说一种共同语言所需的投资。

对于那些对安全和可靠性充满热情的人,我们总结如下建议:你跨知识领域工作的能力,并将专业知识嵌入到合适的位置,是你的组织成功的关键。安全和可靠性需要成为整个计算环境的一个整合部分。所有这些要素必须和谐地共同工作来解决问题。我们所能给出的任何清单或灵丹妙药的建议都无法弥补你自己帮助组织在安全和可靠性挑战的性质不断演变时灵活成长的能力。

当我们结束这本书时,我们预计它将标志着许多重要对话的开始。我们希望你也能加入这个讨论,参与与其他专业人士的社区交流并分享你的故事。在这种对话中,我们鼓励你尊重不同角色带来的许多观点,支持寻找解决方案,并分享对你有用的方法。我们相信这种对话将有助于我们所有人努力创建安全可靠的系统。

附录 A:灾难风险评估矩阵

原文:Appendix. A Disaster Risk Assessment Matrix

译者:飞龙

协议:CC BY-NC-SA 4.0

对于彻底的灾难风险分析,我们建议使用标准化矩阵对您的组织面临的风险进行排名,该矩阵考虑了每种风险发生的概率以及对组织的潜在影响。表 A-1 是一个样本风险评估矩阵,大型和小型组织都可以根据其系统的具体情况进行调整。

要使用该矩阵,评估概率和影响的每一列的适当值。正如我们在第十六章中强调的那样,这些值可能取决于您的组织所做的事情、其基础设施以及其所在位置。位于美国加利福尼亚州洛杉矶的组织可能比位于德国汉堡的组织更有可能经历地震。如果您的组织在多个地点设有办事处,您甚至可能希望对每个地点进行风险评估。

一旦您计算出概率和影响值,就将它们相乘以确定每个风险的等级。得出的值可用于按从高到低的顺序排列风险,这将作为优先级和准备工作的指南。排名为 0.8 的风险可能需要比价值为 0.5 或 0.3 的风险更紧急的关注。一定要为您的组织面临的最关键风险制定应对计划。

表 A-1. 样本灾难风险评估矩阵

主题 风险 一年内发生的概率 风险发生时对组织的影响 排名 风险影响的系统名称
几乎不可能:0.0 微不足道:0.0 概率 x 影响
不太可能:0.2 最小:0.2
有点不太可能:0.4 中等:0.5
可能:0.6 严重:0.8
极有可能:0.8 关键:1.0
不可避免:1.0
环境 地震
洪水
火灾
飓风
基础设施可靠性 停电
互联网连接丢失
认证系统故障
高系统延迟/基础设施减速
安全 系统受损
知识产权内部盗窃
DDos/DoS 攻击
滥用系统资源——例如,加密货币挖矿
破坏/网站篡改
网络钓鱼攻击
软件安全漏洞
硬件安全漏洞
新出现的严重漏洞,例如 Meltdown/Spectre,Heartbleed
posted @ 2025-11-20 09:31  绝不原创的飞龙  阅读(40)  评论(0)    收藏  举报