Un flux de travail de CI exécute du code venu de nombreuses sources : votre dépôt, des actions tierces et des demandes de tirage (pull requests) de personnes que vous ne connaissez pas. Chacune est une porte d’entrée. Cet article présente six contrôles qui ferment les lacunes courantes. Les exemples proviennent de rzv-ci-plb, le guide de CI de Runzivo.

Deux colonnes reliées par une flèche de fusion. Vérifications des demandes de tirage, non fiables : déclencheur pull_request, jeton en lecture seule, aucun secret pour les forks, aucun identifiant conservé, texte de l’événement transmis par env, éviter pull_request_target. Déploiement, de confiance : push vers main, environnement 04-prd-01, OIDC seulement au besoin, groupe de concurrence deploy-04-prd-01, SHA épinglés, actionlint et zizmor.

1. Commencer chaque flux de travail avec des permissions en lecture seule

Erreur : aucune clé permissions:. Le GITHUB_TOKEN reçoit alors l’ensemble par défaut défini pour l’entreprise, l’organisation ou le dépôt, qui peut autoriser l’écriture.

À faire : définissez la lecture seule en haut du fichier, comme le fait chaque flux de travail du guide. Extrait de ci.yml :

yaml
permissions:
  contents: read

Élevez les permissions seulement sur la tâche qui en a besoin. Un exemple simplifié :

yaml
jobs:
  label:
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      pull-requests: write
    steps:
      - run: echo "This job can label pull requests."

Dès que vous définissez une permission, toutes celles que vous omettez passent à none. La référence de la syntaxe des flux de travail le documente. Voir aussi Permissions dans docs/security.md.

2. Épingler les actions tierces à un SHA de commit complet

Erreur : uses: some-org/some-action@v3. Une étiquette peut être déplacée vers du nouveau code à tout moment.

À faire : épinglez au SHA complet et gardez la version dans un commentaire.

yaml
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Vérifiez le SHA avant de l’utiliser. Extrait de docs/security.md :

bash
gh api repos/actions/checkout/git/ref/tags/v7.0.1 --jq '.object.type + " " + .object.sha'
# If the type is "tag", dereference it:
gh api repos/OWNER/REPO/git/tags/TAG_OBJECT_SHA --jq .object.sha

Assurez-vous que le SHA appartient au dépôt de l’action elle-même, et non à un fork. Dependabot peut garder les SHA épinglés à jour. GitHub documente aussi les fichiers de verrouillage des dépendances de flux de travail, actuellement en préversion publique, comme une option plus solide que l’épinglage par SHA.

3. Ne jamais coller le texte d’un événement dans du code shell

À ne pas faire. Cette étape est abrégée à partir du guide de GitHub sur l’injection de script :

yaml
- name: Check PR title
  run: |
    title="${{ github.event.pull_request.title }}"
    if [[ $title =~ ^octocat ]]; then
      echo "PR title starts with 'octocat'"
    fi

GitHub remplace ${{ }} avant le démarrage du shell. Une demande de tirage intitulée a"; ls $GITHUB_WORKSPACE" devient une commande.

À faire : transmettez la valeur par une variable d’environnement et mettez-la entre guillemets. Le shell la traite alors comme une donnée. Adapté de Entrées non fiables dans docs/security.md :

yaml
- name: Show title
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: |
    printf 'Title: %s\n' "$PR_TITLE"

actionlint et zizmor signalent tous deux le modèle dangereux. J’ai exécuté les deux sur l’exemple ci-dessus pour le confirmer.

4. Traiter les demandes de tirage issues de forks comme non fiables

GitHub limite déjà les demandes de tirage issues de forks. Selon la référence des événements, les secrets autres que GITHUB_TOKEN ne sont pas transmis à l’exécuteur, et le jeton est en lecture seule.

Le risque revient lorsque vous utilisez pull_request_target ou workflow_run. Ces déclencheurs s’exécutent avec les privilèges du dépôt de base. Le guide d’utilisation sécurisée de GitHub recommande de les éviter si vous n’en avez pas besoin, et de ne jamais y extraire ni y exécuter du code non fiable provenant d’une demande de tirage.

Empêchez aussi actions/checkout de conserver le jeton pour les étapes suivantes :

yaml
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  with:
    persist-credentials: false

Les exécuteurs autohébergés exigent une règle de plus : n’y exécutez pas les demandes de tirage issues de forks. runzivo.yml, dans le guide, ne s’exécute que sur push vers main et sur déclenchement manuel. Voir Exécuteurs autohébergés dans docs/security.md.

5. Déployer depuis une tâche distincte et protégée

Erreur : le flux de travail des demandes de tirage détient aussi les identifiants de déploiement.

À faire : déployez uniquement depuis main, une fois les vérifications réussies, par un environnement GitHub. Ajoutez des réviseurs obligatoires lorsque votre forfait le permet, n’autorisez que la branche main et stockez les secrets de déploiement dans l’environnement.

Je nomme les environnements <NN>-<env>-<NN> : 01-dev-01, 02-qat-01, 03-stg-01 et 04-prd-01, avec les codes dev, qat, stg et prd. Les listes sont ainsi triées du développement à la production. Cette tâche de déploiement, tirée de la barrière de déploiement dans docs/environments.md, cible le premier environnement de production :

yaml
deploy:
  needs: [test, build]
  if: github.ref == 'refs/heads/main'
  runs-on: ubuntu-latest
  timeout-minutes: 15
  environment:
    name: 04-prd-01
  permissions:
    contents: read
  concurrency:
    group: deploy-04-prd-01
    cancel-in-progress: false
  steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      with:
        persist-credentials: false
    - run: bash "${GITHUB_WORKSPACE}/scripts/ci/03-deploy.sh"

03-deploy.sh représente votre propre script de déploiement, numéroté après la compilation et les tests. Les tâches des demandes de tirage ne nomment jamais d’environnement : elles ne voient donc jamais ses secrets.

Pour l’accès infonuagique, préférez OpenID Connect à une clé de longue durée : ajoutez id-token: write à cette tâche seulement, et GitHub émet un jeton de courte durée à chaque exécution. Voir Identifiants dans docs/security.md.

6. Garder les secrets hors des journaux

  • N’affichez pas les secrets, même partiellement.
  • Masquez les valeurs dérivées avec ::add-mask::, comme le décrit le guide des secrets.
  • Ne stockez pas de blocs JSON ou YAML comme un seul secret. La rédaction repose sur la correspondance de valeurs exactes : les secrets structurés peuvent donc fuir.
  • Si un secret se retrouve dans un journal, supprimez le journal et faites la rotation du secret.

Vérifier automatiquement

Le guide exécute zizmor à chaque demande de tirage, à chaque push vers main et une fois par semaine, par security.yml. En local :

bash
bash scripts/ci/01-validate.sh security

zizmor propose des audits pour l’injection de gabarit, les permissions excessives, les actions non épinglées et les déclencheurs dangereux. Le fichier zizmor.yml du dépôt exige un épinglage par SHA pour chaque action.

Exécuter sur Runzivo

Runzivo est en accès anticipé. Il attribue à chaque tâche admissible une microVM neuve à usage unique, ce qui limite ce qu’une tâche peut laisser derrière elle pour la suivante. Il ne remplace aucun des contrôles ci-dessus. Les permissions, l’épinglage et la protection contre les injections relèvent de votre flux de travail. Les permissions de son application GitHub sont énumérées dans la documentation de Runzivo.

Dans le dépôt

Pour exécuter ces flux de travail sur des exécuteurs GitHub Actions gérés hébergés au Canada, consultez les tarifs des exécuteurs et des bacs à sable Runzivo.

Chaque exemple de cette série est testé dans rzv-ci-plb, the Runzivo CI playbook, fixé à l’étiquette v0.1.0.