Martial Wehrling
← Tous les articles
Par Martial Wehrling6 min de lecture

Turborepo : sécuriser le cache distant Vercel avec OIDC

turborepo

Sécuriser le cache distant Vercel avec OIDC : remplacer les jetons permanents par une identité CI éphémère

Depuis fin juillet 2026, le cache distant de Vercel accepte l'authentification OIDC pour Turborepo. Concrètement, vos pipelines CI peuvent enfin lire et écrire dans le cache sans jeton d'accès personnel stocké dans les secrets du dépôt. J'ai migré mes workflows dès l'annonce, et je ne reviendrais pas en arrière.

Pourquoi les jetons permanents me posaient problème

Si vous utilisez Turborepo en CI, vous connaissez la routine : pour que le cache distant fonctionne, il faut un TURBO_TOKEN. Jusqu'à récemment, ça voulait dire générer un Personal Access Token sur son compte Vercel, puis le coller dans les secrets GitHub du dépôt.

Ce jeton, une fois créé, reste valide jusqu'à révocation manuelle. Il traîne dans les settings du dépôt, il peut fuiter dans un log de build mal masqué, et personne ne pense à le faire tourner. Quand un prestataire quitte le projet, est-ce qu'on révoque le token ? En théorie, oui. En pratique, on oublie.

Les credentials qui fuient via des logs CI ou des workflows mal configurés sont un classique des rapports d'incident. Plutôt que de « faire plus attention », j'ai préféré supprimer le secret permanent tout court.

Le principe : échanger une identité contre un jeton

OIDC (OpenID Connect) permet au pipeline de prouver qui il est sans secret partagé. GitHub Actions émet un jeton d'identité signé, qui contient des revendications vérifiables : le dépôt, la branche, le workflow, l'acteur qui a déclenché le run.

Vercel vérifie la signature et les revendications, puis émet en échange un jeton d'accès Turborepo éphémère. Et détail qui change tout : ce jeton ne donne accès qu'au cache distant. Impossible de s'en servir pour déployer, modifier le projet ou appeler le reste de l'API Vercel. Même intercepté, il est inutile au-delà du cache, et il expire tout seul.

Configuration côté Vercel

Rendez-vous dans les settings de votre équipe : Build and Deployment → OIDC Policies for CLI Access, puis créez une Turborepo CLI Policy.

Pour GitHub, le formulaire est simple : vous sélectionnez votre compte GitHub et le dépôt concerné dans des listes déroulantes. Pas besoin de saisir l'URL de l'émetteur à la main — Vercel connaît déjà GitHub Actions. Vous pouvez ensuite restreindre la policy à une branche ou un workflow précis, et personnaliser l'audience si votre setup l'exige.

Ma méthode : je commence par une policy limitée au dépôt, je teste, puis je resserre sur la branche principale et le workflow de build. Si plusieurs policies peuvent correspondre à un même repo, notez l'identifiant de la policy — il servira côté CI.

Côté GitHub Actions

Deux changements suffisent. D'abord, autoriser le job à demander un jeton OIDC :

permissions:
  id-token: write
  contents: read

Ensuite, ajouter l'action officielle avant toute commande turbo :

- uses: vercel/setup-turborepo-remote-cache-action@v1 # vérifiez le tag le plus récent sur le dépôt
  with:
    team: ${{ vars.TURBO_TEAM }}

L'action fait tout le travail : elle demande le jeton OIDC à GitHub, l'échange contre un jeton Turborepo éphémère, puis exporte TURBO_TOKEN et TURBO_TEAM pour les steps suivants. Si plusieurs policies matchent votre dépôt, ajoutez le paramètre policy avec l'identifiant noté plus tôt.

Petit détail auquel on ne pense pas toujours : team se stocke dans une variable de dépôt (vars), pas dans les secrets. Ce n'est qu'un slug d'équipe, rien de sensible.

La version manuelle, pour comprendre le mécanisme

Si vous voulez voir ce que l'action fait sous le capot, GitHub expose deux variables : ACTIONS_ID_TOKEN_REQUEST_URL et ACTIONS_ID_TOKEN_REQUEST_TOKEN. Attention au piège — la seconde n'est pas le jeton OIDC, c'est le jeton bearer qui autorise votre job à le demander. Le flow complet :

# 1. Demander le jeton OIDC à GitHub
OIDC_TOKEN=$(curl -s \
  -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
  "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=<audience-de-votre-policy>" | jq -r .value)

# 2. L'échanger contre un jeton Turborepo
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=<id-de-la-policy>

La réponse contient le jeton à exporter dans TURBO_TOKEN. Dans la grande majorité des cas, l'action officielle suffit — mais savoir faire l'échange à la main aide beaucoup quand il faut déboguer.

Les vraies limites

Le seul point faible, c'est la dépendance au endpoint d'échange de Vercel. Si api.vercel.com est indisponible au moment du build, pas de jeton, donc pas de cache. Le build ne plante pas pour autant : Turborepo bascule en cache miss et reconstruit tout. C'est plus lent, mais ça passe. Je garde quand même un PAT de secours dans un gestionnaire de secrets externe, avec une procédure écrite pour l'activer manuellement — hors du dépôt, et jamais activé par défaut.

Deuxième point de vigilance : la policy s'applique au niveau du dépôt. Sans restriction par branche ou workflow, n'importe quel run du repo — y compris celui d'une pull request venue d'un fork — peut obtenir un jeton. Certes, ce jeton ne touche que le cache. Mais un accès en écriture au cache, c'est un risque d'empoisonnement : quelqu'un pourrait y déposer des artefacts modifiés que vos builds exécuteraient ensuite. Restreignez la policy aux branches protégées, et évitez l'écriture dans le cache sur les PR non vérifiées.

Comment je valide une nouvelle config

Avant de généraliser, je lance un run sur une branche de test et je vérifie trois choses dans les logs :

  1. L'action s'exécute sans erreur et exporte bien TURBO_TOKEN (masqué) et TURBO_TEAM.
  2. Au second run, Turborepo affiche des cache hits — le fameux >>> FULL TURBO est le signe que tout fonctionne.
  3. L'ancien PAT est supprimé des secrets du dépôt. Sinon, la migration n'a servi à rien.

Inutile de tester l'expiration du jeton à la main : il est éphémère par construction, et il peut rester valide quelque temps après la fin du job — ce n'est pas un bug, c'est simplement sa durée de vie.

FAQ

Et si mon build dure plus longtemps que le jeton ? Vercel ne publie pas de durée exacte, mais le jeton est pensé pour couvrir un run. Si vos builds sont vraiment longs, découpez-les en plusieurs jobs : chaque job refait l'échange et repart avec un jeton frais.

Ça fonctionne avec GitLab CI ? Oui. GitLab émet un jeton OIDC via CI_JOB_JWT_V2, ou via le bloc id_tokens si vous voulez choisir l'audience. Côté Vercel, si votre provider n'apparaît pas dans la liste, la policy permet de configurer manuellement l'émetteur et les revendications attendues. L'échange, lui, reste identique.

Comment auditer les accès au cache ? À ma connaissance, Vercel ne propose pas de journal d'accès dédié au cache distant. La trace la plus fiable reste les logs de vos pipelines, où chaque échange de jeton apparaît. C'est d'ailleurs un argument supplémentaire pour OIDC : chaque accès est lié à un run précis, avec un acteur et une branche identifiés, au lieu d'un token anonyme partagé par toute l'équipe.

Sources