可信发布:提升软件包安全性的新标杆
在过去一年中,我们与Python软件包索引(PyPI)合作,引入了一种名为“可信发布”的新型安全身份验证方法。可信发布消除了对长期API令牌和密码的需求,降低了供应链攻击和凭证泄露的风险,同时简化了发布工作流程。PyPI上的关键软件包已经开始使用可信发布来使其发布过程更加安全。
如果您向PyPI发布软件包,请立即使用官方PyPI文档为您的项目设置可信发布。本文的其余部分将介绍可信发布的技术原理和优势,以及我们希望在未来看到类似技术应用的领域。
我们致力于帮助扩大语言生态系统的信任度。如果您参与某个软件包生态系统(如NPM、Go、Crates等)并希望采用更多此类技术,请与我们联系!
可信发布:入门指南
从本质上讲,可信发布“只是”另一种身份验证机制。从这个意义上说,它与密码或长期API令牌没有什么不同:您向索引提供某种身份证明,声明您的身份和预期权限;索引验证该证明,如果有效,则允许您执行与这些权限相关的操作。
可信发布的有趣之处在于它如何在不要求预先存在共享密钥的情况下实现身份验证。让我们深入了解!
OpenID Connect和“环境”凭证
可信发布建立在OpenID Connect(OIDC)之上,这是一个基于OAuth2构建的开放身份认证和验证标准。OIDC使身份提供商(IdP)能够生成可公开验证的凭证,证明特定身份(如hamilcar@example.com)。这些凭证底层是JSON Web令牌(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令牌的形式)所需的上下文和机制。这些证明中的身份可以是任何事物,包括特定仓库中GitHub Actions工作流的身份。
任何第三方服务(如PyPI)都可以接受OIDC令牌,并基于它们确定一组权限。由于OIDC令牌在密码学上与特定OIDC IdP的公钥绑定,攻击者无法伪造OIDC令牌,即使他们知道其中的声明。
但等等:我们如何从包含身份的OIDC令牌转到特定的PyPI项目?我们如何知道哪些PyPI项目应该信任哪些OIDC身份?
这就需要一点可信设置:用户(在PyPI上)必须登录并配置每个项目与授权代表项目发布的发布者(即OIDC身份)之间的信任关系。
这只需要完成一次,就像普通的API令牌一样。然而,与API令牌不同,它只涉及一方:不需要向CI(和OIDC)提供商提供令牌或任何其他秘密材料。此外,即使是可信设置部分也由完全公开的信息组成:它只是用户认为可用于发布目的的声明值集合。对于通过GitHub Actions发布到PyPI,可信设置将包括以下内容:
- GitHub用户/仓库slug
- 执行发布的GitHub Actions工作流的文件名(例如,release.yml)
- 可选地,工作流使用的GitHub Actions环境的名称(例如,release)
这些状态共同允许依赖方(如PyPI)接受OIDC令牌,确认它们由受信任的身份提供商(如GitHub Actions)签名,然后将签名声明与一个或多个已建立对这些声明的信任的PyPI项目进行匹配。
看,没有秘密!
此时,我们拥有了一切所需,允许通过OIDC验证的身份发布到PyPI。以下是在GitHub案例中的流程:
- 开发者(或自动化流程)触发GitHub Actions工作流向PyPI发布。
- 正常的构建过程(
python -m build或类似命令)开始。 - 自动化流程检索当前工作流运行的OIDC令牌,通过GitHub Actions的OIDC IdP证明当前工作流的身份(用户/仓库、工作流名称、环境等)。
- 该OIDC令牌被发送到PyPI。
- 如果有效,PyPI验证它并将其交换为短期有效的PyPI API令牌,该令牌仅限信任这些令牌声明的PyPI项目使用。
- PyPI将短期API令牌作为对OIDC令牌的响应返回。
- 工作流继续,使用短期API令牌执行正常的PyPI发布步骤(例如,使用
twine)。
对于99%的软件包发布者来说,步骤3到7完全是实现细节:官方的PyPA GitHub Action for publishing to PyPI封装了它们,使得用户面对的部分仅仅是:
我为什么应该关心?
此时,您可能会合理地认为:
“我是一名能干的工程师,我已经把所有事情都做对了。我的令牌被正确地限定为所需的最小权限,它们被存储为工作流(或每个环境)的秘密,并且我仔细审核我的发布工作流以确保所有第三方代码都是可信的。”——您,一位能干的工程师
问题是:您一直做得都对!直到现在,向PyPI进行身份验证的最安全方法是执行以下操作:
- 创建一个项目范围的API令牌。
- 将其作为(限定范围的)秘密存储在您的CI中。
- 在您已审核并建立信任的发布工作流中小心地访问它。
这对于许多用例来说已经足够,但从可用性和安全性的角度来看,仍然有很多不足之处:
- 可用性:手动管理和创建API令牌很繁琐,特别是在单个源代码仓库托管多个PyPI软件包的情况下:每个都需要自己单独限定范围的令牌、唯一的秘密名称等等。您和您的同事有更好的方式度过时间!
- 预入侵安全:并非所有攻击者都相同:有些是被动的,有些是主动的,有些可能只能破坏您发布过程中的特定步骤,等等。减少(或完全消除)这些攻击者之一的能力是有用的,即使所涉及的缓解措施不会对其他攻击者产生实质性影响。不幸的是,使用长期令牌很难做到这一点:长期令牌同样容易受到任何在任何时间获得访问权限的攻击者的攻击。
- 入侵后恢复:为安全而设计意味着试图阻止攻击者,并为成功攻击者带来的风险做好准备和缓解。对于长期凭证(无论是密码还是API令牌),这个过程缓慢、繁琐且容易出错:遗漏单个凭证就会为攻击者留下返回的缺口。一个更好的系统从一开始就不会有这个问题。
可信发布解决了这些问题以及更多:
- 可用性:使用可信发布者,不需要手动管理API令牌:为每个项目配置发布者是一次性操作,包括尚未创建的项目。 这避免了发布全新项目时涉及的烦人API令牌操作,以及工程师在尝试将API令牌交给负责将其添加到CI秘密的一方时玩的“凭证烫手山芋”游戏。不再有包含API令牌的Slack私信!
- 预入侵安全:可信发布减少了对手的数量:仅能访问某些GitHub Actions环境或特定(非权限)步骤的攻击者无法生成使用可信发布者所需的OIDC凭证。这与存储在GitHub Actions秘密中的长期令牌形成鲜明对比,后者任何步骤(并且经常是任何环境)都可以访问该凭证!
- 入侵后恢复:可信发布本质上是短暂的:所涉及的凭证(包括OIDC和PyPI凭证)每次只存活几分钟,这意味着在入侵后响应期间失去访问权限的攻击者会自动被封锁,无需任何人工干预。这意味着更少的手动步骤和更少可能的人为错误。
安全和威胁模型考量
可信发布是安全地向软件包索引进行身份验证的另一种方式。与每个安全功能一样,它必须根据威胁模型进行设计和实现。该威胁模型必须证明可信发布存在的合理性,既要解决先前身份验证方法未解决的攻击者,也要应对它暴露的新攻击场景。
现有威胁:账户接管和供应链攻击
账户接管(ATO)是软件包生态系统中的一个已知问题:成功破坏合法用户PyPI或GitHub账户的攻击者可以上传恶意版本(甚至覆盖以前的版本),而没有任何外部不真实迹象。
在一般情况下,ATO是一个无法解决的问题:像PyPI和GitHub这样的服务可以改进安全功能的可访问性(甚至强制要求这些功能),但基本上无法阻止用户泄露其凭证(例如,通过网络钓鱼),更不用说保护他们免受他们使用的每一个潜在易受攻击软件的侵害。
与此同时,像可信发布这样的功能可以缩小账户接管的范围:在未来,软件包索引允许软件包选择仅使用可信发布,这样即使软件包索引本身发生ATO,攻击者也无法上传恶意版本。
类似地,“供应链安全”如今风靡一时:公司和个人都在重新审视失控的依赖树及其经常无法问责和追溯的组件。
没有可信发布,GitHub Actions的现状是您信任执行的每一个第三方操作:它们都可以读取您配置的秘密。这是非常不理想的,也是可信发布旨在防范的关键攻击模型之一。
新威胁:“账户复活”和恶意提交者
可信发布之所以有效,是因为它绑定了一个“可信身份”的概念:另一端的可信身份(例如,在GitHub Actions上)是用户/仓库、工作流名称和可选环境名称的元组。
但是等等:如果用户更改了他们的用户名,而攻击者接管了他们的旧用户名,会发生什么?我们称之为“账户复活”,大多数服务都明确支持这一点:用户名并非旨在作为底层身份的永久、稳定标识符。
这打开了一个全新的攻击向量:一个信任hamilcar/cartago的PyPI项目可能突然开始信任一个攻击者控制的hamilcar/cartago,仅仅因为原来的hamilcar现在是hannibal(而合法的hamilcar/cartago现在是hannibal/cartago)。
我们在为PyPI设计可信发布时考虑到了这一点,并与GitHub合作添加了一个额外的声明,该声明不仅将OIDC令牌绑定到用户,还绑定到他们唯一的、稳定的用户ID。这为我们提供了防止复活攻击所需的状态:即使攻击者成功成为GitHub上的hamilcar,他们的底层用户ID也不会改变,PyPI将拒绝他们提供的任何身份令牌。
可信发布还揭示了项目信任模型中一个新的(潜在的)分歧:对于任何给定项目,您是否信任该项目的每个成员也成为潜在的发布者?在许多情况下,答案是肯定的:许多项目只有一两个仓库成员,他们也都是软件包索引上的所有者或具有其他特权。
然而,在某些情况下,答案是否定的:许多项目有数十个低活跃度或不活跃的成员,并非所有人都可能遵循保护其账户的最佳实践。由于社区政策或他们需要访问进行不频繁(但关键)的项目活动,这些成员可能无法被移除。这些用户不应该仅仅因为他们在仓库上拥有提交权限就获得向软件包索引发布版本的能力。
这也是我们在设计可信发布时的一个考量,这就是为什么PyPI的实现支持可选的GitHub Actions环境:对于提交用户和发布用户不完全重叠的社区,可以使用环境来施加额外的工作流限制,这些限制会反映在OIDC令牌中(并随后被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版本是从特定的源代码构件构建的。
如果您或您的公司对此工作感兴趣,请与我们联系!我们拥有多年在开源生态系统开发安全功能的经验,并且一直在寻找更多为关键开源项目和服务做出贡献的方法。