Skip to main content

GitHub Actions zum sicheren Veröffentlichen von npm-Paketen

Artikel von

10. November 2020

0 Min. Lesezeit

GitHub Actions werden immer beliebter, seit GitHub die allgemeine Verfügbarkeit für alle Entwickler und Repositories auf der GitHub-Plattform angekündigt hat. Angesichts von Einschränkungen wie den neuen Abrechnungs- und Nutzungslimits für Open Source bei Travis CI werden noch mehr Entwickler ihre Softwareautomatisierungen auf GitHub Actions umstellen.

In diesem Artikel möchte ich Ihnen zeigen, wie ich GitHub Actions nutze, um npm-Pakete zu veröffentlichen, die ich in meinen Open-Source-Projekten pflege. Wenn Sie dem GitHub-Flow mit Workflows für GitHub-Pull-Requests folgen, können Sie so die GitHub-Workflows für Ihre Teams und Community-Mitwirkenden im Projekt noch besser vereinheitlichen.

Was ist GitHub Actions?

GitHub Actions ist eine von GitHub entwickelte Technologie, mit der Entwickler ihre Workflows rund um Continuous Integration automatisieren können. So können sie Software erstellen und bereitstellen, wiederkehrende Aufgaben planen und vieles mehr. GitHub Actions ist nativ verfügbar und in GitHub-Repositories integriert. Außerdem gibt es zahlreiche wiederverwendbare Workflows von Community-Mitwirkenden, etwa zum Veröffentlichen von npm-Paketen und Docker-Images, zum Ausführen von Sicherheitstests und mehr.

Wie funktionieren Actions in GitHub?

Die Cloud-Infrastruktur von GitHub führt GitHub Actions für Nutzer aus. Dazu wird im Verzeichnis eines GitHub-Repositories eine Workflow-Datei namens .github/workflows erstellt, in der der Auslöser, der Zeitplan für den Job und die Aktionen, die der Job ausführen soll, in YAML beschrieben sind.

Was ist ein GitHub-Workflow?

Ein GitHub-Workflow besteht aus einer Reihe von Jobs, die durch einen Auslöser oder einen Cron-Zeitplan gestartet werden. Ein Job umfasst einen oder mehrere Schritte, aus denen sich ein automatisierter Workflow zusammensetzt.

Ein Node.js-Projekt für GitHub Actions einrichten

Fügen wir einem Node.js-Projekt zunächst eine GitHub-Actions-Automatisierung hinzu. Der GitHub-Actions-Job installiert alle erforderlichen npm-Pakete, führt Tests aus und veröffentlicht schließlich unser Projekt als npm-Paket, das Nutzer verwenden können.

Unser npm-Paket wird eine Command-Line-Schnittstelle (CLI) sein, mit der Sie die beeindruckende Liste der Vorträge von SnykCon 2020 durchsuchen können – Snyks erstem globalen Sicherheitsevent, das 2020 stattfand.

Das vollständige Projekt finden Sie auf GitHub. Hier sehen Sie schon einmal, wie das npm-Paket aussieht:

Terminalähnliche Grafik mit einer Liste der Vorträge auf der SnykCon 2020, darunter Sessions zu DevSecOps und zur Sicherheit der Angriffsfläche im Frontend.

Ein vollständiger GitHub-CI-Workflow beginnt damit, die folgende GitHub-Actions-Datei im Stammverzeichnis des Repositorys zu erstellen: .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

Das obige Beispiel ist eine Standardvorlage für Build- und Testprozesse von Node.js-Projekten. Sie führt folgende Schritte aus:

  1. Der Auslöser ist jeder Push auf jeden Branch.

  2. Wir führen diese Action mit Node.js-Version 12.x und 14.x aus, um die Kompatibilität mit beiden Node.js-LTS-Versionen sicherzustellen.

  3. Anschließend führt der Job mehrere Schritte aus: Er checkt die Git-Codebasis aus, führt eine sichere und deterministische npm-Installation aus, startet gegebenenfalls einen Build-Schritt und führt schließlich Tests aus.

Wenn Sie neue Commits in das Repository pushen – unabhängig davon, ob Sie dem GitHub-Flow oder einem Workflow für GitHub-Pull-Requests folgen –, werden Sie neue Jobs sehen, die im Tab Actions des Repositorys in die Warteschlange gestellt werden. Hier sehen Sie beispielsweise, wie unsere Tests für das npm-Paket snykcon ausgeführt werden:

GitHub-Actions-Workflow für das snykcon-Repository mit einem erfolgreichen Node.js-12.x-Build und npm-Testlauf.

npm-Pakete mit GitHub Actions veröffentlichen

Wenn alle Tests erfolgreich sind und dieser Automatisierungsworkflow in unserem Main-Branch ausgeführt wird, können wir auch die Veröffentlichung neuer Versionen vollständig automatisieren!

Bei der Veröffentlichung von Paketen sollten Sie die damit verbundenen Sicherheitsrisiken berücksichtigen. Sehen wir uns einige davon an:

  1. Bösartiger Code: Wird eine Sicherheitslücke absichtlich von jemandem in einem Code-Beitrag eingefügt und übersehen Sie sie, steht sie Nutzern sofort zur Verfügung, wenn beim Zusammenführen in Ihren Main-Branch automatisch eine neue Version veröffentlicht wird. Bei einer manuellen Veröffentlichung direkt nach dem Zusammenführen gibt es keinen Unterschied. Je mehr Zeit jedoch zwischen dem Zusammenführen von Pull Requests und der Veröffentlichung vergeht, desto mehr Zeit hat die Community, solche Probleme zu prüfen und aufzudecken.

  2. Verwundbare Abhängigkeit: Fügt eine böswillige Person eine Abhängigkeit mit einer bekannten, öffentlich dokumentierten Sicherheitslücke hinzu, gelangt diese Abhängigkeit beim Installationsvorgang auch zu Ihren Nutzern. Mit einem kostenlosen Tool wie Snyk können Sie Ihre Git-Repositories verbinden und eine Statusprüfung einrichten, die die Veröffentlichung von Versionen mit verwundbaren Abhängigkeiten verhindert. Damit lässt sich genau dieses Problem lösen.

  3. Bösartige Abhängigkeit in einer Lockfile: Haben Sie schon einmal darüber nachgedacht, was passieren würde, wenn ein Beitrag zum Aktualisieren der Abhängigkeiten in package.json ein bösartiges Paket in die Lockfile einschleust, die ohnehin niemand prüft? Ich habe darüber geschrieben, warum npm-Lockfiles ein Sicherheitsrisiko darstellen können, wenn bösartige Module eingeschleust werden. Falls Sie diesen Angriffsvektor noch nicht berücksichtigt haben, sollten Sie sich das unbedingt genauer ansehen.

  4. Ihr npm-Token stehlen:In der Vergangenheit gab es zahlreiche Versuche, bei denen bösartige Pakete darauf abzielten, das npm-Token einer Person oder andere vertrauliche Informationen aus Umgebungsvariablen zu stehlen.

Die obige Liste umfasst nicht alle Sicherheitsrisiken, macht aber auf einige wichtige Risiken aufmerksam, die wir kennen sollten.

Apropos Sicherheitsrisiken: Finden Sie diese Liste möglicher Sicherheitsprobleme interessant und lehrreich? Dabei handelt es sich um einen kurzen Threat-Modeling-Prozess, den Sie beim Entwickeln neuer Funktionen regelmäßig anwenden können. Wir haben darüber in unserem DevSecOps Prozess geschrieben. Außerdem gibt es von Alyssa Miller auf der SnykCon einen guten Vortrag zum Thema User Story Threat Modeling: It’s the DevSecOps Way. Sehen Sie sich die Beiträge an!

Kehren wir zu unserer Liste zurück und sehen uns das zuletzt erwähnte Sicherheitsrisiko genauer an: den Diebstahl Ihres npm-Tokens. Da es in diesem Artikel um die Veröffentlichung von npm-Paketen geht, müssen wir dem GitHub-Actions-Workflow ein npm-Token bereitstellen. Das galt aus folgenden Gründen lange als nicht empfehlenswert:

  • npm-Funktionen: Früher mussten Sie für die Veröffentlichung von npm-Paketen mit einem npm-Token die Zwei-Faktor-Authentifizierung für Ihren npm-Nutzer deaktivieren. Das ist nicht mehr nötig – und wir zeigen Ihnen, wie es geht!

  • Token-Diebstahl durch die Installation bösartiger Pakete:Wenn Sie das npm-Token in Ihrem CI-System als Umgebungsvariable bereitstellen, können bösartige Pakete in Ihrem Abhängigkeitsbaum (auch jenseits Ihrer direkten Abhängigkeiten) während des Installationsvorgangs, etwa bei npm install, möglicherweise darauf zugreifen. Standardmäßig dürfen Pakete dabei beliebige Befehle ausführen.

Wie gehen wir also mit diesen beiden berechtigten Sicherheitsbedenken um?

Erstens ermöglicht npm mittlerweile die Zwei-Faktor-Authentifizierung und die Ausstellung von Automatisierungs-Token, sodass sich diese beiden Funktionen nicht mehr gegenseitig ausschließen. Zweitens können Sie mit GitHub Actions Umgebungsvariablen nur in einem bestimmten Schritt eines Jobs bereitstellen. Das bedeutet, dass wir das Token nur dem Befehl npm publish zur Verfügung stellen können, nicht aber npm install, wodurch auch indirekte Abhängigkeiten darauf zugreifen könnten.

Legen wir los.

Aktivieren Sie zunächst die Zwei-Faktor-Authentifizierung für Ihren npm-Nutzer. Rufen Sie die Kontoeinstellungen unter https://npmjs.com/ auf und aktivieren Sie den 2FA-Modus sowohl für die Autorisierung als auch für die Veröffentlichung.

Einrichtungsbildschirm für die Zwei-Faktor-Authentifizierung mit den Optionen „Autorisierung“, „Autorisierung und Veröffentlichung“ oder „Deaktivieren“; ausgewählt ist „Autorisierung und Veröffentlichung“

Sie müssen ein Authentifizierungsgerät verknüpfen, zum Beispiel die Google-Authenticator-App auf Ihrem Smartphone oder 1Password, falls Sie damit Ihre Passwörter verwalten.

Rufen Sie anschließend die Verwaltung der Access Tokens auf npm auf und erstellen Sie ein neues Token.

Dashboard für Zugriffstokens mit Paketsuche sowie den Steuerelementen „Neues Token generieren“ und „Ausgewählte Tokens löschen“

Erstellen Sie unbedingt ein Token vom Typ Automation. Wie im folgenden Screenshot zu sehen ist, umgeht dieser Token laut Beschreibung die Zwei-Faktor-Authentifizierung und lässt sich in Continuous-Integration-Workflows (CI) verwenden:

Formular für ein neues npm-Zugriffstoken mit ausgewählter Option „Automation“ unter den Token-Typen „read-only“, „automation“ und „publish“

Anschließend können wir dieses Token für GitHub Actions verfügbar machen. Erstellen Sie es dazu zunächst als Secret in der Secret-Verwaltung des GitHub-Repositorys, wie hier gezeigt:

GitHub-Repositoryeinstellungen für Secrets mit einem verschlüsselten NPM_TOKEN-Secret sowie den Schaltflächen „Aktualisieren“ und „Entfernen“

Aktualisieren Sie zum Schluss unseren GitHub-Actions-Workflow um einen Veröffentlichungsschritt. Fügen Sie nach dem Job build den folgenden Job publish hinzu:

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 }}

Beachten Sie unbedingt das Argument --ignore-scripts für den Befehl npm publish, das für einen sicheren Veröffentlichungsworkflow entscheidend ist. Es weist den Veröffentlichungsbefehl der npm-CLI an, alle im Manifest packge.json angegebenen Lebenszyklus-Skripte zu überspringen. Das ist wichtig: Wird ein bösartiges Paket als Teil des Abhängigkeitsgraphen installiert, kann es dem eigenen Paket einen Eintrag wie prepack: “echo ‘do something malicious’” hinzufügen. Dieser wird ausgeführt, wenn Sie npm publish aufrufen. Wie Sie sich vorstellen können, kann dieser bösartige Eintrag weit mehr Schaden anrichten, als nur etwas auf dem Bildschirm auszugeben.

Der hier verwendete Workflow verringert das Risiko, dass npm-Token gestohlen oder beliebige Befehle auf andere Ziele ausgeführt werden, erheblich, und zwar aus folgenden Gründen:

  1. Im Job build installieren wir Pakete, ohne ihnen die Ausführung beliebiger Befehle zu erlauben. Dafür sorgt das Befehlsargument --ignore-scripts für npm ci.

  2. Unser Job publish beginnt mit einem frischen Checkout des Repositorys. Selbst wenn bei der Paketinstallation im vorherigen Schritt build die Ausführung von Skripten erforderlich wäre, wären alle Versuche, Daten in die Datei package.json des Pakets einzuschleusen, damit wirkungslos.

  3. Als zusätzliche Vorsichtsmaßnahme enthält unser Schritt publish das Befehlsargument --ignore-scripts, damit während dieser Phase keine npm-Lebenszyklus-Skripte ausgeführt werden.

  4. Das npm-Automatisierungs-Token für die Veröffentlichung eines Pakets wird ausschließlich im Schritt publish bereitgestellt.

Angesichts all dieser Punkte würden Sie vermutlich beruhigter schlafen, wenn Sie für jede Veröffentlichung einer Paketversion die Zwei-Faktor-Authentifizierung verlangen. Dafür wäre in einer CI-Umgebung jedoch eine umfangreichere Konfiguration nötig, auf die wir in diesem Artikel nicht eingehen.

Dieser Job wird nur im Main-Branch ausgeführt – damit während eines Pull-Request-Testlaufs keine Veröffentlichung stattfindet – und verwendet nur eine bestimmte Node.js-Version, statt parallele Jobs für verschiedene Versionen auszuführen.

Sobald ein GitHub-Pull-Request zusammengeführt oder ein Commit in den Main-Branch gepusht wird, wird der folgende Job publish ausgeführt und die Veröffentlichung des npm-Pakets ausgelöst.

GitHub-Actions-Workflow mit einem erfolgreich abgeschlossenen Publish-Job und den erledigten Schritten für Setup, Checkout, Node.js, Publish und Abschluss

Eine Referenz finden Sie in der vollständigen GitHub-Actions-Workflow-Datei.

Wichtig ist, dass wir keine automatisierte semantische Versionierung behandelt haben, bei der die Versionen unserer npm-Pakete automatisch erhöht werden – je nachdem, ob ein Commit einen Patch, ein Minor- oder ein Major-Update am Code darstellt. Dafür empfehle ich Ihnen, semantic-release zu prüfen. Auch Snyk Advisor stuft das Paket als sehr gesund ein:

Snyk-Dashboard zum Zustand des Pakets semantic-release mit einem Wert von 90/100, Download-Trends und Diagrammen zur Wartungsaktivität.

Wo werden GitHub Actions ausgeführt?

GitHub Actions gelten für ein bestimmtes Repository. Sie werden über die Cloud-Infrastruktur von GitHub ausgeführt und verwaltet. Dabei werden Runner für macOS, Windows und Linux unterstützt. Es ist jedoch auch möglich, selbst gehostete GitHub-Runner zu verwenden, zum Beispiel auf der Google-Cloud-Infrastruktur.

Was ist der GitHub-Flow?

GitHub Actions gelten für ein bestimmtes Repository. Sie werden über die Cloud-Infrastruktur von GitHub ausgeführt und verwaltet. Dabei werden Runner für macOS, Windows und Linux unterstützt. Es ist jedoch auch möglich, selbst gehostete GitHub-Runner zu verwenden, zum Beispiel auf der Google-Cloud-Infrastruktur.

Zusammenfassung

Zusammenfassend lässt sich sagen: Mit GitHub Actions können Sie die Erstellung von npm-Paketen und ihre Veröffentlichung im gesamten npm-Ökosystem sicher automatisieren. Ein Vorteil von GitHub Actions ist, dass wir die Developer Experience eines Git-Workflows vollständig innerhalb der GitHub-Plattform beibehalten.

Außerdem empfehle ich Ihnen, sich weitere GitHub-Actions-Integrationen anzusehen, die Sie ganz einfach in Ihr Projekt einbinden können:

Probieren Sie meine is-website-vulnerable GitHub Action aus, wenn Sie durchgängige Sicherheitstests für eine Website nachverfolgen möchten (zum Erkennen anfälliger JavaScript-Bibliotheken).

Starten Sie mit Capture the Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.