GitHub Actions needs OIDC audience constraints
GitHub Actions needs OIDC audience constraints
GitHub Actions 需要 OIDC 受众(Audience)约束
TL;DR: GitHub Actions should allow end-users to express audience constraints, to make it harder for an attacker to pivot across services that use independent OIDC-bearing jobs. They could do this with relatively small syntax tweak, although the backend implications are probably nontrivial. 简而言之:GitHub Actions 应允许最终用户设置受众(Audience)约束,以增加攻击者在不同服务间利用独立 OIDC 作业进行横向移动的难度。这可以通过相对较小的语法调整来实现,尽管其后端实现可能并不简单。
Like many CI/CD providers, GitHub Actions provides verifiable machine identities via OpenID Connect (OIDC). These are awesome for a lot of reasons, not least of which is that they allow workflows running on GitHub Actions to federate with other (third-party) services without GitHub having to intermediate and pre-bless every interaction. This is the backbone of how both Trusted Publishing and Sigstore work: an individual workflows on GitHub Actions presents its machine identity (via an OIDC token) to an external service, which then authenticates and for some purpose (uploading to PyPI or signing artifacts, respectively). 像许多 CI/CD 提供商一样,GitHub Actions 通过 OpenID Connect (OIDC) 提供可验证的机器身份。这些身份非常有用,其中一个重要原因是它们允许在 GitHub Actions 上运行的工作流与其它(第三方)服务进行联合认证,而无需 GitHub 作为中间人预先批准每一次交互。这是 Trusted Publishing 和 Sigstore 运作的基石:GitHub Actions 上的单个工作流向外部服务提供其机器身份(通过 OIDC 令牌),外部服务随后进行身份验证并执行特定任务(例如上传到 PyPI 或签署制品)。
Unfortunately, GitHub’s mechanism for exposing OIDC tokens in workflows contains a significant weakness, one that (in my opinion) will present an increasingly serious security risk over time. This post is about that weakness. 遗憾的是,GitHub 在工作流中暴露 OIDC 令牌的机制存在一个重大缺陷,在我看来,随着时间的推移,这将构成日益严重的安全性风险。本文旨在探讨这一缺陷。
CI/CD and OIDC
CI/CD 与 OIDC
At the core of all “OIDC in CI/CD” implementations is some mechanism that allows the workflow (pipeline definition, etc.) to request or otherwise be pre-loaded with an OIDC identity. Here’s how that looks in GitLab CI/CD: 所有“CI/CD 中的 OIDC”实现的核心,都是某种允许工作流(流水线定义等)请求或预加载 OIDC 身份的机制。以下是 GitLab CI/CD 中的实现方式:
my-job:
id_tokens:
PYPI_ID_TOKEN:
aud: pypi
script:
|
do-something.sh --id-token "${PYPI_ID_TOKEN}"
and here’s the equivalent in GitHub Actions: 而在 GitHub Actions 中,等效的实现如下:
my-job:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- run: |
resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi")
oidc_token=$(jq '.value' <<< "${resp}")
do-something.sh --token "${oidc_token}"
The difference between these two is small but important: GitLab requires the OIDC audience (the aud) to be declared up-front and statically, while GitHub requires the workflow to dynamically request a token with an audience selected at runtime (the audience parameter in the HTTP request). 这两者之间的区别虽小但至关重要:GitLab 要求预先静态声明 OIDC 受众(aud),而 GitHub 要求工作流在运行时动态请求一个带有特定受众的令牌(HTTP 请求中的 audience 参数)。
Why does this matter?
为什么这很重要?
First, a very quick foray into OIDC. Under the hood, OIDC is (mostly) just OAuth 2.0, and OIDC identity tokens are just JSON Web Tokens (JWTs), with some additional constraints on the claims they should express. The most important claim in an OIDC ID token is arguably sub, since it identifies the principal (the “subject”). However, the second most important claim is aud, for the audience. The audience is critical because it constrains who honors the token: services that accept ID tokens should only do so when they recognize the audience as matching theirs.
首先,简单了解一下 OIDC。在底层,OIDC(大部分)只是 OAuth 2.0,而 OIDC 身份令牌只是 JSON Web 令牌 (JWT),并对它们应表达的声明(claims)有一些额外约束。OIDC ID 令牌中最重要的声明可以说是 sub,因为它标识了主体(“subject”)。然而,第二重要的声明是 aud,即受众。受众至关重要,因为它限制了谁可以接受该令牌:接受 ID 令牌的服务只有在识别出受众与自身匹配时才应接受。
In other words: the aud claim prevents an ID token that’s intentionally been issued for a specific service from being stolen by the attacker and mis-applied to another service. This is intended as a defense-in-depth: even if an attacker manages to exfiltrate an OIDC credential, they should not be able to pivot to other services it. It’s a flimsy defense but one that’s generally effective, unless you give the attacker the ability to control the aud claim as well.
换句话说:aud 声明防止了为特定服务签发的 ID 令牌被攻击者窃取并滥用到其他服务上。这旨在作为一种纵深防御:即使攻击者设法窃取了 OIDC 凭据,他们也不应能利用它转向其他服务。这是一种脆弱但通常有效的防御手段,除非你让攻击者也有能力控制 aud 声明。
Unfortunately, that’s exactly what GitHub Actions enables: id-token: write gives the job (or entire workflow) the ability to mint any ID token it pleases, with any audience. This matters a great deal in a world (our world) where jobs that are given id-token: write also run a lot of third-party code: any vulnerability (or malware) in that code has the potential to ask for new ID tokens for audiences that it isn’t supposed to have access to.
遗憾的是,这正是 GitHub Actions 所允许的:id-token: write 赋予了作业(或整个工作流)随意铸造任何 ID 令牌的能力,且可以指定任何受众。在我们这个世界中,这一点非常重要,因为被授予 id-token: write 权限的作业通常也会运行大量第三方代码:这些代码中的任何漏洞(或恶意软件)都有可能请求其本不应有权访问的受众的 ID 令牌。
We thought about this problem when designing Trusted Publishing, and came to the conclusion that the machine identity that Trusted Publishing uses must include the workflow name, preventing an attacker from impersonating pypi-publish.yml by inducing an ID token from aws-deploy.yml with aud: pypi. However, this constraint is not suitable for all possible use cases: many integrations want to use just the org/repo slug as a sufficient identity, meaning that all workflows are effectively co-equal when issuing ID tokens.
我们在设计 Trusted Publishing 时考虑过这个问题,并得出结论:Trusted Publishing 使用的机器身份必须包含工作流名称,从而防止攻击者通过从 aws-deploy.yml 获取 aud: pypi 的 ID 令牌来冒充 pypi-publish.yml。然而,这种约束并不适用于所有用例:许多集成只需要 org/repo 标识符作为身份就足够了,这意味着所有工作流在签发 ID 令牌时实际上是平等的。
What should GitHub do?
GitHub 应该怎么做?
Add constraints! Ideally, something like this: 增加约束!理想情况下,应该是这样的:
my-job:
runs-on: ubuntu-latest
permissions:
id-token: [pypi]
The basic idea here is to constrain what audiences the job can request ID tokens for. In the example above, attempting to request a token for aud: sts.amazonaws.com would cause an error, preventing a job that’s intended only for publishing to PyPI from serving as a pivot to AWS.
这里的基本思想是限制作业可以请求哪些受众的 ID 令牌。在上面的例子中,尝试请求 aud: sts.amazonaws.com 的令牌会导致错误,从而防止仅用于发布到 PyPI 的作业被用作访问 AWS 的跳板。
There are, of course, some potential downsides to this. For example, some services might (inadvisedly) require dynamic information in their audience, meaning that a value can’t be statically pre-declared. I think this is rare enough in practice to not be worth blocking a security improvement and, when they do occur, GitHub could continue to allow the id-token: write form as a (less secure!) catch-all.
当然,这也有一些潜在的缺点。例如,某些服务可能(不建议地)要求在受众中使用动态信息,这意味着值无法预先静态声明。我认为这种情况在实践中非常罕见,不值得因此阻碍安全改进。当这种情况确实发生时,GitHub 可以继续允许使用 id-token: write 形式作为一种(安全性较低的!)兜底方案。