
1. 先搞清楚一个问题OIDC 令牌被拿到到底意味着什么如果你最近开始把 GitHub Actions 接到云厂商的凭证体系里比如 AWS、Azure 或 Google Cloud那你一定绕不开一个概念OpenID ConnectOIDC。不少团队用它替代长期密钥把 GitHub Actions 工作流里的云凭证换成了短期令牌。这个方向本身没有错甚至可以说是目前比较推荐的做法。但很多人刚上手时只关注“能不能跑通”忽略了一个非常关键的安全细节——audience constraints也就是受众约束。先说一个具体场景。假设你的仓库里有这样一段 Actions 工作流它通过aws-actions/configure-aws-credentials这个 action用 OIDC 方式向 AWS 申请一个临时凭证然后执行部署、上传对象或操作 Kubernetes 之类的任务。流程是这样的GitHub 的 OIDC provider 会签发给你的工作流一个 JWT 令牌这个令牌会带着你的仓库名、分支名、环境名等信息云平台验证这个令牌后再按照你配置的角色信任策略授予对应的权限。听起来很顺。但这里有一个容易忽略的点这个 JWT 令牌里有一个字段叫aud也就是 audience表示这个令牌是给谁用的。如果这个字段的值设置得太宽甚至允许客户端自己指定成任意值那么攻击面就会出现。更具体地说如果攻击者能让 GitHub Actions 在某个你控制的仓库里运行而这个仓库里的工作流又能向同一个 OIDC provider 请求令牌那么当云平台的信任策略没有严格校验aud字段时攻击者就有可能通过构造一个和自己的仓库匹配的 token冒充你原本的部署流程换取拥有高权限的云临时凭证。很多人会觉得云平台的信任策略里明明写了sub字段要匹配某个仓库。但问题在于sub往往只能说明“这个令牌确实来自 GitHub Actions 的某个仓库”。如果攻击者也能运行 GitHub Actions——比如 fork 了你的仓库或者你使用了pull_request_target这类处理 fork 的机制——那sub校验就不够用了。这时候aud字段就成了一个额外的边界。换句话说OIDC 的 audience 约束不是一个可选的“锦上添花”配置而是整个信任链里可以决定“这个令牌到底给谁用”的关键边界。这个问题的本质是当我们用 OIDC 替换长期密钥时并不是简单地把“一个不变的密码”换成了“一个会过期的密码”而是把信任模型从“你拥有什么”变成了“你是谁、你在哪、你被允许干什么”。而 audience 约束就是“你被允许干什么”的底层语言之一。2. 为什么很多人没有注意到 audience 约束我见过很多团队把 OIDC 接入 Actions 时第一版配置都是从官方文档或社区模板复制过来的。比如 AWS 的信任策略里通常会有这样一段{ Version: 2012-10-17, Statement: [ { Effect: Allow, Principal: { Federated: arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com }, Action: sts:AssumeRoleWithWebIdentity, Condition: { StringEquals: { token.actions.githubusercontent.com:sub: repo:your-name/your-repo:ref:refs/heads/main } } } ] }这段配置看起来没问题它限定了只有your-name/your-repo的main分支才能换取这个角色。但如果你仔细看就会发现里面没有对aud做任何限制。也就是说只要 GitHub 的 OIDC provider 签发了令牌云平台不关心这个令牌的aud是给谁的它只检查sub是否匹配。这带来一个风险。AWS 的 OIDC provider 配置里支持自定义audienceGitHub Actions 在默认情况下发出的令牌aud值是sigstore。如果你在使用aws-actions/configure-aws-credentials时没有显式指定audience那默认值就是这个。而配置 AWS 侧的 OIDC provider 时也可以指定一个 audience 值但要和令牌里的aud匹配才能通过校验。问题往往出在两类场景。第一类你配置了多个仓库或组织共用同一个 OIDC provider但没有为每个仓库单独配置 audience。结果就是任何能触发工作流的仓库只要sub匹配比如 fork 后使用pull_request_target的仓库或者仓库被攻击者控制后创建的 branch就能换取这个角色。第二类你在 Actions 工作流里显式指定了audience但云平台侧的信任策略根本没有校验这个值。这种情况更隐蔽因为流程是通的你不会意识到信任策略里缺少关键条件。更麻烦的是很多云平台的默认配置模板不会强制要求填写 audience。有些团队为了“先跑通”直接使用了sts:AssumeRoleWithWebIdentity而没有附加任何 audience 条件。等到生产环境出了事故才意识到问题。我倾向于把这里理解为“门禁系统只检查了你有没有工牌但没有检查工牌上写的是哪个部门”。OIDC 解决的是“你是不是 GitHub Actions”的问题但没有解决的“你是哪个仓库、哪条分支、哪个环境”的问题就需要条件表达式来约束。而aud恰恰是这一层约束的重要一环。所以当你听到“GitHub Actions needs OIDC audience constraints”这句话时不要先把它当成一个复杂的安全理论。它真正在说的是你的信任链上可能少了一个关键的验证条件。3. 三种常见配置方式以及它们各自的风险边界3.1 默认配置最简单但最容易忽略风险最常见的做法是在 Actions 工作流里直接用官方 action不显式指定audience。- name: Configure AWS credentials uses: aws-actions/configure-aws-credentialsv4 with: role-to-assume: arn:aws:iam::123456789012:role/my-deploy-role aws-region: us-east-1这种情况下GitHub 签发的 JWT 令牌中aud的值默认为sigstore。如果 AWS 侧配置 OIDC provider 时把 audience 也设置成sigstore就可以正常换取凭证。但风险在于一旦攻击者也能触发这个工作流他并不需要更改aud字段因为你的信任策略根本没有限制 audience。他只要让sub匹配即可。更隐蔽的风险是如果同一个 AWS 账号下存在多个 GitHub 仓库使用同一个 OIDC provider攻击者只需要找到任意一个能触发工作流的仓库就能尝试换取这个角色。3.2 显式指定 audience但云平台侧并未校验有些团队会在工作流里显式指定audience比如- name: Configure AWS credentials uses: aws-actions/configure-aws-credentialsv4 with: role-to-assume: arn:aws:iam::123456789012:role/my-deploy-role aws-region: us-east-1 audience: my-repo-audience同时在 AWS 侧配置 OIDC provider 时把 audience 设置为my-repo-audience。这样流程可以正常通过因为令牌的aud匹配了 provider 的 audience。但请注意AWS 的 OIDC provider 可以通过一个 provider 支持多个 audience。你可以在创建 provider 时添加多个值。此时如果你在信任策略中没有额外校验aud那么所有支持该 audience 的仓库都能换取角色。也就是说OIDC provider 的 audience 配置和信任策略中的 audience 校验是两个不同层面的问题。前者是“这个 token 能不能被这个 provider 使用”后者是“这个 token 能不能换取这个角色”。很多人把这两者混为一谈认为只要 provider 配了 audience角色就安全了。实际上角色信任策略里必须同时声明条件才能完成最终授权。3.3 推荐的完整配置sub 和 aud 双重校验更稳妥的配置是在角色信任策略里同时加上sub和aud校验。以 AWS 为例一个更完整的信任策略可以这样写{ Version: 2012-10-17, Statement: [ { Effect: Allow, Principal: { Federated: arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com }, Action: sts:AssumeRoleWithWebIdentity, Condition: { StringEquals: { token.actions.githubusercontent.com:aud: my-repo-audience, token.actions.githubusercontent.com:sub: repo:your-name/your-repo:ref:refs/heads/main } } } ] }这里的关键变化是aud不再是 OIDC provider 层面的“隐含配置”而是角色信任策略里的一个显式条件。即使攻击者能让sub匹配他仍然过不了aud这一关因为他的令牌aud值不是你期望的那个。对于 Azure 用户类似。az cli 使用的 OIDC 登录方式在配置 federated credential 时你也可以指定audience。对于 GitHub Actions这个值通常是api://AzureADTokenExchange。你可以在 GitHub Actions 工作流里通过azure/loginaction 指定audience参数同时在 Azure 侧配置 federated credential 时选择对应的 audience。对于 Google Cloudgoogle-github-actions/auth也支持audience参数但一般建议你在 Workload Identity Provider 的配置里明确设置允许的受众。我的建议是不管云平台是什么都应该形成一个固定习惯——角色信任策略里同时校验sub和aud。这不是某个云平台特有的做法而是 OIDC 模型里最朴素的安全边界。4. 为什么 fork 场景和 pull_request_target 会让问题放大如果说上面的配置细节还只是“风险潜伏”那 fork 场景和pull_request_target就是最容易直接引爆风险的地方。如果你没有给 OIDC 分配 audience 约束配合pull_request_target这类事件攻击面就会明显扩大。先讲一下pull_request_target是什么。它允许 GitHub Actions 工作流在处理来自 fork 仓库的 PR 时使用原仓库的 secrets 和权限。这个机制本身是为了解决 fork 仓库无法安全访问 secrets 的问题但它有一个特点工作流代码来自目标仓库而不是 fork 仓库。这意味着攻击者可以通过修改 PR 标题、分支名或其他可控字段让工作流执行一些非预期操作。当你把 OIDC 凭证也加入到这个链条里时风险链路就变成攻击者 fork 你的仓库创建一个分支提交一个包含恶意内容的 PR。你的仓库配置了pull_request_target事件触发了部署或测试工作流。工作流通过 OIDC 向云平台请求凭证。如果信任策略只校验了sub而sub里可能有分支名攻击者可以把自己的分支命名为main或refs/heads/main之类的可匹配值。云平台将高权限临时凭证交给了工作流。攻击者利用 PR 中可控的输入让工作流执行了恶意操作。当然sub里可以更精确地匹配refs/heads/main来限制分支。但如果你没有同时校验aud攻击者如果能找到其他满足sub条件的路径风险依然存在。有一个更致命的组合如果工作流中使用了pull_request_target并且还允许读取 PR 标题或描述来动态决定部署逻辑攻击者就能通过一个看起来无害的 PR让工作流以高权限凭证执行任意命令。这就是为什么我反复强调OIDC audience 约束不是一个高级话题它是在多分支、多仓库、多贡献者场景下保障安全的基础条件。如果只是自己的私人仓库只有你自己能触发工作流那风险相对可控。但一旦涉及团队协作、开源项目或使用 fork 的流程你就必须把 audience 约束纳入标配。5. 如何落地一个 5 步检查清单讲了这么多如果只看结论你可能觉得“那就加一个aud条件不就行了吗”。但实际落地时还需要考虑到配置位置、参数传递、版本兼容等多个环节。下面是一个我常用的 5 步检查清单可以帮助你逐步排查。5.1 第一步确认 OIDC provider 配置在你的云平台上找到 OIDC provider 配置。对于 AWS是 IAM Identity providers对于 Azure是 Entra ID App registrations Certificates secrets Federated credentials对于 Google Cloud是 IAM Workload Identity Federation。需要确认的事项包括Provider 的 URL 是否正确https://token.actions.githubusercontent.com。支持的 audience 列表是否符合预期。是否允许多个 audience。如果允许要了解每个 audience 对应哪个仓库或环境。这里最容易犯的错误是为了省事把所有仓库都放在同一个 audience 下。这样做虽然方便但也意味着一旦某个仓库被攻破攻击者会尝试用它去换取所有角色的凭证。更合理的做法是为不同权限级别的角色配置不同的 audience。5.2 第二步检查角色信任策略找到你希望让 GitHub Actions 使用的角色或 workload identity provider。检查信任策略里是否包含aud条件。对于 AWS搜索 JSON 文档中是否有token.actions.githubusercontent.com:aud这个 key。如果只有sub缺少aud就要补上。一个常见的情况是信任策略中aud的值写的和 OIDC provider 中配置的 audience 不一致。例如provider 配置的 audience 是sigstore但信任策略里写的却是my-audience。这会导致正常的 Actions 工作流也无法换取凭证。所以在修改后一定要跑一次最小工作流做验证。5.3 第三步配置 Actions 工作流在 Actions 工作流中根据云平台和 action 类型显式指定 audience 参数。AWS 示例- name: Configure AWS credentials uses: aws-actions/configure-aws-credentialsv4 with: role-to-assume: arn:aws:iam::123456789012:role/my-deploy-role aws-region: us-east-1 audience: my-repo-audienceAzure 示例- name: Azure Login uses: azure/loginv1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} audience: api://AzureADTokenExchangeGoogle Cloud 示例用 action 时audience一般会自动生成但也可以显式指定- name: Authenticate to Google Cloud uses: google-github-actions/authv2 with: workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider service_account: my-samy-project.iam.gserviceaccount.com还需要检查你使用的 action 版本是否支持自定义 audience 参数。以aws-actions/configure-aws-credentials为例从 v4 开始支持audience输入。如果你还在用 v1 或 v2需要先升级 action 版本。5.4 第四步验证完整链路修改配置后不要只看“工作流变绿了”就结束。建议做以下验证在 Actions 日志中确认工作流日志里显示的 role ARN 和租户 ID 是正确的。手动在云平台 CLI 中测试 AssumeRoleWithWebIdentity 或等效操作确认角色信任策略生效。尝试用一个不符合aud条件的仓库或分支触发工作流确认它无法换取凭证。对pull_request_target场景找一个 fork 仓库提交一个测试 PR确认工作流不会错误获得高权限凭证。只有当一个“错误身份”的工作流被成功拦截时你才能确定 audience 约束真正生效了。5.5 第五步记录和定期复查OIDC audience 配置不是一次性工作。当新增仓库、新增环境或调整角色权限时信任策略都可能需要更新。建议在团队的基建文档中记录以下几点每个角色对应的 GitHub 仓库和分支。每个仓库使用的 audience 值。修改信任策略时的验证步骤。最近一次 OIDC 配置审查的日期。我见过不少团队在配置完 OIDC 后就不管了直到某个云安全扫描工具报告说“某个角色的信任策略缺少 audience 条件”才意识到问题。定期复查应该成为基础设施变更流程的一部分。6. 常见误区和排查路径6.1 误区一只要换掉长期密钥就安全了OIDC 替换长期密钥只是把“静态凭证”换成了“动态令牌”。如果信任策略本身没有足够的条件约束短生命周期令牌一样可以被滥用。角色信任策略中的条件粒度决定了安全边界的大小。6.2 误区二sub已经能精准定位仓库不需要audsub确实能定位仓库和分支但多仓库、多分支、fork 场景下仍可能存在绕过路径。aud提供了一个额外的、独立于仓库名的控制维度可以在不同仓库之间划分边界。两者不是二选一而是组合使用。6.3 误区三OIDC provider 配置好 audience 就够了OIDC provider 里的 audience只是保证令牌能进入云平台。最终角色能否被换取取决于信任策略。这是两个独立的检查点。只配置 provider 而不配置信任策略相当于门禁系统认识你的工牌但某个房间的门没有锁。6.4 排查路径如果配置了aud之后工作流突然失败了按照下面的顺序排查看 Actions 日志里的报错信息。如果提示类似Access denied或Not authorized先确认角色信任策略有没有生效。看工作流中audience参数的值和云平台 OIDC provider 中配置的值是否一致。看角色信任策略里条件 Key 的完整名称。AWS 里通常是token.actions.githubusercontent.com:aud不要把aud写成别的前缀或缺少 provider 域名前缀。检查 OIDC provider 是否支持该 audience。AWS 的 OIDC provider 可以配置多个 audience但最终sts:AssumeRoleWithWebIdentity使用时令牌里的aud必须已经在 provider 的 audience 列表里。如果一切看起来正确但仍然失败在云平台侧启用详细日志查看具体的拒绝原因。6.5 如果只是学习和尝鲜可以做轻量配置如果只是学习 OIDC 机制或者仓库里没有任何敏感资产先按默认配置跑通也可以。但一旦涉及生产环境、云资源、数据库或 Kubernetes 集群就应该立即补上 audience 约束。7. 最后说一点更通用的经验OIDC audience 约束看似只是一个字段但它背后是一个真正重要的工程习惯不要因为流程能跑通就跳过安全边界的审视。我接触过不少团队第一次接入 OIDC 时最关心的是“这个 action 怎么用”“怎么让工作流不报错”。这些当然重要但真正的分水岭在于你是否在跑通之后愿意花半个小时去看一眼信任策略里的每个条件。这不是一个能立刻看到回报的工作。它更像是在流程正常运转时预埋的一条安全底线。如果你现在正准备接入 OIDC我的建议很简单在自己的最小示例里从一开始就把aud条件加上。不要等出现安全事故再补。因为当你回头再补的时候可能已经忘记当初为什么这么配置或者已经有很多工作流依赖旧的信任策略改动成本会更高。把这个习惯固定下来之后你会发现OIDC 的好处不只是省去了管理密钥的麻烦更在于它迫使你把“身份边界”这个命题想清楚。而 audience 约束就是这个命题里最基础也最不能跳过的部分。