本文介绍了PyPI(Python Package Index)上新的、更安全的身份验证方法——“可信发布”。它基于OpenID Connect (OIDC) 构建,无需长期存在的APIToken和密码,降低了供应链攻击和凭据泄露的风险,简化了发布工作流程,允许CI/CD系统安全地发布包而无需共享密钥。文章还探讨了可信发布的安全模型、潜在威胁及应对措施。
阅读 PyPI 博客上的官方公告!
在过去的一年里,我们与 Python 包索引 合作,添加了一种新的、更安全的身份验证方法,称为“可信发布”。可信发布消除了对长期存在的 API Token和密码的需求,降低了供应链攻击和凭据泄露的风险,同时简化了发布工作流程。PyPI 上的 关键 软件包 urllib3 已经在使用可信发布来提高其发布过程的安全性。
如果你将软件包发布到 PyPI,请使用官方 PyPI 文档立即为你的项目设置可信发布。本文的其余部分将介绍可信发布的技术原理和原因,以及我们希望在未来看到类似技术应用的地方。
我们乐于帮助扩展对语言生态系统的信任。如果你参与打包生态系统(例如,NPM、Go、Crates 等),并希望采用更多这些技术,请 联系我们!
从本质上讲,可信发布“只是”另一种身份验证机制。从这个意义上讲,它与密码或长期存在的 API Token没有什么不同:你向索引提供某种证明,说明你的身份和预期权限;索引验证该证明,如果有效,则允许你执行与这些权限相关的操作。
可信发布有趣的地方在于它如何在 不需要预先存在的共享密钥 的情况下实现身份验证。让我们深入了解一下!
可信发布建立在 OpenID Connect (OIDC) 之上,这是一个基于 OAuth2 构建的开放身份证明和验证标准。OIDC 使身份提供商 (IdP) 能够生成可公开验证的凭据,以证明特定身份(如 hamilcar@example.com)。这些凭据在底层是 JSON Web Tokens (JWT),这意味着 OIDC 下的身份是 JWT 中相关 声明 的集合。
为了更清楚地说明这一点,以下是 GitHub 的 OIDC IdP 提供的用户身份的(稍微经过编辑的)声明集可能如下所示:
(在实际的 JWT 中,此声明集将附带数字签名,证明其对于 IdP 持有的 可信签名密钥 的真实性。没有该数字签名,我们就没有理由相信这些声明!)
任何人都可以成为 OpenID Connect 方案中的 IdP。但是,OIDC 的大部分实际价值都来自与大型的、假定为值得信赖且保护良好的 IdP 的交互。证明对 GitHub 和 Google 帐户等事物的所有权是有价值的,特别是对于诸如 SSO 和服务联合之类的事情。
到目前为止,一切都很好,但是这些与 PyPI 之类的打包索引并没有特别相关。PyPI 可以允许用户使用 OIDC 而不是密码登录,但是尚不清楚这如何使发布工作流程(尤其是基于 CI 的工作流程)更加方便。
OIDC 对 PyPI 之类的程序包索引有用的原因是,OIDC 身份不需要一定是人:它可以是机器标识符、源存储库,甚至是 CI 运行的特定实例。此外,它不需要通过交互式 OAuth2 流来获得:它可以“环境地”作为一种只有该身份(机器等)才能访问的对象或资源来提供。
CI 提供商不久前就发现了这一点:GitHub Actions 在 2021 年末 添加了对 环境 OIDC 凭据的支持,而 GitLab 在 几个月前 添加了该功能。以下是在 GitHub Actions 上检索这些凭据中的一种的方式:
这是 GitHub Actions 工作流程运行的(再次,经过筛选)声明集可能如下所示:
这是 很多 可以使用的上下文:假设我们信任 IdP,并且签名已签出,则我们可以将身份验证到确切的 GitHub 存储库、运行的工作流程、触发该工作流程的用户等等。反过来,这些中的每一个都可以成为身份验证系统中的约束。
回顾一下:OpenID Connect 为我们提供了验证来自 IdP 的身份证明(以 OIDC Token的形式)所需的上下文和机制。这些证明中的身份可以是任何身份,包括特定存储库中 GitHub Actions 工作流程的身份。
反过来,任何第三方服务(如 PyPI)都可以接受 OIDC Token并基于这些Token确定一组权限。由于 OIDC Token以加密方式绑定到特定 OIDC IdP 的公钥,因此攻击者无法欺骗 OIDC Token,即使他们知道其中的声明也是如此。
但是请稍等:我们如何从包含身份的 OIDC Token转换为特定的 PyPI 项目?我们如何知道哪个 PyPI 项目应该信任哪个或哪些 OIDC 身份?
这就是需要一些 受信任的设置 的地方:用户(在 PyPI 上)必须登录并配置每个项目与授权代表该项目发布的发布者(即 OIDC 身份)之间的信任关系。
只需执行一次此操作,就像使用普通 API Token一样。但是,与 API Token不同,该过程仅涉及到一方:不需要向 CI(和 OIDC)提供程序提供Token或任何其他秘密材料。此外,即使是受信任的设置部分也完全由公共信息组成:它只是用户认为可以信任其进行发布的声明值集。对于 GitHub Actions 发布到 PyPI,受信任的设置应包括以下内容:
总之,这些状态允许依赖方(例如,PyPI)接受 OIDC Token,确认它们是由受信任的身份提供商(例如,GitHub Actions)签名的,然后将签名的声明与建立对这些声明的信任的一个或多个 PyPI 项目进行匹配。
至此,我们拥有了允许通过 OIDC 验证的身份发布到 PyPI 所需的一切。以下是在 GitHub 中的情况:
对于 99% 的软件包发布者而言,步骤 3 到 7 完全是实现细节:用于发布到 PyPI 的官方 PyPA GitHub Action 将它们封装起来,使面向用户的部分只有以下内容:
此时,你可能会合理地认为:
我是一名有能力的工程师,我已经做对了一切。我的Token已正确地限定为所需的最小权限,它们存储为工作流程(或每个环境的)机密,并且我仔细审核我的发布工作流程,以确保所有第三方代码都值得信赖。” – 你,一名有能力的工程师
事实是:你一直在做正确的事情!到目前为止,向 PyPI 进行身份验证的最安全方法是执行以下步骤:
这足以满足许多用例,但也从 可用性 和 安全性 的角度来看,有很多不足之处:
可信发布可解决这些问题以及更多问题:
可用性。使用可信发布者,无需手动管理 API Token:配置发布者是每个项目的一次性操作,包括尚未创建的项目。 这避免了在发布全新项目时涉及到麻烦的 API Token舞,以及工程师在尝试将 API Token交给负责将其添加到 CI 机密的一方时所玩的“凭据烫手山芋”游戏。不再需要通过 Slack DM 发送 API Token!
Pre-compromise security. Trusted publishing reduces the number of adversaries: an attacker with access to only some GitHub Actions environments or particular (non-permission) steps can’t mint the OIDC credential needed to use the trusted publisher. This is in marked contrast to a long-lived token stored in a GitHub Actions secret, where any step (and frequently any environment) can access the credential!
泄漏后恢复。可信发布从根本上来说是短暂的:所涉及的凭据(OIDC 和 PyPI 凭据)每次都只能持续几分钟,这意味着在违规后响应期间失去访问权限的攻击者会自动被封锁,而无需任何人为干预。这意味着更少的手动步骤和更少可能的人为错误。
可信发布是一种安全地向程序包索引进行身份验证的另一种方法。像每个安全功能一样,必须针对威胁模型来设计和实现它。该威胁模型必须证明可信发布的合理性,既可以解决以前的身份验证方法未解决的攻击者,又可以解决它暴露的新攻击场景。
在打包生态系统中,帐户接管 (ATO) 是一个已知的问题:设法损害合法用户的 PyPI 或 GitHub 帐户的攻击者可以上传恶意版本(甚至覆盖以前的版本),而没有任何不真实的外部指示。
在一般情况下,ATO 是一个无法解决的问题:PyPI 和 GitHub 之类的服务可以改善对安全功能的访问(甚至 强制执行这些功能),但从根本上来说,无法阻止用户泄露其凭据(例如,通过网络钓鱼),更无法保护他们免受他们使用的每个可能存在漏洞的软件的侵害。
同时,诸如可信发布之类的功能可以减少帐户入侵的范围:程序包索引允许程序包选择仅加入可信发布的未来,即对程序包索引本身的 ATO 不允许攻击者上传恶意版本。
同样,“供应链安全”如今已风靡一时:公司和业余爱好者都在重新审视失控的依赖关系树及其经常无法解释且无法追踪的组件。
如果没有可信发布,GitHub Actions 的现状是你 信任你执行的每个第三方操作:他们都可以读取你配置的机密。这是非常不理想的,并且是可信发布旨在保护的关键攻击模型之一。
可信发布之所以有效,是因为它与“可信身份”的概念相关联:另一侧(例如,在 GitHub Actions 上)的可信身份是 user/repo、工作流程名称和可选环境名称的元组。
但是请稍等:如果用户更改其用户名并且攻击者接管了其旧用户名会发生什么?我们称其为“帐户复活”,并且大多数服务 明确支持 它:用户名不旨在成为基础身份的永久、稳定的标识符。
这打开了一种全新的攻击媒介:信任 hamilcar/cartago 的 PyPI 项目可能会突然开始信任受攻击者控制的 hamilcar/cartago,所有这些都是因为原始 hamilcar 现在是 hannibal(并且合法的 hamilcar/cartago 现在是 hannibal/cartago)。
在为 PyPI 设计可信发布时,我们考虑到了这一点,并 与 GitHub 合作 添加了一个额外的声明,该声明不仅将 OIDC Token绑定到 用户,还绑定到他们的唯一、稳定的用户 ID。这为我们提供了 我们需要的状态 来防止复活攻击:即使攻击者设法在 GitHub 上成为 hamilcar,他们基础的用户 ID 也不会改变,并且 PyPI 会拒绝他们提供的任何身份Token。
可信发布还揭示了一个项目信任模型中的一个新的(潜在的)划分:对于任何给定的项目,你是否信任该项目的每个成员也都是潜在的发布者?在许多情况下,答案是肯定的:许多项目只有一两个存储库成员,他们都是程序包索引上的所有者或以其他方式具有特权。
但是,在某些情况下,答案是否定的:许多项目都有数十个低活动或不活动的成员,并非所有这些成员都遵循保护其帐户的最佳实践。由于社区政策或因为他们需要访问不频繁(但至关重要)的项目活动,这些成员可能无法删除。这些用户不应仅仅因为他们在存储库上拥有提交位就一定获得将版本发布到打包索引的权限。
这也是我们在设计可信发布时考虑的一个因素,这就是为什么 PyPI 的实现支持一个可选的 GitHub Actions 环境:对于提交者和发布者不完全重叠的社区,可以使用环境来施加其他工作流程限制,这些限制会反映在 OIDC Token中(随后受到 PyPI 的尊重)。PyPI 自己的安全模型文档中给出了一个 详细的示例。
我们对 PyPI 的工作由令人难以置信的 Google 开源安全团队 (GOSST) 资助,我们还与该团队合作开发了用于 Python 生态系统整体安全性的 新 工具。特别是,我们要感谢 Dustin Ingram 与我们不懈努力,并指导了 PyPI 可信发布的总体步伐和设计。
目前,据我们所知,PyPI 是唯一提供可信发布的程序包索引。话虽如此,可信发布没有任何特定于 Python 或 Python 打包的特性:它可以很容易地被 Rust 的 Crates、Ruby 的 RubyGems、JavaScript 的 NPM 或任何其他从第三方服务发布很常见的生态系统(如 GitHub Actions 或 GitLab 的 CI/CD)采用。
我们认为,就像 2019 年的双因素身份验证 一样,这种可信发布方案将成为开源打包安全模型的工具。我们将其视为各种后续改进的基础,包括能够 生成强大的加密证明,证明 PyPI 版本是从特定源工件构建的。
如果你或你的公司对这项工作感兴趣,请 与我们联系!我们在 开源生态系统中的安全功能方面拥有多年的经验,并且一直在寻找更多方法来为关键开源项目和服务做出贡献。
- 原文链接: blog.trailofbits.com/202...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!