Skip to main content

GitHub Actionsでnpmパッケージを安全に公開する

著者

2020年11月10日

0 分で読めます

GitHubがプラットフォーム上のすべての開発者とリポジトリに対して一般提供を開始して以来、GitHub Actionsの人気は高まっています。エコシステムで見られるレート制限の一部、たとえばTravis CIによるオープンソース向けの新たな課金とレート制限も、開発者がソフトウェアの自動化をGitHub Actionsへ移行する流れをさらに後押しするでしょう。

この記事では、オープンソースプロジェクトで管理しているnpmパッケージを公開するために、私がGitHub Actionsをどのように使っているかをご紹介します。GitHubのプルリクエストワークフローを中心としたGitHub Flowを採用しているなら、GitHubワークフローを軸に、チームやプロジェクトのコミュニティ貢献者の体験をさらに統一できます。

GitHub Actionsとは?

GitHub Actionsは、継続的インテグレーションを中心としたワークフローを自動化する手段を開発者に提供するためにGitHubが開発したテクノロジーです。ビルドやデプロイ、定期タスクのスケジュール設定などに役立ちます。GitHub ActionsはGitHubリポジトリで標準搭載され、統合されています。また、npmパッケージの公開、Dockerイメージの公開、セキュリティテストの実行など、コミュニティ貢献者が作成した再利用可能なワークフローが数多くあります。

GitHub Actionsの仕組み

GitHubのクラウドインフラストラクチャは、GitHubリポジトリ内に.github/workflowsというディレクトリを作成し、トリガーやジョブのスケジュール、YAMLを使ってジョブで実行するアクションを記述したワークフローファイルを用意することで、ユーザー向けにGitHub Actionsを実行します。

GitHubワークフローとは?

GitHubワークフローとは、トリガーまたはcronベースのスケジュールに基づいて実行されるジョブの集まりです。ジョブは、自動化されたワークフローを構成する1つ以上のステップから成ります。

GitHub Actions用にNode.jsプロジェクトをセットアップする

まず、Node.jsプロジェクトに基本的なGitHub Actionsの自動化を追加しましょう。GitHub Actionsのジョブで必要なnpmパッケージをすべてインストールし、テストを実行したうえで、ユーザーが利用できるnpmパッケージとしてプロジェクトを公開します。

今回のnpmパッケージは、2020年に開催されたSnyk初のグローバルセキュリティイベント、SnykCon 2020の講演一覧を閲覧するためのコマンドラインインターフェース(CLI)です。

プロジェクトの完全版はGitHubで公開されています。npmパッケージの概要を見てみましょう。

SnykCon 2020の講演一覧を表示したターミナル風の画像。DevSecOpsやフロントエンドの攻撃対象領域のセキュリティに関するセッションを含む。

GitHub CIワークフローを構成するには、まずリポジトリのルートに次のGitHub Actionsファイルを作成します。.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

上記は、Node.jsプロジェクトのビルドとテストに使う標準的なテンプレートで、次の処理を行います。

  1. すべてのブランチへのプッシュがトリガーになります。

  2. Node.jsの両方のLTSバージョンとの互換性を確保するため、Node.js 12.xと14.xでこのアクションを実行します。

  3. 次にジョブで、Gitコードベースをチェックアウトし、安全かつ再現性のあるnpmインストールを実行して、必要に応じてビルドを行い、最後にテストを実行する複数のステップを処理します。

GitHub FlowまたはGitHubのプルリクエストワークフローを使っているかどうかにかかわらず、リポジトリに新しいコミットをプッシュすると、リポジトリのActionsタブに新しいジョブがキューに追加されていくのがわかります。たとえば、npmパッケージsnykconのテストが実行されている様子は次のとおりです。

snykconリポジトリのGitHub Actionsワークフロー。Node.js 12.xのビルドとnpmテストの実行が成功したことを示しています。

GitHub Actionsでnpmパッケージを公開する

すべてのテストに合格し、この自動化ワークフローがメインブランチで実行されたら、新しいバージョンのリリースもすべて自動化できます。

パッケージをリリースする際は、その作業に伴うセキュリティ上の影響を考慮することが大切です。いくつか確認してみましょう。

  1. 悪意のあるコード:コードの貢献に脆弱性が意図的に追加されているのを見逃した場合、メインブランチへのマージ時に自動リリースすると、すぐにユーザーがそのコードを利用できるようになります。手動ですぐにリリースする場合も違いはありません。ただし、プルリクエストのマージからリリースまでの時間が長いほど、コミュニティが精査し、問題の解明に協力する時間を確保できます。

  2. 脆弱な依存関係:悪意のある人物が、既知の公開脆弱性を含む依存関係を追加すると、インストール時にその依存関係が取り込まれ、ユーザーにも影響が及びます。Snykのような無料ツールをGitリポジトリに接続し、依存関係に脆弱なバージョンが含まれる状態でのリリースを防ぐステータスチェックを追加すれば、この問題を解決できます。

  3. ロックファイルを介した悪意のある依存関係:package.jsonの依存関係を更新する貢献によって、誰も確認しないロックファイルに悪意のあるパッケージが挿入されたらどうなるか、考えたことはありますか?npmのロックファイルが悪意のあるモジュールを挿入するためのセキュリティ上の盲点になり得る理由について記事を書きました。この攻撃経路をまだ検討していない場合は、ぜひ詳しく確認してください。

  4. npmトークンの窃取:過去には、悪意のあるパッケージによってnpmトークンや、環境変数に含まれるその他の機密情報を盗み出そうとする試みが数多くありました。

上記はセキュリティ上の懸念事項を網羅したものではありませんが、注意すべき点をいくつも示しています。

セキュリティ上の注意点といえば、このような懸念事項のリストは、興味深く学びのある内容だと思いませんか?これは簡単な脅威モデリングのプロセスで、機能を開発する際に、より頻繁に実践できます。詳しくはDevSecOpsのプロセスで紹介しています。また、Alyssa MillerによるSnykConの講演SnykCon「ユーザーストーリーの脅威モデリング:DevSecOps流のアプローチ」もおすすめです。ぜひご覧ください。

リストに戻り、最後に挙げたセキュリティ上の懸念、npmトークンの窃取に注目しましょう。この記事ではnpmパッケージの公開を扱うため、GitHub Actionsワークフローでnpmトークンを使えるようにする必要があります。これまでそれが推奨されてこなかった理由は、次のとおりです。

  • npmの機能:これまで、npmトークンを使ってパッケージを公開するには、npmユーザーの2要素認証を無効にする必要がありました。現在はその必要はありません。その方法を見ていきましょう。

  • 悪意のあるパッケージのインストールによるトークン窃取:npmトークンをCIの環境変数として利用できるようにすると、依存関係ツリー内(直接の依存関係以外も含む)にある悪意のあるパッケージが、たとえばnpm installの実行中にトークンへアクセスする可能性があります。npm installでは、デフォルトでパッケージが任意のコマンドを実行できます。

では、この2つの正当な懸念にどう対処すればよいでしょうか?

まず、npmでは最近、2要素認証と自動化トークンを併用できるようになったため、この2つの機能が競合することはありません。次に、GitHub Actionsでは、ジョブ内の特定のステップだけで環境変数を利用できるように設定できます。つまり、npm publishコマンドでのみ利用可能にし、間接的な依存関係からもアクセスできてしまうnpm installでは利用できないようにできます。

さっそく始めましょう。

まず、npmユーザーの2要素認証を有効にします。https://npmjs.com/のアカウント設定に移動し、認証と公開の両方で2FAモードを有効にしてください。

「認証」「認証と公開」「無効化」の選択肢が表示され、「認証と公開」が選択された2要素認証の設定画面

次に、認証デバイスを登録します。たとえば、モバイル端末のGoogle Authenticatorアプリや、パスワード管理に使っている場合は1Passwordを利用できます。

続いて、npmのアクセストークン管理画面に移動し、新しいトークンを作成します。

パッケージ検索、「新しいトークンを生成」、「選択したトークンを削除」の操作項目があるアクセストークンのダッシュボード

下のスクリーンショットに示すように、必ずAutomationタイプのトークンを作成してください。説明にあるとおり、これは2要素認証をバイパスでき、継続的インテグレーション(CI)ワークフローから使用できます。

npmの新しいアクセストークン作成フォーム。読み取り専用、自動化、公開のトークンタイプの中から「自動化」が選択されています

次に、GitHubリポジトリのシークレット管理でシークレットとしてトークンを作成し、GitHub Actionsから利用できるようにします。

暗号化されたNPM_TOKENシークレットと「更新」「削除」ボタンが表示された、GitHubリポジトリのシークレット設定

最後に、GitHub Actionsワークフローを更新して、リリースステップを追加します。buildジョブの後に、次のpublishジョブを追加してください。

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

--ignore-scripts引数をnpm publishコマンドに指定することは、安全な公開ワークフローに欠かせないため、必ず注意してください。この引数を指定すると、npm CLIの公開コマンドは、packge.jsonマニフェストで指定されたライフサイクルスクリプトをすべてスキップします。これは重要です。依存関係グラフ内の悪意のあるパッケージがインストールされると、自分のパッケージにprepack: “echo ‘do something malicious’”のようなエントリーを追加する可能性があり、npm publishの実行時にトリガーされます。ご想像のとおり、この悪意のあるエントリーは、画面に何かを表示するだけにとどまらず、さらに大きな被害をもたらす可能性があります。

ここで説明するワークフローに従うと、npmトークンの窃取や、任意のコマンド実行によるその他の被害の懸念を大幅に軽減できます。理由は次のとおりです。

  1. buildジョブでは、npm ciに--ignore-scriptsコマンドライン引数を指定することで、パッケージのインストール時に任意のコマンドが実行されないようにします。

  2. publishジョブは、新たにチェックアウトしたリポジトリで開始します。つまり、前のbuildステップでnpmパッケージのインストール時にスクリプトの実行を許可する必要があったとしても、パッケージのpackage.jsonファイルへのデータの挿入を試みる悪意のある処理はすべて無効になります。

  3. 予防策として、publishステップには--ignore-scriptsコマンド引数を指定し、この段階でnpmのライフサイクルスクリプトが実行されないようにしています。

  4. パッケージのリリースに使うnpm自動化トークンは、公開ステップでのみ利用できます。

以上を踏まえると、パッケージのバージョンを公開するたびに2要素認証を必須にすれば、さらに安心できるでしょう。ただし、CI環境でこれを有効にするには、より複雑なセットアップが必要になるため、この記事では扱いません。

このジョブはメインブランチでのみ実行されるため、プルリクエストのテスト実行時にリリースされることはありません。また、異なるバージョンでジョブを並列実行せず、特定のNode.jsバージョンでのみ実行します。

GitHubのプルリクエストがマージされるか、メインブランチにコミットがプッシュされると、次のpublishジョブが実行され、npmパッケージのリリースが開始されます。

セットアップ、チェックアウト、Node.js、公開、完了の各ステップが完了し、公開ジョブが成功したGitHub Actionsワークフロー

参考として、GitHub Actionsワークフローの完全版をご覧ください。

重要な点として、コミットのプッシュによるコード変更がパッチ、マイナー、メジャーのどれに当たるかに応じてnpmパッケージのバージョンを自動で更新する、セマンティックバージョニングの自動化については触れていません。この用途にはsemantic-releaseの評価をおすすめします。Snyk Advisorでも、非常に健全なパッケージとして紹介されています。

semantic-releaseのSnykパッケージ健全性ダッシュボード。スコア90/100、ダウンロード数の推移、メンテナンス状況のグラフを表示。

GitHub Actionsはどこで実行されますか?

GitHub Actionsは特定のリポジトリを対象とし、GitHubのクラウドインフラストラクチャサービスを使って実行、管理されます。Mac、Windows、Linuxの各プラットフォームのランナーに対応しています。ただし、Google CloudなどでGitHubランナーをセルフホストすることも可能です。

GitHub Flowとは?

GitHub Actionsは特定のリポジトリを対象とし、GitHubのクラウドインフラストラクチャサービスを使って実行、管理されます。Mac、Windows、Linuxの各プラットフォームのランナーに対応しています。ただし、Google CloudなどでGitHubランナーをセルフホストすることも可能です。

まとめ

まとめると、GitHub Actionsを使えば、npmパッケージのビルドとnpmエコシステムへの公開を安全に自動化できます。GitHub Actionsを選ぶメリットは、GitHubプラットフォーム内でGitワークフローを使った開発者体験全体を維持できることです。

プロジェクトに簡単に導入できるGitHub Actionsのインテグレーションについても、ぜひご覧ください。

ウェブサイトのエンドツーエンドのセキュリティテスト(脆弱なJavaScriptライブラリの検出)を追跡したい場合は、is-website-vulnerable GitHub Actionをお試しください。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。