Martial Wehrling
← All articles
By Martial Wehrling6 min read

Turborepo: securing the Vercel remote cache with OIDC

turborepo

Since late July 2026, Vercel's remote cache accepts OIDC authentication for Turborepo. In practice, your CI pipelines can finally read from and write to the cache without a personal access token stored in the repository secrets. I migrated my workflows as soon as it was announced, and I wouldn't go back.

Why permanent tokens bothered me

If you use Turborepo in CI, you know the routine: for the remote cache to work, you need a TURBO_TOKEN. Until recently, that meant generating a Personal Access Token on your Vercel account, then pasting it into the repository's GitHub secrets.

Once created, that token stays valid until it is manually revoked. It sits in the repository settings, it can leak into a poorly masked build log, and nobody thinks of rotating it. When a contractor leaves the project, do you revoke the token? In theory, yes. In practice, people forget.

Credentials leaking through CI logs or misconfigured workflows are a classic in incident reports. Rather than "being more careful", I preferred to get rid of the permanent secret altogether.

The principle: exchanging an identity for a token

OIDC (OpenID Connect) lets the pipeline prove who it is without a shared secret. GitHub Actions issues a signed identity token containing verifiable claims: the repository, the branch, the workflow, the actor who triggered the run.

Vercel verifies the signature and the claims, then issues a short-lived Turborepo access token in exchange. And here is the detail that changes everything: this token only grants access to the remote cache. It can't be used to deploy, modify the project or call the rest of the Vercel API. Even if intercepted, it is useless beyond the cache, and it expires on its own.

Configuration on the Vercel side

Go to your team settings: Build and Deployment → OIDC Policies for CLI Access, then create a Turborepo CLI Policy.

For GitHub, the form is simple: you select your GitHub account and the relevant repository from drop-down lists. No need to type the issuer URL by hand — Vercel already knows GitHub Actions. You can then restrict the policy to a specific branch or workflow, and customize the audience if your setup requires it.

My method: I start with a policy limited to the repository, I test it, then I narrow it down to the main branch and the build workflow. If several policies can match the same repo, note the policy ID — you'll need it on the CI side.

On the GitHub Actions side

Two changes are enough. First, allow the job to request an OIDC token:

permissions:
  id-token: write
  contents: read

Then add the official action before any turbo command:

- uses: vercel/setup-turborepo-remote-cache-action@v1 # check the latest tag on the repository
  with:
    team: ${{ vars.TURBO_TEAM }}

The action does all the work: it requests the OIDC token from GitHub, exchanges it for a short-lived Turborepo token, then exports TURBO_TOKEN and TURBO_TEAM for the following steps. If several policies match your repository, add the policy parameter with the ID you noted earlier.

A small detail people don't always think of: team is stored in a repository variable (vars), not in the secrets. It's just a team slug, nothing sensitive.

The manual version, to understand the mechanism

If you want to see what the action does under the hood, GitHub exposes two variables: ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN. Watch out for the trap — the second one is not the OIDC token, it's the bearer token that authorizes your job to request it. The full flow:

# 1. Request the OIDC token from GitHub
OIDC_TOKEN=$(curl -s \
  -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
  "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=<your-policy-audience>" | jq -r .value)

# 2. Exchange it for a Turborepo token
curl -s -X POST https://api.vercel.com/login/oauth/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
  -d subject_token=$OIDC_TOKEN \
  -d policy_id=<policy-id>

The response contains the token to export into TURBO_TOKEN. In the vast majority of cases, the official action is enough — but knowing how to do the exchange by hand helps a lot when you need to debug.

The real limits

The only weak point is the dependency on Vercel's exchange endpoint. If api.vercel.com is unavailable at build time, there's no token, so no cache. The build doesn't fail, though: Turborepo falls back to a cache miss and rebuilds everything. It's slower, but it works. I still keep a backup PAT in an external secrets manager, with a written procedure to activate it manually — outside the repository, and never enabled by default.

Second point to watch: the policy applies at the repository level. Without restricting it by branch or workflow, any run in the repo — including one from a pull request coming from a fork — can obtain a token. Admittedly, that token only touches the cache. But write access to the cache is a poisoning risk: someone could drop modified artifacts there that your builds would then run. Restrict the policy to protected branches, and avoid writing to the cache on unverified PRs.

How I validate a new configuration

Before rolling it out everywhere, I run it on a test branch and check three things in the logs:

  1. The action runs without errors and correctly exports TURBO_TOKEN (masked) and TURBO_TEAM.
  2. On the second run, Turborepo shows cache hits — the famous >>> FULL TURBO is the sign that everything works.
  3. The old PAT has been removed from the repository secrets. Otherwise, the migration was pointless.

There's no need to test token expiry by hand: it is short-lived by design, and it may stay valid for a while after the job ends — that's not a bug, it's simply its lifetime.

FAQ

What if my build lasts longer than the token? Vercel doesn't publish an exact duration, but the token is designed to cover a run. If your builds are really long, split them into several jobs: each job redoes the exchange and starts with a fresh token.

Does it work with GitLab CI? Yes. GitLab issues an OIDC token via CI_JOB_JWT_V2, or via the id_tokens block if you want to choose the audience. On the Vercel side, if your provider doesn't appear in the list, the policy lets you manually configure the issuer and the expected claims. The exchange itself stays the same.

How do I audit cache access? As far as I know, Vercel doesn't offer an access log dedicated to the remote cache. The most reliable trace remains your pipeline logs, where every token exchange appears. That's actually one more argument for OIDC: every access is tied to a specific run, with an identified actor and branch, instead of an anonymous token shared by the whole team.

Sources