基于OIDC实现GitHub Actions免密安全部署至阿里云OSS

📅 2026/8/12 17:06:51
基于OIDC实现GitHub Actions免密安全部署至阿里云OSS
1. 项目概述为什么我们需要更优雅的构建产物同步方案如果你和我一样经常用 GitHub Actions 来自动化项目的构建、测试和部署流程那你肯定遇到过这个经典难题如何安全地把构建好的产物比如打包好的前端静态文件、编译后的二进制程序、或者 Docker 镜像推送到远端的对象存储服务上传统的做法几乎无一例外都是在 GitHub 仓库的 Secrets 里配置一串长长的 AccessKey ID 和 AccessKey Secret。每次 Actions 运行时脚本里读取这两个密钥然后调用阿里云 OSS 的命令行工具或者 SDK 进行上传。这个方法用了好几年看似没问题实则隐患重重。首先密钥管理就是个麻烦事。你得定期轮换密钥吧每次轮换得去阿里云控制台生成新密钥然后手动更新 GitHub 仓库里好几个 Secrets万一漏了一个流水线立马就挂。其次安全风险高。这个密钥一旦配置到 Secrets 里理论上就对拥有仓库写入权限的所有人“可见”虽然不能直接看到明文但能使用它。如果某个协作者的账号被盗或者某个第三方 Action 有恶意行为这个密钥就可能泄露攻击者就能用它在你的 OSS 里为所欲为。最后权限控制太粗。你给的这个密钥往往拥有整个 OSS Bucket 的完全管理权限而你的构建脚本可能只需要上传文件的权限这违背了最小权限原则。所以当我看到阿里云 OSS 支持了 OIDCOpenID Connect联合身份认证并且 GitHub Actions 原生支持 OIDC 来申请云服务商的临时访问凭证时我知道是时候彻底告别硬编码的 AccessKey 了。这个方案的核心就是利用 OIDC 建立 GitHub 和阿里云之间的信任关系。你的 GitHub Actions 工作流在运行时可以向阿里云的安全令牌服务STS证明“我是来自 GitHub 上某个特定仓库的某个特定工作流”阿里云 STS 验证通过后就会颁发一个临时、具有特定权限的安全令牌Token。你的工作流脚本直接用这个临时令牌去操作 OSS整个过程完全不需要预置任何长期密钥。安全、便捷、符合最佳实践这就是“OIDC 免密同步构建产物”要解决的问题。接下来我会带你从零开始手把手搭建这套既安全又高效的自动化流水线。2. 核心原理与架构设计拆解在动手配置之前我们必须先吃透这套方案背后的几个核心概念和它们是如何串联起来的。这能帮你更好地理解每一步配置的意义出问题时也能快速定位。2.1 OIDC 与 GitHub Actions 的信任机制OIDC 是构建在 OAuth 2.0 之上的一个身份层协议。你可以把它理解为一个“标准化的工作证开具流程”。在这个场景里身份提供方 (IdP)GitHub。它负责认证 Actions 工作流这个“员工”的身份。依赖方 (RP)阿里云。它需要验证 GitHub 开具的“工作证”是否真实有效。工作证就是一张由 GitHub 签发的、包含特定声明Claims的 JWTJSON Web Token令牌。这张“工作证”里会写明这个工作流来自哪个仓库repository、由哪个事件触发event_name、正在运行哪个工作流文件workflow甚至具体是哪个提交sha。GitHub Actions runner 在启动任务时会自动从 GitHub 的 OIDC 服务获取这样一张 JWT 令牌并通过环境变量ACTIONS_ID_TOKEN_REQUEST_URL和ACTIONS_ID_TOKEN_REQUEST_TOKEN暴露给工作流步骤。我们后续的步骤就是利用这个令牌去阿里云“换门禁卡”。2.2 阿里云 RAM 角色与信任策略阿里云这边我们不再使用长期固定的用户 AccessKey而是创建一个RAM 角色。RAM 角色本身没有密码和密钥它只是一组权限的集合。关键点在于角色的信任策略。信任策略定义了“谁可以扮演Assume这个角色”。我们要在这里写上 GitHub 的 OIDC 提供商信息并精确地限定允许来自哪些仓库、哪些分支、甚至哪些工作流的工作流来申请扮演这个角色。这就像公司的门卫他只认来自特定合作公司GitHub、并且持有指定工号和部门证明仓库、分支等声明的员工。一个典型的信任策略文档如下所示它精确地限定了权限的边界{ Statement: [ { Effect: Allow, Principal: { Federated: [acs:ram::你的阿里云账号ID:oidc-provider/github] }, Action: sts:AssumeRoleWithWebIdentity, Condition: { StringEquals: { oidc:aud: https://github.com/你的GitHub用户名或组织名, oidc:sub: repo:你的GitHub用户名或组织名/仓库名:ref:refs/heads/main } } } ], Version: 1 }这个策略的意思是允许来自github这个 OIDC 提供商的、aud受众声明为https://github.com/你的用户名的、并且sub主体声明精确匹配repo:用户名/仓库名:ref:refs/heads/main的 JWT 令牌持有者来扮演这个 RAM 角色。2.3 临时凭证的交换与使用流程整个免密同步的流程可以概括为以下几步触发工作流向main分支推送代码触发 GitHub Actions。获取身份令牌GitHub Actions runner 自动从 GitHub OIDC 服务获取 JWT。申请云凭证在工作流步骤中使用actions/github-script或aws-actions/configure-aws-credentials适配阿里云等 Action将 JWT 发送给阿里云 STS 服务的AssumeRoleWithWebIdentity接口。验证与颁发阿里云 STS 验证 JWT 的签名确保证书是 GitHub 发的、有效期以及其中包含的声明是否匹配 RAM 角色的信任策略。验证通过后颁发一组临时安全凭证包含 AccessKeyId, SecretAccessKey, SecurityToken。执行操作工作流后续的步骤如使用aliyun/ossutil或 SDK会利用这组临时凭证来操作 OSS完成文件上传。凭证失效临时凭证通常有效期为 1 小时任务结束后自动失效极大降低了密钥泄露的风险。这套架构将身份认证的动态性和权限管理的精确性结合了起来是云原生 CI/CD 的最佳实践之一。3. 阿里云侧详细配置实操理论清晰后我们进入实战环节。首先在阿里云控制台完成所有必要的配置。3.1 创建 OIDC 身份提供商这是建立信任关系的第一步告诉阿里云“以后会有来自 GitHub 的 OIDC 令牌来找你请你认这个签发者。”登录阿里云控制台进入RAM 访问控制。在左侧导航栏选择身份管理 OIDC 身份提供商。点击创建身份提供商。在创建页面按以下信息填写提供商名称填写github。这个名称会在后续的信任策略中被引用建议保持简洁一致。提供商URL填写https://token.actions.githubusercontent.com。这是 GitHub Actions OIDC 服务的固定地址务必准确。客户端ID这里需要重点理解。客户端ID对应 OIDC 令牌中的aud受众声明。对于 GitHub Actions通常填写你的 GitHub 主页 URL例如https://github.com/your-username。如果你在组织下也可以填写组织的主页 URL如https://github.com/your-org。这个值需要与后续工作流中配置的audience参数以及信任策略里的oidc:aud条件完全一致。点击获取指纹系统会自动从提供的 URL 获取 GitHub OIDC 服务的证书指纹并进行验证。验证成功后点击确认创建。注意客户端ID的选择决定了信任的粒度。如果你只为单个仓库配置填个人主页 URL 即可。如果你希望一个提供商能被组织下多个仓库使用填组织主页 URL 会更灵活但需要在信任策略中通过sub声明来进一步限制具体的仓库。3.2 创建 RAM 角色并配置信任策略接下来创建一个承载具体操作权限的角色并把它和上一步创建的 OIDC 提供商关联起来。在 RAM 控制台进入身份管理 角色。点击创建角色选择身份提供商类型。选择身份提供商在下拉列表中选择你刚刚创建的github。配置角色角色名称例如GitHubActionsDeployToOSS名称要有明确含义。信任策略系统会生成一个模板。我们需要编辑它使其更精确。点击编辑信任策略将内容替换为如下更严格的策略{ Version: 1, Statement: [ { Effect: Allow, Principal: { Federated: [ acs:ram::1234567890123456:oidc-provider/github ] }, Action: sts:AssumeRoleWithWebIdentity, Condition: { StringEquals: { oidc:aud: https://github.com/your-username, oidc:sub: repo:your-username/your-repo:ref:refs/heads/main } } } ] }关键参数解释与替换acs:ram::1234567890123456:oidc-provider/github将1234567890123456替换为你的阿里云账号ID一串数字。账号ID可以在控制台右上角头像处查看。oidc:aud必须与创建 OIDC 提供商时填写的客户端ID完全一致。oidc:sub这是最核心的过滤条件。repo:your-username/your-repo:ref:refs/heads/main表示只允许your-username/your-repo这个仓库的main分支触发的工作流来申请角色。你可以根据需要调整允许所有分支repo:your-username/your-repo:ref:refs/heads/*允许特定环境GitHub Environmentrepo:your-username/your-repo:environment:production允许标签触发repo:your-username/your-repo:ref:refs/tags/*点击下一步此时先不添加任何权限策略直接点击完成创建角色。权限策略我们单独配置这样更清晰。3.3 为 RAM 角色授权 OSS 访问权限角色创建好了但它现在还是个“空壳”没有任何操作资源的权限。我们需要为它绑定一个权限策略。在角色列表中找到刚创建的GitHubActionsDeployToOSS角色点击角色名称进入详情页。切换到权限策略标签页点击添加权限。在授权范围选择整个云账号。在选择权限策略部分我们可以选择系统策略或创建自定义策略。为了遵循最小权限原则强烈建议创建自定义策略。点击创建自定义策略选择脚本编辑。输入策略名称例如GitHubActionsOSSDeployPolicy然后在策略内容中填入{ Version: 1, Statement: [ { Effect: Allow, Action: [ oss:PutObject, oss:GetObject, oss:DeleteObject, oss:ListObjects ], Resource: [ acs:oss:*:*:your-bucket-name, acs:oss:*:*:your-bucket-name/* ] } ] }策略解读Action只授予了上传(PutObject)、下载(GetObject)、删除(DeleteObject)和列举(ListObjects)对象的权限。这已经覆盖了构建产物同步的基本需求。特别注意这里没有授予oss:PutBucket等管理存储空间Bucket的权限角色无法创建或删除 Bucket。Resource将your-bucket-name替换为你实际的 OSS Bucket 名称。acs:oss:*:*:your-bucket-name指向 Bucket 本身用于 ListObjectsacs:oss:*:*:your-bucket-name/*指向 Bucket 内的所有对象用于 Put/Get/Delete Object。这种写法将权限牢牢锁死在指定的 Bucket 内。创建好自定义策略后回到角色授权页面在自定义策略中找到并勾选刚创建的GitHubActionsOSSDeployPolicy点击确定完成授权。至此阿里云侧的配置全部完成。我们创建了一个信任 GitHub 特定仓库的 OIDC 提供商一个与之关联的、拥有指定 OSS Bucket 读写权限的 RAM 角色。接下来我们转到 GitHub 仓库进行配置。4. GitHub Actions 工作流配置详解在 GitHub 仓库中我们需要创建一个工作流文件例如.github/workflows/deploy-to-oss.yml并配置使用 OIDC 进行认证。4.1 基础工作流结构与 OIDC 权限申请首先工作流需要显式声明需要id-token的write权限这是获取 JWT 令牌的前提。name: Deploy to Aliyun OSS via OIDC on: push: branches: [ main ] # 仅在推送到 main 分支时触发 permissions: id-token: write # 这是核心声明需要写入 id-token 的权限 contents: read # 通常还需要读取仓库内容的权限 jobs: build-and-deploy: runs-on: ubuntu-latest steps: # 步骤1: 检出代码 - name: Checkout repository uses: actions/checkoutv4 # 步骤2: 构建你的项目 (此处以Node.js项目为例) - name: Build project run: | npm ci npm run build # 假设构建产物输出到 dist 目录 # 后续步骤配置阿里云凭证并上传4.2 使用官方 Action 配置阿里云临时凭证GitHub 社区有成熟的 Action 可以帮助我们完成“用 JWT 换取阿里云 STS 令牌”的过程。这里我推荐使用aliyun/configure-oidc-credentials这个官方 Action它对阿里云的支持最直接。在上面的工作流中在构建步骤之后添加如下步骤# 步骤3: 配置阿里云 OIDC 临时凭证 - name: Configure Aliyun Credentials via OIDC uses: aliyun/configure-oidc-credentialsv1 with: role-session-name: github-actions # 会话名称可自定义 role-arn: acs:ram::1234567890123456:role/GitHubActionsDeployToOSS # 替换为你的角色ARN oidc-provider-arn: acs:ram::1234567890123456:oidc-provider/github # 替换为你的OIDC提供商ARN audience: https://github.com/your-username # 必须与创建提供商时的客户端ID一致 mask-secret: true # 隐藏敏感输出推荐开启参数详解role-arn你在阿里云创建的 RAM 角色的 ARN。格式为acs:ram::账号ID:role/角色名称。oidc-provider-arn你在阿里云创建的 OIDC 身份提供商的 ARN。格式为acs:ram::账号ID:oidc-provider/提供商名称。audience必须与阿里云 OIDC 提供商配置中的“客户端ID”以及信任策略中的oidc:aud条件完全一致。mask-secret设置为true后Action 输出的临时密钥会在日志中被隐藏增强安全性。这个 Action 执行成功后它会将获取到的临时安全凭证AccessKeyId, SecretAccessKey, SecurityToken自动注入到当前 job 的环境变量中通常命名为ALIBABACLOUD_ACCESS_KEY_ID,ALIBABACLOUD_ACCESS_KEY_SECRET,ALIBABACLOUD_SECURITY_TOKEN。同时它也会配置好阿里云 CLI 的默认配置文件使后续的aliyun或ossutil命令能直接使用这些凭证。4.3 使用 ossutil 同步构建产物配置好凭证后就可以使用阿里云 OSS 的命令行工具ossutil来上传文件了。我们可以使用另一个官方 Actionaliyun/ossutil它预装了ossutil并会自动使用上一步配置的凭证。# 步骤4: 使用 ossutil 上传构建产物到 OSS - name: Upload to Aliyun OSS uses: aliyun/ossutilv1 with: # 使用上一步配置的 OIDC 凭证无需额外指定 access-key command: cp -r ./dist oss://your-bucket-name/your-prefix/ --meta Cache-Control:no-cache --update命令解释cp -r ./dist oss://your-bucket-name/your-prefix/递归地将本地dist目录下的所有文件上传到 OSS Bucket 的your-prefix/目录下。--meta Cache-Control:no-cache为上传的文件设置 HTTP 头Cache-Control: no-cache。这对于前端静态资源非常有用可以避免浏览器缓存旧版本。你可以根据需求设置其他元信息如Content-Encoding: gzip。--update只上传发生变化的文件跳过未修改的文件能显著提升上传速度。如果你需要更复杂的同步逻辑比如删除远端已不存在于本地的文件可以使用ossutil sync命令command: sync ./dist oss://your-bucket-name/your-prefix/ --delete --meta Cache-Control:max-age3600--delete参数会使远端与本地严格同步本地没有的文件在远端也会被删除使用时需谨慎。4.4 完整工作流文件示例将以上所有步骤整合一个完整的、使用 OIDC 免密同步到阿里云 OSS 的工作流文件如下name: Deploy to Aliyun OSS via OIDC on: push: branches: [ main ] # 你也可以添加 release 触发 # release: # types: [published] permissions: id-token: write contents: read jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkoutv4 - name: Setup Node.js uses: actions/setup-nodev4 with: node-version: 18 - name: Install dependencies and build run: | npm ci npm run build - name: Configure Aliyun Credentials via OIDC uses: aliyun/configure-oidc-credentialsv1 with: role-session-name: github-actions-${{ github.sha }} role-arn: acs:ram::1234567890123456:role/GitHubActionsDeployToOSS oidc-provider-arn: acs:ram::1234567890123456:oidc-provider/github audience: https://github.com/your-username mask-secret: true - name: Upload to OSS uses: aliyun/ossutilv1 with: command: cp -r ./dist oss://your-production-bucket/static-site/ --meta Cache-Control:public,max-age31536000 --update5. 高级配置、调试与安全最佳实践基础流程跑通后我们还需要关注一些高级场景和安全细节让整个方案更健壮、更安全。5.1 多环境与分支策略管理在实际项目中我们通常有开发、测试、生产等多个环境。通过精细化的信任策略和工作流条件可以实现一套配置管理多环境。1. 阿里云侧为不同环境创建不同角色这是最清晰、最安全的方式。例如角色GitHubActionsDeployToOSS-Staging信任策略限定为repo:xxx/xxx:ref:refs/heads/develop并授权访问staging-bucket。角色GitHubActionsDeployToOSS-Prod信任策略限定为repo:xxx/xxx:ref:refs/heads/main或repo:xxx/xxx:environment:production并授权访问production-bucket。2. GitHub Actions动态选择角色和 Bucket在工作流中可以使用 GitHub 上下文和环境变量来动态决定使用哪个角色 ARN 和 Bucket 名称。env: # 根据分支名设置环境变量 DEPLOY_ENV: ${{ github.ref refs/heads/main production || staging }} jobs: deploy: runs-on: ubuntu-latest environment: ${{ env.DEPLOY_ENV }} # 使用环境可以在GitHub仓库设置环境变量和Secrets steps: - name: Configure Aliyun Credentials uses: aliyun/configure-oidc-credentialsv1 with: role-arn: ${{ env.DEPLOY_ENV production acs:ram::xxx:role/prod-role || acs:ram::xxx:role/staging-role }} # ... 其他参数 - name: Upload to OSS uses: aliyun/ossutilv1 with: command: cp -r ./dist oss://${{ env.DEPLOY_ENV production prod-bucket || staging-bucket }}/path/ --update使用environment关键字还有一个好处你可以在 GitHub 仓库的 “Settings Environments” 中为production环境配置审批流程要求必须手动批准后才能运行部署步骤这为生产环境部署增加了一道安全闸门。5.2 调试如何查看与验证 JWT 令牌内容当配置不成功时第一步是确认 GitHub 发出的 JWT 令牌内容是否符合你的预期。你可以在工作流中添加一个调试步骤来打印令牌的声明Payload。- name: Debug OIDC Token run: | # 请求并解码 JWT 令牌的 Payload 部分 curl -s -H Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN \ $ACTIONS_ID_TOKEN_REQUEST_URLaudiencehttps://github.com/your-username \ | jq -r .value | cut -d . -f 2 | base64 -d | jq .这个步骤会向 GitHub 的 OIDC 服务请求一个针对特定audience的 JWT然后解码并漂亮地打印出其中的声明。你可以检查sub、repository、ref等字段是否与你在阿里云 RAM 角色信任策略中配置的条件完全匹配。注意调试完成后请务必移除或注释掉这个步骤避免令牌信息在日志中泄露。5.3 安全最佳实践与注意事项最小权限原则这是黄金法则。为 RAM 角色授权的策略权限范围必须精确到所需的 Bucket 和所需的 API 操作如oss:PutObject。切勿直接使用oss:*或*:*。精确的信任策略尽量在信任策略的Condition中使用最精确的匹配条件。例如生产环境角色应限定为main分支或production环境而不是refs/heads/*。使用环境进行生产隔离如前所述利用 GitHub Environments 为生产部署设置审批人和环境特定的变量增加一道人工确认屏障。定期审计定期在阿里云 RAM 控制台的“操作日志”中查看AssumeRoleWithWebIdentity事件确认扮演角色的请求来源、时间、IP 是否符合预期。令牌有效期阿里云 STS 颁发的临时凭证默认有效期为 1 小时这已经足够一次 CI/CD 运行。无需修改短有效期是安全优势。避免在日志中输出敏感信息确保mask-secret: true被设置并且不要在run步骤中直接echo包含凭证的环境变量。6. 常见问题排查与解决方案实录在实际配置过程中你可能会遇到一些错误。下面是我总结的几个典型问题及其排查思路。6.1 错误“The requested role is not authorized for use.”错误信息在 GitHub Actions 日志中Configure Aliyun Credentials步骤失败提示AssumeRoleWithWebIdentity调用失败原因The requested role is not authorized for use.或The role is not authorized for use.排查思路检查角色 ARN 和提供商 ARN确保工作流 YAML 中填写的role-arn和oidc-provider-arn完全正确包括账号 ID 和名称的大小写。核对信任策略的主体登录阿里云控制台检查 RAM 角色的信任策略。确保Principal中的Federated值格式正确且包含了你创建的 OIDC 提供商 ARN。验证 Condition 条件这是最常见的问题。使用上文提到的“调试 OIDC Token”步骤获取实际的 JWT 声明。然后逐字对比工作流中audience参数的值、OIDC 提供商配置的“客户端ID”、信任策略中的oidc:aud条件三者必须完全一致。JWT 中的sub声明值是否与信任策略中的oidc:sub条件匹配。特别注意ref部分如果你在push到feature/xxx分支时触发sub会是repo:username/repo:ref:refs/heads/feature/xxx而你的策略如果只允许main分支就会失败。检查 OIDC 提供商状态确认 OIDC 提供商已成功创建且“提供商URL”正确。6.2 错误“ossutil: AccessDenied”错误信息Configure Aliyun Credentials步骤成功但Upload to OSS步骤失败报错AccessDenied。排查思路检查 RAM 角色权限策略这是根本原因。进入 RAM 角色详情检查其被授权的权限策略。确认策略中的Action包含了你要执行的操作如oss:PutObject并且Resource精确指向了你试图操作的 Bucket 和对象路径如acs:oss:*:*:your-bucket-name/*。检查 Bucket 名称和路径确保ossutil命令中的 Bucket 名称 (oss://your-bucket-name/) 没有拼写错误且该 Bucket 确实存在于你的阿里云账号下。检查 Bucket 权限虽然角色有策略但 Bucket 自身的 ACL 或 Policy 如果显式拒绝Deny了请求也会导致AccessDenied。检查 Bucket 的公共读写设置和授权策略确保没有冲突的拒绝规则。6.3 错误“Invalid identity token.”错误信息Configure Aliyun Credentials步骤失败提示Invalid identity token.排查思路检查 OIDC 提供商配置确保在阿里云创建的 OIDC 提供商“提供商URL”填写的是https://token.actions.githubusercontent.com一个字母都不能错。检查网络连通性极少数情况下GitHub Actions runner 的网络可能无法访问阿里云 STS 服务。可以尝试在步骤中添加retry-on-error: true如果 Action 支持或检查 runner 所在地区的网络出口。令牌格式问题确保工作流中permissions设置了id-token: write。没有这个权限后续步骤获取不到有效的 JWT。6.4 上传速度慢或部分文件失败问题现象ossutil cp或sync命令执行时间过长或大量小文件上传时部分失败。优化建议使用--update参数如示例所示只上传变化的文件。调整ossutil并发和分片设置对于大量小文件或大文件可以通过环境变量调整ossutil的性能参数。在Upload to OSS步骤前设置- name: Upload to OSS env: OSSUTIL_MAX_UPLOAD_JOBS: 20 # 增加上传并发数 OSSUTIL_PART_SIZE: 1048576 # 设置分片大小为1MB针对小文件优化 uses: aliyun/ossutilv1 with: command: sync ./dist oss://your-bucket/path/ --update检查网络和 Bucket 区域确保 GitHub Actions runner 的地域与你 OSS Bucket 的地域尽可能接近。例如Bucket 在华东1杭州可以选择runs-on: ubuntu-latest默认可能在美西也可以考虑使用阿里云自己的 GitHub Actions runner如果可用或选择其他地域的 runner。分步上传如果目录非常大可以考虑按子目录分批上传或者使用ossutil的cp命令配合--include/--exclude模式过滤文件。从长期维护的角度看OIDC 免密方案将密钥管理、权限控制和审计追踪的责任清晰地划分给了云平台和代码平台开发者只需要关注仓库和角色的映射关系。一旦配置完成后续的密钥轮换、权限变更都变得非常直观和安全。我自己的项目全面切换到这套方案后再也没为 AccessKey 泄露或过期的问题困扰过部署流程既安全又省心。如果你还在使用硬编码密钥强烈建议花一两个小时迁移过来这笔时间投资在安全性和运维效率上的回报是巨大的。