Skip to main content

Publier des packages npm en toute sécurité avec GitHub Actions

Écrit par

10 novembre 2020

0 minutes de lecture

GitHub Actions gagne en popularité depuis que GitHub a annoncé sa disponibilité générale pour tous les développeurs et tous les dépôts de la plateforme GitHub. Les limites de débit que nous observons dans l’écosystème, comme les limites de facturation et de débit de Travis CI pour l’open source, inciteront encore davantage les développeurs à migrer leurs automatisations logicielles vers GitHub Actions.

Dans cet article, j’aimerais vous montrer comment j’utilise GitHub Actions pour publier les packages npm que je maintiens dans mes projets open source. Si vous suivez le workflow GitHub, qui repose sur les workflows de pull requests GitHub, cette approche unifiera encore davantage l’expérience de vos équipes et des contributeurs de la communauté autour des workflows GitHub de votre projet.

Qu’est-ce que GitHub Actions ?

GitHub Actions est une technologie développée par GitHub qui permet aux développeurs d’automatiser leurs workflows d’intégration continue : ils peuvent ainsi compiler, déployer, planifier des tâches récurrentes et bien plus encore. GitHub Actions est disponible nativement et intégré aux dépôts GitHub. Il propose de nombreux workflows réutilisables créés par des contributeurs de la communauté, pour publier des packages npm, publier des images Docker, exécuter des tests de sécurité et bien plus encore.

Comment fonctionnent les actions dans GitHub ?

L’infrastructure cloud de GitHub exécute GitHub Actions pour les utilisateurs. Pour cela, il faut créer un fichier de workflow dans un répertoire de dépôt GitHub nommé .github/workflows, puis décrire le déclencheur, la planification du job et les actions qu’il doit effectuer à l’aide de YAML.

Qu’est-ce qu’un workflow GitHub ?

Un workflow GitHub est un ensemble de jobs exécutés en fonction d’un déclencheur ou d’une planification basée sur cron. Un job comprend une ou plusieurs étapes qui constituent un workflow automatisé.

Configurer un projet Node.js pour GitHub Actions

Ajoutons une première automatisation GitHub Actions à un projet Node.js. Le job GitHub Actions installera tous les packages npm nécessaires, exécutera les tests, puis publiera notre projet sous la forme d’un package npm que les utilisateurs pourront exploiter.

Notre package npm sera une interface en ligne de commande (CLI) qui vous permettra de parcourir la formidable liste des conférences de SnykCon 2020, le tout premier événement mondial sur la sécurité organisé par Snyk, en 2020.

Le projet complet est hébergé sur GitHub. Voici un aperçu du package npm :

Visuel de style terminal répertoriant les conférences de SnykCon 2020, notamment des sessions sur le DevSecOps et la sécurité de la surface d’attaque front-end.

Pour créer un workflow CI GitHub complet, commencez par ajouter le fichier GitHub Actions suivant à la racine du dépôt : .github/workflows/main.yml

name: main

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12.x, 14.x]
    steps:
      - uses: actions/checkout@v2
      - name: Build on Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci --ignore-scripts
      - run: npm run build --if-present
        name: Build
      - run: npm test
        env:
          CI: true

Le modèle ci-dessus est assez classique pour compiler et tester des projets Node.js. Il effectue les opérations suivantes :

  1. Le déclencheur s’active à chaque push, sur toutes les branches.

  2. Nous exécutons cette action avec les versions 12.x et 14.x de Node.js afin de garantir la compatibilité avec les deux versions LTS de Node.js.

  3. Le job exécute ensuite plusieurs étapes : il récupère le code du dépôt git, effectue une installation npm sécurisée et déterministe, lance une étape de compilation si elle est définie, puis exécute les tests.

Lorsque vous poussez de nouveaux commits vers le dépôt, que vous suiviez un workflow GitHub ou un workflow de pull request GitHub, vous voyez de nouveaux jobs s’ajouter dans l’onglet Actions du dépôt. Voici un exemple montrant les tests du package npm snykcon en cours d’exécution :

Workflow GitHub Actions pour le dépôt snykcon, indiquant la réussite d’une compilation Node.js 12.x et de l’exécution des tests npm.

Publier des packages npm avec GitHub Actions

Si tous les tests réussissent et que ce workflow d’automatisation s’exécute sur notre branche principale, nous pouvons automatiser entièrement la publication des nouvelles versions !

Lors de la publication de packages, il est judicieux de prendre en compte les implications de sécurité associées. Passons-en quelques-unes en revue :

  1. Code malveillant : Si une personne ajoute délibérément une vulnérabilité dans une contribution au code et que vous ne la repérez pas, une publication automatique lors de la fusion dans votre branche principale la rendra immédiatement accessible aux utilisateurs. Une publication manuelle immédiate n’y change rien. En revanche, plus le délai entre la fusion des pull requests et la publication est long, plus la communauté a le temps d’examiner le code et de contribuer à détecter ces problèmes.

  2. Dépendance vulnérable : Si une personne malveillante ajoute une dépendance présentant une vulnérabilité connue et publique, celle-ci sera transmise à vos utilisateurs lors de son installation. Un outil gratuit comme Snyk, connecté à vos dépôts Git, peut ajouter une vérification d’état qui vous empêche de publier des versions contenant des dépendances vulnérables et résoudre précisément ce problème.

  3. Dépendance malveillante via un fichier de verrouillage : Avez-vous envisagé ce qui se passerait si une contribution visant à mettre à jour les dépendances de package.json injectait en réalité un package malveillant dans le fichier de verrouillage, que personne ne prend de toute façon le temps d’examiner ? J’ai expliqué pourquoi les fichiers de verrouillage npm peuvent constituer un angle mort de sécurité permettant d’injecter des modules malveillants. Si vous n’avez pas encore envisagé ce vecteur d’attaque, prenez le temps d’approfondir le sujet.

  4. Vol de votre jeton npm :Par le passé, plusieurs tentatives liées à des packages malveillants visaient à dérober le jeton npm d’une personne ou d’autres informations sensibles stockées dans des variables d’environnement.

La liste ci-dessus ne prétend pas couvrir tous les risques de sécurité, mais elle met clairement en évidence plusieurs points dont nous devons être conscients.

À propos des risques de sécurité, trouvez-vous cette liste intéressante et instructive ? Il s’agit d’un exercice rapide de modélisation des menaces, que vous pouvez pratiquer plus régulièrement lors du développement de fonctionnalités. Nous en parlons dans notre processus DevSecOps, et Alyssa Miller a présenté une excellente conférence à SnykCon sur le Threat modeling des user stories : l’approche DevSecOps. Découvrez-les !

Revenons à notre liste et intéressons-nous au dernier risque mentionné : le vol de votre jeton npm. Comme cet article porte sur la publication de packages npm, nous devons mettre un jeton npm à la disposition du workflow GitHub Actions. Cette pratique a longtemps été déconseillée pour les raisons suivantes :

  • Fonctionnalités npm : jusqu’à récemment, la publication de packages npm avec un jeton npm exigeait de désactiver l’authentification à deux facteurs pour votre compte npm. Ce n’est plus le cas, et nous allons voir comment procéder !

  • Vol d’un jeton lors de l’installation de packages malveillants :si vous mettez le jeton npm à disposition de votre CI sous forme de variable d’environnement, des packages malveillants présents dans l’arborescence de dépendances (au-delà de vos dépendances directes) peuvent y accéder pendant le processus d’installation npm, par exemple. Par défaut, les packages peuvent alors exécuter n’importe quelle commande.

Comment répondre à ces deux risques de sécurité bien réels ?

Tout d’abord, npm permet désormais d’utiliser l’authentification à deux facteurs et d’émettre des jetons d’automatisation : ces deux fonctionnalités ne sont donc plus incompatibles. Ensuite, GitHub Actions permet de rendre les variables d’environnement accessibles uniquement à une étape précise d’un job. Nous pouvons ainsi les rendre disponibles uniquement pour la commande npm publish, et non pour npm install, qui aurait également permis aux dépendances indirectes d’y accéder.

Commençons.

Tout d’abord, activez l’authentification à deux facteurs pour votre compte npm. Accédez aux paramètres de votre compte sur https://npmjs.com/ et activez le mode 2FA pour l’authentification et la publication.

Écran de configuration de l’authentification à deux facteurs proposant les options « Autorisation », « Autorisation et publication » ou « Désactiver », avec « Autorisation et publication » sélectionnée

Vous devrez associer un appareil d’authentification, par exemple l’application Google Authenticator sur votre téléphone ou 1Password si vous l’utilisez pour gérer vos mots de passe.

Accédez ensuite à la gestion des jetons d’accès sur npm et créez un nouveau jeton.

Tableau de bord des jetons d’accès avec une recherche de packages et les commandes « Générer un nouveau jeton » et « Supprimer les jetons sélectionnés »

Veillez à créer un jeton de type Automation, comme le montre la capture d’écran ci-dessous. Comme indiqué dans la description, ce type de jeton contourne l’authentification à deux facteurs et peut être utilisé dans des workflows d’intégration continue (CI) :

Formulaire de création d’un nouveau jeton d’accès npm, avec l’option Automatisation sélectionnée parmi les types de jetons en lecture seule, automatisation et publication

Nous pouvons ensuite mettre ce jeton à disposition de GitHub Actions en le créant d’abord comme secret dans la gestion des secrets du dépôt GitHub, comme ceci :

Paramètres des secrets du dépôt GitHub affichant un secret NPM_TOKEN chiffré avec les boutons Mettre à jour et Supprimer

Enfin, mettons à jour notre workflow GitHub Actions pour y ajouter une étape de publication. Après le job build, ajoutez le job publish suivant :

publish:
    if: github.ref == 'refs/heads/main'
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: 14
      - name: Publish
        run: |
          npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
          npm publish --ignore-scripts
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Il est essentiel de noter l’argument --ignore-scripts de la commande npm publish, indispensable à un workflow de publication sécurisé. Il indique à la commande de publication de la CLI npm d’ignorer tous les scripts de cycle de vie définis dans le manifeste packge.json. C’est important : lorsqu’un package malveillant est installé dans le cadre du graphe de dépendances, il peut ajouter à votre propre package une entrée telle que prepack: “echo ‘do something malicious’”, qui sera exécutée lorsque vous lancerez npm publish. Comme vous pouvez l’imaginer, cette entrée malveillante peut faire bien plus qu’afficher un message à l’écran.

Le workflow que nous utilisons ici réduit considérablement le risque de vol des jetons npm ou d’exécution de commandes arbitraires visant d’autres cibles, car :

  1. Dans notre job build, nous installons les packages sans leur permettre d’exécuter des commandes arbitraires, grâce à l’argument --ignore-scripts transmis à npm ci.

  2. Notre job publish démarre à partir d’un nouveau checkout du dépôt. Ainsi, même s’il avait fallu autoriser l’exécution de scripts lors de l’installation des packages npm à l’étape précédente de build, toute tentative malveillante d’injecter des données dans le fichier package.json du package serait vaine.

  3. Par mesure de précaution, notre étape publish inclut l’argument --ignore-scripts afin d’éviter l’exécution des scripts de cycle de vie npm pendant cette étape.

  4. Le jeton d’automatisation npm utilisé pour publier un package est uniquement mis à disposition de l’étape de publication.

Compte tenu de tout ce qui précède, vous dormiriez sans doute mieux en exigeant une authentification à deux facteurs à chaque publication d’une nouvelle version du package. Toutefois, sa mise en place dans un environnement CI est plus complexe, et nous n’aborderons pas ce sujet dans cet article.

Ce job ne s’exécute que sur la branche principale — nous ne publions donc rien lors des tests d’une pull request — et utilise une version précise de Node.js, au lieu de lancer des jobs en parallèle avec différentes versions.

Une fois une pull request GitHub fusionnée, ou un commit poussé vers la branche principale, le job publish suivant s’exécutera et publiera le package npm.

Workflow GitHub Actions montrant une publication réussie, avec les étapes de configuration, de récupération du code, de configuration de Node.js, de publication et de finalisation terminées

Consultez le fichier du workflow GitHub Actions complet à titre de référence.

Il est important de noter que nous n’avons pas abordé le versionnement sémantique automatisé, qui permet d’incrémenter automatiquement les versions de nos packages npm selon qu’un commit correspond à une mise à jour corrective, mineure ou majeure du code. Pour cela, je vous recommande d’évaluer semantic-release, que Snyk Advisor considère également comme un package très fiable :

Tableau de bord Snyk sur l’état du package semantic-release, affichant un score de 90/100, les tendances de téléchargement et des graphiques sur l’activité de maintenance.

Où s’exécutent les GitHub Actions ?

GitHub Actions s’applique à un dépôt spécifique. Les actions sont exécutées et gérées à l’aide des services d’infrastructure cloud de GitHub, qui prennent en charge des runners pour Mac, Windows et Linux. Il est également possible d’utiliser des runners GitHub auto-hébergés, par exemple sur Google Cloud Infrastructure.

Qu’est-ce que le workflow GitHub ?

GitHub Actions s’applique à un dépôt spécifique. Les actions sont exécutées et gérées à l’aide des services d’infrastructure cloud de GitHub, qui prennent en charge des runners pour Mac, Windows et Linux. Il est également possible d’utiliser des runners GitHub auto-hébergés, par exemple sur Google Cloud Infrastructure.

En résumé

En résumé, la création de packages npm et leur publication dans l’écosystème npm au sens large peuvent être automatisées de manière sécurisée avec GitHub Actions. L’avantage de GitHub Actions, c’est que toute l’expérience de développement liée à un workflow git reste au sein de la plateforme GitHub.

Je vous recommande également de découvrir d’autres intégrations GitHub Actions que vous pouvez facilement ajouter à votre projet :

Essayez mon is-website-vulnerable GitHub Action si vous souhaitez suivre les tests de sécurité de bout en bout d’un site web (détection des bibliothèques JavaScript vulnérables).

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.