把 CI 里的长期凭证换成 OIDC,以推镜像到 Vercel Container Registry 为例

2026-09-28 CI凭证OIDCVercel安全

9 月 25 日 Vercel 加了一个新的 GitHub Action:vercel/vcr-action/login。它用 workflow 自带的 OIDC 令牌换一张短时 Vercel 访问令牌,拿这张令牌登录 vcr.vercel.com,job 结束时登出并吊销。

要做的就三件事,然后是一段能直接粘的 workflow。

第一步:在 Vercel 上建 OIDC 策略

进 Vercel 团队设置 → Build and Deployment,加一条 VCR 类型的 OIDC 策略。三个字段是关键:

  • Repository:精确到 owner/repo,不要用通配符
  • Workflow:精确到文件名,例如 push-image.yml
  • 权限:Container Registry 的 read-write

策略只匹配这一个仓库的这一个 workflow。别的仓库、别的 workflow 拿不到令牌,这是整套方案的收口点。

第二步:在 GitHub 存变量

这四个都不含密钥,存成 repo variable(Settings → Secrets and variables → Actions → Variables)就行:

变量名说明
VERCEL_TEAM_ID团队 ID,形如 team_xxxxxxxx,不是团队 slug
VERCEL_TEAM_SLUG团队 slug,用在镜像 tag 里
VERCEL_PROJECT_SLUG项目 slug
VCR_REPOSITORY你在 VCR 里的仓库名

最常见的翻车点就是第一个:VERCEL_TEAM_ID 要 ID,镜像 tag 里要 slug,两个长得不一样,填反了会报莫名其妙的鉴权错。

第三步:workflow

name: Push to VCR

on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write

jobs:
  push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Log in to VCR
        uses: vercel/vcr-action/login@v1
        with:
          team: ${{ vars.VERCEL_TEAM_ID }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64
          provenance: false
          tags: vcr.vercel.com/${{ vars.VERCEL_TEAM_SLUG }}/${{ vars.VERCEL_PROJECT_SLUG }}/${{ vars.VCR_REPOSITORY }}:latest
          outputs: type=image,push=true,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true

id-token: write 是必需的,没有它 GitHub 不会签发 OIDC 令牌,login 步骤直接失败。

provenance: false 别删。默认的 provenance attestation 会让镜像多一层 VCR 目前不认的附件,推上去之后拉不下来。

要用 Podman 或 Buildah 而不是 Docker,给 login 步骤加 engines 参数。

推完之后,这个镜像可以直接当 Vercel Sandbox 的自定义镜像用,同一个项目里引用 <repository>:<tag> 即可。

怎么知道自己做对了

这是这篇的重点。跑通一次不算做对,下面 5 条全过才算:

  1. 仓库里没有长期凭证。 gh api repos/OWNER/REPO/actions/secrets --jq '.secrets[].name' 输出里不该出现 VERCEL_TOKEN、VERCEL_ACCESS_TOKEN 之类的东西。有就删掉,然后确认 workflow 还能跑通——能跑通才说明真的没在用它。

  2. 关掉 OIDC 权限后必须失败。 把 permissions.id-token 临时改成 read,重跑。login 步骤一定要失败。如果还成功,说明你还在靠某个 secret 兜底,第 1 条没查干净。

  3. 非白名单来源必须被拒。 从另一个仓库复制同一份 workflow 跑,或者从一个 feature 分支(策略里只放行 main 的)跑。应当被 OIDC 策略拒绝,而不是静默成功。

  4. 策略的匹配范围收得住。 回 Vercel 后台看那条策略,repository 和 workflow 都应该是精确值。写成 owner/* 或留空的,等于把写权限开给了整个组织。

  5. 令牌是短时的。 job 结束后 action 会登出并吊销。你可以在本地试着拿日志里出现过的令牌去调 Vercel API,应该已经失效。如果隔天还能用,说明吊销没生效,这个方案对你就没有意义。

几个坑

  • 区域和团队是两回事。 镜像 tag 里的路径顺序固定是 vcr.vercel.com/<team-slug>/<project-slug>/<repository>,别把 team ID 塞进去。
  • 只支持 linux/amd64。 Sandbox 镜像目前只接受这个架构,加 arm64 会推上去但用不了。
  • fork 的 PR 拿不到你想要的令牌。 来自 fork 的 workflow 权限默认是只读的,id-token: write 不生效。如果合作方要从 fork 推镜像,走 pull_request_target 得额外评估,别默认开。
  • variables 不是 secrets。 团队 slug 这类信息放变量没有问题,但别顺手把任何真实令牌也放进去——它们在日志里是明文可见的。
  • 这是刚上线 3 天的 action。 版本号写死 @v1,不要用 @main。

顺带一句

这套东西的价值不在 Vercel 这一家。OIDC 换短时令牌现在已经是各家 CI 的主流做法——AWS、GCP、Azure、Cloudflare 都支持。你在一个地方配顺了,剩下几个是同一套心智:策略精确到身份,令牌短时,仓库里不留长期凭证。

真正要改的不是配置文件,是习惯。一个人维护十几个仓库的时候,最容易出现的不是被黑,是某个仓库里躺着一个两年前生成、谁也记不清还有什么权限的令牌。OIDC 把这个可能性从源头删掉了。

原文:https://vercel.com/changelog/vcr-login-github-action VCR 文档:https://vercel.com/docs/container-registry GitHub Actions 指南:https://vercel.com/docs/container-registry/github-actions