Un cache CI est souvent considéré comme un simple réglage de performance. Pourtant, les fichiers restaurés peuvent finir dans un chemin exécuté par le build : dépendances, scripts, objets compilés, outils téléchargés ou couches intermédiaires. Un cache modifié par un job peu fiable peut donc devenir un canal d’exécution dans un workflow plus privilégié.
Depuis le 10 septembre 2026, GitHub Actions permet de fixer explicitement les droits du cache avec cache-mode. La clé accepte quatre valeurs, read, write, write-only et none, au niveau du workflow ou d’un job. La fonction est disponible sur github.com pour tous les plans.
Le réglage le plus utile en pratique consiste à séparer les rôles : les validations de pull requests restaurent un cache en lecture seule, tandis qu’un workflow déclenché depuis une branche de confiance produit les nouvelles entrées. Les jobs de publication qui n’ont pas besoin du cache le désactivent complètement.
cache-mode impose une limite de droits appliquée par le service de cache. Il ne vérifie toutefois pas l’intégrité des fichiers déjà présents et ne corrige pas un workflow qui exécute du code non fiable avec des secrets.
En bref#
cache-mode: readautorise la restauration, mais interdit l’enregistrement d’une nouvelle entrée.cache-mode: writeautorise la restauration et l’enregistrement.cache-mode: write-onlyautorise l’enregistrement, mais pas la restauration.cache-mode: nonecoupe tout accès au cache GitHub Actions.- Le réglage d’un job remplace celui défini au niveau du workflow.
- Sans réglage explicite, GitHub utilise
writepour un déclencheur considéré comme fiable etreadpour un déclencheur peu fiable qui travaille dans la portée de la branche par défaut. - Déclarer explicitement
writeouwrite-onlysurpull_request_target,issue_commentouworkflow_runcontourne cette protection par défaut. GitHub affiche alors une annotation d’avertissement. - Le mode est appliqué avec un jeton de cache limité. Il est distinct des droits définis dans
permissionspour leGITHUB_TOKEN. - Un workflow réutilisable ne peut pas obtenir plus de droits de cache que la limite explicite de son appelant.
- Un cache restauré reste une entrée non fiable : son contenu n’est ni signé ni vérifié par GitHub.
Comment fonctionne un empoisonnement de cache#
Les caches GitHub Actions ne sont pas isolés par nom de workflow ou par job. Leur visibilité dépend notamment de la clé, de la version interne du cache et de la branche ou du tag. Plusieurs workflows d’un même dépôt peuvent donc partager une entrée dans une portée compatible.
Une branche peut restaurer les caches de sa propre portée et ceux de la branche par défaut. Une pull request peut aussi lire les caches de sa branche de base. Ce comportement est nécessaire pour accélérer les builds, mais il impose de considérer le contenu restauré comme une donnée externe au workflow courant.
Un scénario d’attaque réaliste comporte généralement quatre étapes :
- un acteur externe peut influencer un job, par exemple au moyen d’une pull request, d’un commentaire ou d’une donnée interpolée sans précaution dans un script ;
- ce job dispose d’un accès en écriture à une portée de cache qu’un autre workflow peut lire ;
- il enregistre des fichiers sous une clé exacte ou un préfixe que le workflow cible acceptera ;
- un job plus privilégié restaure ces fichiers puis les exécute ou les interprète avec des secrets, un jeton en écriture ou un accès au cloud.
Le risque n’est donc pas identique pour tous les chemins mis en cache. Un cache de téléchargements npm utilisé ensuite par npm ci avec un lockfile et des contrôles d’intégrité offre moins de possibilités directes qu’un répertoire node_modules, un environnement Python complet, un dossier de binaires ou des objets de compilation exécutés sans reconstruction. Il faut néanmoins garder la même frontière : le cache améliore les performances, il ne constitue pas une source de confiance.
Le cas le plus sensible concerne les événements qui s’exécutent dans le contexte de la branche par défaut tout en pouvant être déclenchés ou influencés par une personne sans droit d’écriture. pull_request_target, issue_comment et certaines cascades workflow_run en font partie. Une injection de commande ou un checkout imprudent de la tête d’une pull request peut alors donner du code exécutable à l’attaquant dans un contexte plus privilégié.
L’événement pull_request classique est un cas particulier. Les caches qu’il crée sont associés à la référence de fusion refs/pull/.../merge. Ils peuvent être relus par une nouvelle exécution de cette pull request, mais pas par la branche de base ou par une autre pull request. Lui imposer read reste utile pour appliquer une politique stable, réduire les écritures et empêcher un empoisonnement entre les nouvelles exécutions de la même PR, mais le risque pour le cache de main est déjà limité par cette portée.
Impact réel#
La publication de cache-mode ne signifie pas que tous les dépôts GitHub étaient immédiatement exploitables avant le 10 septembre. Depuis juin 2026, GitHub limite déjà en lecture seule plusieurs déclencheurs peu fiables lorsqu’ils utilisent la portée de la branche par défaut. Un workflow qui n’a pas redonné explicitement l’écriture bénéficie de cette protection.
La priorité d’audit est élevée lorsqu’un dépôt réunit ces éléments :
- des workflows
pull_request_target,issue_commentouworkflow_runtraitent des données externes ; - l’un de ces jobs déclare
writeouwrite-only, ou appelle un workflow réutilisable capable de le faire ; - un workflow de publication, de déploiement ou de signature restaure des clés compatibles ;
- le contenu restauré peut être exécuté avant ou pendant l’accès aux secrets.
Le risque est plus faible lorsque les seules écritures viennent de push sur une branche protégée, que les consommateurs n’ont pas de secret et que les clés incluent le hash du lockfile. Il n’est pas nul : un compte collaborateur compromis, une action tierce détournée ou une injection dans un workflow dit fiable peut encore produire une entrée malveillante.
Pour un dépôt qui n’utilise aucun cache, le changement n’impose rien. Pour un dépôt qui cache uniquement des téléchargements dans des jobs sans privilège, l’enjeu est surtout de formaliser la politique. L’urgence concerne les chemins où un cache relie un contexte influençable de l’extérieur à un contexte plus privilégié.
Ce que contrôle réellement cache-mode#
La matrice est simple :
| Mode | Restaurer | Enregistrer | Usage courant |
|---|---|---|---|
read | Oui | Non | Tests de PR et consommateurs du cache |
write | Oui | Oui | Build fiable qui réutilise et actualise son cache |
write-only | Non | Oui | Producteur dédié qui ne restaure pas de cache Actions |
none | Non | Non | Publication, signature ou déploiement sans besoin de cache |
Au niveau du workflow, la clé s’applique à tous les jobs :
permissions:
contents: read
cache-mode: readUn job peut ensuite définir sa propre valeur :
cache-mode: none
jobs:
build-cache:
runs-on: ubuntu-latest
cache-mode: write-only
steps:
- run: echo "Ce job peut enregistrer un cache sans en restaurer"L’application ne repose pas uniquement sur le bon comportement de actions/cache. Le runner obtient un jeton de cache limité et le service refuse l’opération qui dépasse le mode effectif. Les actions basées sur le toolkit @actions/cache respectent également ce mode.
Le runner expose la valeur réellement appliquée dans ACTIONS_CACHE_MODE. Pour lever un doute pendant une migration, un job peut afficher uniquement cette variable :
- name: Afficher le mode de cache effectif
shell: bash
run: printf 'ACTIONS_CACHE_MODE=%s\n' "$ACTIONS_CACHE_MODE"Une opération interdite par le mode ne fait pas échouer le job. Une restauration refusée devient un cache miss et un enregistrement refusé est ignoré. L’action écrit un message d’information dans les logs, puis le workflow continue. Il faut en tenir compte lors des mesures de durée : un pipeline peut devenir plus lent sans passer en erreur.
Les valeurs par défaut#
Si cache-mode est absent, GitHub conserve un comportement dépendant du déclencheur :
| Contexte effectif | Mode par défaut |
|---|---|
| Déclencheur fiable | write |
| Déclencheur peu fiable dans la portée de la branche par défaut | read |
Pour la portée de la branche par défaut, les événements autorisés à écrire par défaut sont push, workflow_dispatch, repository_dispatch, delete, registry_package, page_build et schedule. Les autres événements résolus sur cette portée reçoivent normalement un accès en lecture seule.
Cette protection est en place depuis juin 2026. La nouveauté de septembre permet de rendre la politique explicite et de la réduire davantage. Elle permet aussi de la contourner : une déclaration cache-mode: write ou cache-mode: write-only prend le pas sur le défaut sécurisé, même avec un déclencheur peu fiable. GitHub ajoute une annotation, mais n’interdit pas l’exécution.
Il ne faut donc jamais utiliser write-only comme synonyme de « faible privilège ». Ce mode ne peut pas lire un cache, mais il peut précisément déposer le contenu qui sera consommé plus tard. Sur un job influencé par une contribution externe, cette capacité reste dangereuse.
Quelle politique choisir#
Une politique raisonnable peut se résumer ainsi :
| Type de job | Mode conseillé | Réserve |
|---|---|---|
| Validation d’une pull request externe | read ou none | read accélère le job, mais le cache restauré reste non fiable |
pull_request_target, issue_comment, workflow_run | read par défaut, sinon none | Ne jamais accorder l’écriture à un job qui traite une donnée contrôlée par l’appelant |
Tests sur un push vers main | read si un producteur séparé existe, sinon write | write doit rester réservé à du code et des entrées de confiance |
Producteur de cache dédié sur main | write-only | Construire depuis le lockfile sans restaurer d’ancien cache |
| Publication, signature ou déploiement | none si possible | Si une lecture est indispensable, isoler le build du job qui reçoit les secrets |
| Appel d’un workflow réutilisable | Limite explicite sur le job appelant | Ne pas dépendre du mode demandé par le workflow appelé |
Le choix ne doit pas être basé uniquement sur le nom de l’événement. Un workflow_dispatch est classé parmi les déclencheurs autorisés à écrire par défaut, mais ses entrées peuvent encore devenir dangereuses si elles sont injectées directement dans une commande. De même, un push vers une branche protégée est fiable seulement si les règles de revue et les comptes autorisés le sont réellement.
Architecture recommandée : consommateurs en lecture, producteur séparé#
La séparation la plus lisible utilise deux workflows. Le premier valide les pull requests sans pouvoir écrire dans le cache. Le second alimente le cache depuis main sans restaurer une entrée existante.
Workflow de pull request en lecture seule#
Cet exemple demande le cache de téléchargement npm avec une clé incluant le hash du lockfile. Il ne restaure pas node_modules et n’utilise pas de restore-keys large.
name: Pull request checks
on:
pull_request:
permissions:
contents: read
cache-mode: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: false
- name: Restore npm cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.npm
key: ${{ runner.os }}-node-24-npm-${{ hashFiles('package-lock.json') }}
- run: npm ci
- run: npm testL’action restore exprime l’intention sans ambiguïté. Le mode read ajoute la frontière côté service : même si une action ajoutée plus tard tente d’enregistrer un cache, elle n’obtiendra pas le droit nécessaire.
package-manager-cache: false est volontaire. setup-node v7 peut activer automatiquement la mise en cache npm si package.json déclare npm dans packageManager ou devEngines.packageManager. Sans ce réglage, une revue limitée aux étapes actions/cache peut manquer une opération implicite.
Producteur fiable en write-only#
Le workflow suivant s’exécute uniquement après un push sur main. Il télécharge les dépendances sans script de cycle de vie, vérifie le cache npm local, puis enregistre l’entrée. Comme le job est en write-only, une ancienne entrée du cache GitHub Actions ne peut pas influencer sa construction.
name: Maintain npm cache
on:
push:
branches:
- main
paths:
- package.json
- package-lock.json
- .github/workflows/cache-main.yml
permissions:
contents: read
cache-mode: none
jobs:
seed-npm-cache:
runs-on: ubuntu-latest
cache-mode: write-only
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: false
- name: Populate npm cache from the lockfile
run: npm ci --ignore-scripts
- name: Verify npm cache
run: npm cache verify
- name: Save npm cache
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.npm
key: ${{ runner.os }}-node-24-npm-${{ hashFiles('package-lock.json') }}Ce modèle accepte un coût : une nouvelle version du lockfile n’aura pas de cache pour sa première validation en PR. C’est généralement préférable à un workflow qui autorise chaque contribution externe à produire une entrée partagée. Si le dépôt reçoit peu de contributions externes, le surcoût reste limité.
Un build fiable qui a besoin de restaurer une entrée précédente pour produire la suivante peut utiliser write. Il faut alors accepter que son point de départ dépend du contenu déjà stocké. Pour un cache de compilation complexe, une reconstruction périodique en write-only ou none permet de vérifier que le build ne dépend pas d’une restauration du cache Actions. Sur un runner auto-hébergé persistant, il faut également nettoyer ou recréer l’environnement de travail.
Job de publication sans cache#
Un job qui reçoit un droit OIDC, un identifiant de registre ou un secret de signature ne devrait pas restaurer un cache par habitude. S’il n’en a pas besoin, coupez cet accès explicitement. Les étapes de publication sont volontairement omises dans cet extrait :
permissions:
contents: read
id-token: write
cache-mode: none
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: false
# Restaurer ici un artefact préalablement vérifié, puis le publier.Séparez la construction et la publication dès que le job de build exécute des dépendances ou des scripts. Le job de publication doit vérifier l’identité et la provenance de l’artefact reçu avant de lui accorder un contexte privilégié. Déplacer le fichier vers un artefact GitHub Actions sans vérification ne supprime pas la frontière de confiance.
Le cas des workflows réutilisables#
Un workflow réutilisable peut cacher une opération de restauration ou d’enregistrement derrière une interface très simple. cache-mode est transmis depuis l’appelant et limite les droits que le workflow appelé peut demander.
jobs:
shared-ci:
permissions:
contents: read
cache-mode: read
uses: example-org/ci/.github/workflows/node.yml@<sha-complet-vérifié>Si le workflow appelé demande write alors que l’appelant a fixé read, GitHub refuse de démarrer l’exécution avec une erreur de validation. read et write-only ne sont pas ordonnés : le premier autorise uniquement la lecture, le second uniquement l’écriture. Un appelant en write-only ne peut donc pas appeler un workflow qui exige read.
Le point subtil concerne l’absence de valeur explicite. Si le job appelant ne définit ni n’hérite de cache-mode, un workflow appelé peut demander write, y compris lorsque le défaut du déclencheur appelant aurait été read. Pour un workflow tiers ou centralisé, fixez toujours la limite sur le job qui utilise uses:.
Vérifier son exposition#
Commencez par inventorier les opérations explicites et les actions de setup susceptibles de gérer un cache :
rg -n \
--glob '*.yml' \
--glob '*.yaml' \
'^[[:space:]]*(-[[:space:]]*)?(cache-mode:|cache:|package-manager-cache:|uses:.*(actions/cache|actions/setup-(node|python|java|go|dotnet)|ruby/setup-ruby))' \
.github/workflowsRecherchez ensuite les déclencheurs qui demandent une revue attentive :
rg -n \
--glob '*.yml' \
--glob '*.yaml' \
'\b(pull_request_target|issue_comment|workflow_run|pull_request|workflow_dispatch|repository_dispatch|schedule|push)\b' \
.github/workflowsCes commandes constituent un inventaire, pas une preuve d’absence. Elles ne développent pas les workflows réutilisables et ne détectent pas nécessairement une action tierce qui utilise directement le toolkit de cache. Pour chaque uses: externe, contrôlez la documentation et le code de la version réellement épinglée.
La revue doit répondre à quelques questions concrètes :
- quel événement déclenche le job ?
- qui peut contrôler son payload, ses inputs ou le code checkouté ?
- quelle portée de cache reçoit-il réellement ?
- peut-il écrire, directement ou via une action de setup ?
- quels autres workflows restaurent la même clé ou un
restore-keyscompatible ? - les fichiers restaurés sont-ils ensuite exécutés ?
- le job consommateur possède-t-il des secrets ou des permissions en écriture ?
Évitez les clés trop génériques dans les workflows privilégiés. Une clé incluant le système, la version du runtime et le hash du lockfile réduit les collisions accidentelles :
key: ${{ runner.os }}-node-24-npm-${{ hashFiles('package-lock.json') }}Un restore-keys comme npm- accepte la dernière entrée portant ce préfixe lorsqu’aucune clé exacte ne correspond. C’est pratique pour les performances, mais la surface de sélection devient plus large. Sur un job de publication ou de signature, évitez ces préfixes larges et désactivez le cache s’il n’est pas indispensable.
Même sans restore-keys, l’action peut rechercher une correspondance par préfixe de key si la clé exacte est absente. Inclure le hash du lockfile resserre la sélection sans garantir une correspondance strictement exacte. La sortie cache-hit: 'true' indique une correspondance exacte, mais ne prouve pas l’authenticité du contenu restauré.
Détection et réponse à incident#
L’interface Actions > Caches affiche les clés, les références, la taille, la date de création et le dernier accès. La CLI permet d’obtenir le même inventaire sans modifier le dépôt :
gh cache list \
--limit 100 \
--sort created_at \
--order desc \
--json id,key,ref,sizeInBytes,createdAt,lastAccessedAtPour se concentrer sur la branche par défaut :
gh cache list \
--ref refs/heads/main \
--limit 100 \
--json id,key,ref,sizeInBytes,createdAt,lastAccessedAtCes métadonnées ne donnent pas une provenance cryptographique du contenu. Une création à une heure inhabituelle, une taille qui change fortement ou une clé inattendue justifie une corrélation avec les exécutions Actions, mais ne prouve pas à elle seule un empoisonnement.
Dans les logs, recherchez aussi :
- une annotation signalant
writeouwrite-onlysur un déclencheur peu fiable ; - une sauvegarde provenant d’un job
pull_request_target,issue_commentouworkflow_run; - une restauration par préfixe alors qu’une clé exacte était attendue ;
- un cache miss soudain suivi d’une création depuis un contexte inhabituel ;
- des commandes, connexions sortantes ou modifications du dépôt inexpliquées après la restauration.
Si une entrée semble compromise, empêchez d’abord les workflows privilégiés de la restaurer en passant temporairement leur mode à none. Identifiez ensuite précisément l’entrée avant suppression. La commande suivante supprime le cache distant ciblé et n’est pas réversible. Remplacez CACHE_ID par son identifiant numérique et OWNER/REPO par le dépôt concerné :
gh cache delete CACHE_ID --repo OWNER/REPOAprès suppression, reconstruisez le cache avec un producteur fiable en write-only. La suppression ne revient pas sur les jobs déjà exécutés. Si un workflow privilégié a restauré puis utilisé l’entrée suspecte :
- examinez les logs du job et les journaux des services auxquels il avait accès ;
- révoquez les secrets persistants présents dans son environnement ;
- contrôlez les modifications réalisées avec le
GITHUB_TOKEN, dans les registres et dans le cloud ; - reconstruisez un runner auto-hébergé persistant si du code arbitraire a pu s’y exécuter ;
- relancez les builds et publications depuis une révision connue après assainissement.
Ce que cache-mode ne protège pas#
Le nouveau réglage ferme une capacité précise. Il ne doit pas devenir une justification pour relâcher les autres contrôles.
Il ne valide pas un cache restauré#
read signifie uniquement que le job courant ne peut pas enregistrer une entrée. Il peut toujours recevoir un cache malveillant produit auparavant par un autre job autorisé à écrire. Les caches GitHub ne sont ni signés ni vérifiés avant extraction.
Il ne protège pas les secrets stockés dans le cache#
Une pull request peut restaurer les caches visibles depuis sa branche de base. Ne placez jamais de jeton, fichier de configuration authentifié, clé privée ou identifiant de registre dans un chemin mis en cache. Passer ensuite le workflow en none ne retire pas les entrées déjà stockées.
Il ne remplace pas permissions#
permissions limite le GITHUB_TOKEN. cache-mode limite le jeton du service de cache. Un job devrait généralement définir les deux :
permissions:
contents: read
cache-mode: readLes secrets, droits OIDC, environnements protégés et jetons fournis manuellement gardent leurs propres règles.
Il ne couvre pas tous les stockages intermédiaires#
Le réglage vise le cache GitHub Actions et les clients qui utilisent son jeton limité. Il ne contrôle pas :
- un cache S3 ou Redis configuré par l’application ;
- un cache Docker exporté vers un registre OCI ;
- les fichiers laissés sur le disque d’un runner auto-hébergé persistant ;
- les artefacts transmis entre jobs ou workflows ;
- un proxy de paquets interne.
Chacun de ces canaux nécessite sa propre politique d’écriture, de lecture et d’intégrité.
Il ne sécurise pas pull_request_target#
Ce déclencheur reste dangereux si le workflow checkout la tête de la pull request puis l’exécute avec des secrets ou un jeton privilégié. cache-mode: read retire une voie d’écriture vers le cache, pas l’exécution de code ni les injections de commandes dans les expressions GitHub.
Plan de migration#
Pour un dépôt existant, la migration peut rester progressive :
- inventorier
actions/cache, les optionscache:des actions de setup et les workflows réutilisables ; - ajouter un
cache-modeexplicite au niveau de chaque workflow ; - choisir
readounonepour les jobs déclenchés par des données externes ; - concentrer les écritures dans un nombre réduit de jobs déclenchés depuis des branches protégées ;
- utiliser
write-onlypour les producteurs capables de repartir sans cache ; - désactiver le cache dans les jobs qui publient, signent ou déploient lorsqu’il n’est pas indispensable ;
- resserrer les clés et supprimer les
restore-keystrop larges des chemins privilégiés ; - contrôler
ACTIONS_CACHE_MODEet les logs sur quelques exécutions avant de comparer les temps de build ; - documenter la suppression des caches et la rotation des secrets dans la procédure d’incident.
Le premier changement rentable consiste souvent à placer cache-mode: read sur tous les workflows de validation, puis à accorder write ou write-only uniquement aux producteurs identifiés. Cette approche rend les exceptions visibles pendant la revue de code.
Conclusion#
cache-mode donne au cache GitHub Actions le contrôle de moindre privilège qui lui manquait au niveau du workflow et du job. La valeur par défaut protège déjà plusieurs déclencheurs peu fiables, mais une déclaration explicite évite de dépendre d’une classification implicite et limite aussi les workflows réutilisables.
Le modèle le plus défendable reste simple : des consommateurs en read, un petit nombre de producteurs fiables en write ou write-only, et none dès que le gain de performance ne justifie pas l’exposition. Il faut surtout retenir que write-only est un droit d’écriture complet et que read ne certifie jamais le contenu restauré.
Pour un dépôt qui utilise pull_request_target, issue_comment ou workflow_run, la priorité est de rechercher toute déclaration write ou write-only. Sur ces événements, l’annotation GitHub ne remplace pas une correction : sauf cas très encadré, le cache doit rester en lecture seule et être alimenté par un workflow de confiance.
Sources#
Sources vérifiées le 15 septembre 2026 :
- GitHub Changelog - annonce de cache-mode du 10 septembre 2026
- GitHub Docs - syntaxe cache-mode au niveau du workflow et des jobs
- GitHub Docs - référence du cache, déclencheurs peu fiables et workflows réutilisables
- GitHub Docs - modèle de sécurité du cache de dépendances
- GitHub Changelog - passage en lecture seule des déclencheurs non fiables en juin 2026
- GitHub Docs - consulter et supprimer les caches Actions
- GitHub Docs - sécuriser l’utilisation de GitHub Actions
- actions/cache - action officielle et actions restore/save
- actions/setup-node - cache npm automatique et option package-manager-cache
- GitHub CLI - options de gh cache list
- npm - vérification du cache local




