パブリック コード リポジトリでは一般的に、任意のユーザーがフォークを実行しプル リクエストを発行することが許可されています (この記事ではこれを「フォークされた PR」と呼びます)。これらのユーザーは組織外からアクセスしている可能性があるため、自動ビルドを実行する目的では、一般的に未信頼のユーザーと見なされます。このことは、継続的インテグレーション (CI) サービスを利用して、認証情報や機密データへのアクセスを伴うテストを実行する場合に問題となります。悪意ある開発者の提案したコードによって認証情報が CI ログに開示されてしまう可能性があるためです。プライベート リポジトリの場合でも、組織内の一部のユーザーにはリポジトリの閲覧権限を提供して、シークレットを開示するビルドをトリガーする可能性をなくすことが必要な場合があります。

以前、「外部コントリビューターのプル リクエストがある場合のシークレットの管理」で、フォークされた PR から認証情報を非表示にして、認証情報を必要とするビルドをスキップする方法について説明しました。ただしこの記事では、認証情報を取得するビルドが、ビルド アーティファクトのステージングを目的としていることと、master へのマージにのみ本当に必要であることを前提としていました。このため、信頼済みジョブの出力を、マージ前の適切な変更レビューに使用する場合には、ジョブをスキップできません。必要とされているのは、フォークされた PR をチームの信頼済みメンバーがチェックし、シークレットを開示するような変更が追加されていないことを確認してから信頼済みの CI ジョブを実行して、変更の整合性を確保する方法です。この記事の前半では、Git 自体を使用してコードを信頼済みとしてマークすることで、このようなワークフローを有効化する方法について説明します。後半では、この概念を具体的なリポジトリ ホスト (GitHub) と CI プロバイダー (CircleCI) に適用する方法を、詳細なデモンストレーションを通じて解説します。

アップストリームにプッシュすることで、コードを信頼済みとしてマークする

プル リクエストをリポジトリにマージする行為は、部分的には、コードを有効で信頼できるものとしてマークする行為だと言えます。フォークは、ユーザーによる変更が検証されるまでの間、その変更を隔離する方法として捉えることができます。外部コントリビューターは通常、コードをメイン リポジトリのどのブランチにもプッシュできません。プッシュできるのは自分のフォークに対してだけです。

GitHub などのプロバイダーでは、セキュリティ コントロールをセットアップすることで、このようなモデルをサポートできます。これは一般的には、特定の名前付きユーザーのグループだけがリポジトリにアクセスし、変更を加えられるというモデルです。同様に CI プロバイダーの多くが、このモデルを前提に権限をセットアップしています。一般的な CI システムでは、メイン リポジトリへのプッシュによりトリガーされたジョブだけに認証情報を開示するよう構成されています。

ビルド構成を直接リポジトリに置くすべてのシステムでは、ビルドが PR の作成時に自動的にトリガーされる場合、未信頼のコードを実行する危険性があります。しかしこのことは、アップストリーム ブランチにコードをプッシュする行為を信頼の表現として使用し、信頼済みジョブを実行できるということでもあります。フォークされた PR のコードをアップストリーム ブランチにプッシュすると、アップストリーム ブランチとフォークされたブランチの Git リファレンス (「refs」) は、同じコミットを参照する同一のものになります。GitHub はこれらが同一のコードであることを認識します。GitHub の API エンドポイントの多くにとって重要なのは、参照が関連付けられているブランチではなく、コミットの参照だけです。CI ジョブの場合、GitHub はコミット単位でインデックスを作成するので、アップストリーム ブランチにコードをプッシュした結果として実行される信頼済みジョブは、同じコミットを参照するフォークされた PR にも関連付けられます。

信頼済み CI ジョブをトリガーするワークフローを実装する

では、上記の理論を実践するサンプル プロジェクトを作成してみましょう。まず、フォークからのものを含むあらゆる PR を実行できる基本的な test ジョブを 1 つ用意します。同時に、Amazon S3 からプライベート データを取得するために AWS の認証情報を必要とする、より包括的な test-with-data ジョブを用意します。後者のジョブについては、リポジトリのコミット権限を持つユーザーが PR をレビューして、認証情報が漏えいする可能性がないことを確認するまで、トリガーされないようにします。

プロジェクトを作成する

外部コントリビューターのプル リクエストがある場合のシークレットの管理」で作成したものと同じ、小さな Java プロジェクトから開始します。ビルド ツールには Apache Maven を使用し、次のようにプロジェクトを生成します。

mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=managing-secrets -DarchetypeArtifactId=maven-archetype-quickstart -Dversion=1.3 -DinteractiveMode=false

次に、test ジョブを 1 つ実行するシンプルな CircleCI ワークフローを作成します。

version: 2.1

jobs:
  test:
    docker:
      - image: circleci/openjdk:8u171-jdk
    steps:
      - checkout
      - run: mvn clean test

workflows:
  version: 2
  build:
    jobs:
      - test

これを GitHub にコミットし、CircleCI のプロジェクトとして有効化すると、プッシュするたびに CircleCI で build ワークフローの実行がトリガーされるようになります。メイン リポジトリの任意のブランチから発行された PR には、test ジョブのステータスが表示されます。

フォークされたプル リクエストの CI を有効にする

ここで、フォークされたリポジトリからのプル リクエストでも、同じワークフローを有効化したいと思います。これにより、コミット権限を持たないコントリビューターによる変更提案が可能になるだけでなく、個人のフォークからの作業を好むコミッターも作業がしやすくなります。

フォークされたプル リクエストの CI を有効にするには、CircleCI のプロジェクト設定ページに移動し、Build Settings (ビルド設定) > Advanced Settings (詳細設定) を選択して、Build Forked Pull Requests (フォークされたプル リクエストをビルド)] オプションを有効にします。

同じ画面で、このオプションの隣に [Pass secrets to builds from forked pull requests (フォークされたプル リクエストからビルドにシークレットを渡す)] があります。このオプションは既定では無効になっていますが、これがまさに今回の目的に必要な状態です。次の手順では、AWS の認証情報をアップロードすると共に、組織外のユーザーに誤って開示されないようにします。

シークレットを追加する

AWS の認証情報を信頼済みビルドで利用できるようにするには、この認証情報をプロジェクト固有の環境変数として設定します。また、コンテキストを作成して複数のプロジェクトで共有する方法もあります。

次に、CircleCI プロジェクト設定ページの Build Settings (ビルド設定) > Environment Variables (環境変数) セクションに移動して、変数 AWS_ACCESS_KEY_ID および AWS_SECRET_ACCESS_KEY を追加します。プライベート テスト データをステージングした Amazon S3 の場所からの読み取りを許可された認証情報は、これらの変数に格納されます。これらの変数は、コミット権限を持つユーザーが開始したメイン リポジトリのブランチへのプッシュに対してのみ設定されます。フォークされたプル リクエストから開始された CircleCI ジョブに対しては設定されません。

フォークされた PR の場合には実行されない信頼済みジョブを追加する

この時点で、認証情報にアクセスし、プライベート データに基づいてテストを実行する追加ジョブのセットアップ準備が整いました。更新された構成を確認してみましょう。

version: 2.1

jobs:
  test:
    docker:
      - image: circleci/openjdk:8u171-jdk
    steps:
      - checkout
      - run: mvn clean test
  test-with-data:
    docker:
      - image: circleci/openjdk:8u171-jdk
    steps:
      - checkout
      - run: |
          if [ -z "$AWS_ACCESS_KEY_ID" ]; then
            echo "No AWS_ACCESS_KEY_ID is set! Failing..."
            exit 1;
          else
            echo "Credentials are available. Let's fetch the data!"
          fi

workflows:
  version: 2
  build:
    jobs:
      - test
      - test-with-data:
        filters:
          branches:
            # Forked pull requests have CIRCLE_BRANCH set to pull/XXX
            ignore: /pull\/[0-9]+/

この新しい test-with-data ジョブには、1 点省略した部分があります。今回取り上げる内容を簡潔にするために、実際のデータ取得や、そのデータを使用するテストを実装していないのです。その部分を省略し、認証情報の存在をチェックするのみにとどめています。このジョブがメイン リポジトリへのプッシュによって開始されれば、環境変数が利用可能になり、ジョブは成功するはずです。

ワークフローに配置した test-with-data ジョブには、フォークされた PR でトリガーされた場合はジョブが実行されないようなフィルターを設定しています。前回の記事では、フィルターの代わりに、フォークされた PR の場合には早期段階で返すような手法を実装していました。フィルターでも処理内容はほぼ同じですが、より洗練された方法となります。

フォークされた PR のレビュー ワークフローをテストする

テストがスキップされていることをさらに明確化するために、わずかな構成の変更をリポジトリに加えてみます。結局のところ、実際のデータでテストを実行するために、余計な手間をかけて信頼済みジョブを追加しているというのが問題の中心です。つまり、それらのテストを確実に成功させることが、プロジェクトの整合性を確保する最重要事項だと考えられます。

GitHub リポジトリで Settings (設定) > Branches (ブランチ) に移動し、Branch Protection Rule (ブランチ保護ルール) を追加します。今回のケースではターゲット ブランチが triggering であるため、[Require status checks to pass before merging (マージにはステータス チェック合格が必須)] トグルを有効にして、必要に応じて (ci/circleci: test および ci/circleci: test-with-data として表示される) 両方の CI ジョブを選択します。

ここで、あるユーザーがフォークから PR を発行すると、test ジョブが実行され、(問題がなければ) 成功を示す緑色になります。test-with-data ジョブは実行されませんが (フォークされた PR であるため)、必須として指定しているので、PR ページのステータス チェックのセクションに「Expected — Waiting for status to be reported (必須 - ステータスの報告を待機中)」と表示され、マージの前に当該ジョブを実行する必要があることをレビュアーに通知します。さらに良い方法は、PULL_REQUEST_TEMPLATE.md を作成してレビュアー向けチェックリストを追加し、信頼済みビルドを実行する前に潜在的なセキュリティの問題を検証するよう明示的に指示することです。

この状態の PR について「jklukas/managing-secrets#4」を例に考えてみましょう。この PR を見たレビュアーは、コードの初期スキャンを実施し、認証情報をログに出力する変更が CI ジョブに加えられていることに気付き、変更依頼または PR のクローズのいずれかを行います。これによりtest-with-data は実行されず、認証情報が開示されることはありません。

また、変更が有害なものではなく実行しても安全とレビュアーが判断した場合、PR のコミットをメイン リポジトリのブランチにプッシュすることで、信頼済みジョブを実行できます。これは git を直接起動して手動で実施できますが、少し面倒です。この一般的なワークフローは、PR のフォークをリモートとして追加し、PR のブランチをプル ダウンし、そのブランチをメイン リポジトリにプッシュしてクリーン アップするというものです。jklukas/git-push-fork-to-upstream-branch to make this a more convenient one-liner: の小さな bash スクリプトをインストールすると、この処理を次のように 1 行で実行できます。

git-push-fork-to-upstream-branch upstream <fork_username>:<fork_branch>

アップストリーム ブランチにコードがプッシュされると、フォークされた PR の test-with-data のステータス チェックが実行状態になり、問題がなければ完全にグリーンな状態 (成功) に移行し、マージの準備が完了します。

参考情報

外部コントリビューターのプル リクエストがある場合のシークレットの管理」では、CI における認証情報管理のオプションについて詳しく説明し、アーティファクトのステージングに認証情報を必要とする CI ジョブで、フォークされた PR を早期段階で返すような手法を検討しました。

今回の記事では、この手法をさらに洗練させ、リポジトリ コミッターがアップストリーム ブランチにコミットをプッシュすることで、フォークされた PR で信頼済みビルドをトリガーする方法を探りました。この手法が実際に使用されている、より現実的な例は mozilla-services/mozilla-pipeline-schemas で確認できます。Mozilla では、このリポジトリに格納されている JSON スキーマを使用して、データ パイプラインが受信するペイロードのバリデーションを行っています。このユース ケースの最重要目標は、実際のクライアント データのサンプルに対するすべての変更をテストして、以前有効だったペイロードのクラスに対する拒否をパイプラインに生じさせるようなリグレッションをすべて特定することです。このリポジトリが受信する PR は通常 Mozilla のスタッフが提案していますが、スタッフは通常メイン リポジトリへの書き込み権限を持たないため、PR はフォークから発行されます。このため、フォークされた PR の提案とテストの円滑なプロセスをサポートすることが重要なのです。

また、CircleCI が最近発表した制限付きコンテキスト という新機能についてもご確認ください。制限付きコンテキストを使用すると、手動での承認ステップによってトリガーされたワークフロー グラフの一部にシークレットを挿入することができます。これを基にして、信頼済みビルドをトリガーするレビュアーのために、さらに効率的かつ柔軟なアプローチを実現できるようになります。


Jeff Klukas 氏は、実験素粒子物理学の知識と経験をバックグラウンドに、教師と研究者という 2 つの立場で活動されており、ヒッグス粒子の発見にも貢献しています。現在はオハイオ州コロンバスを拠点とするリモート ワーカーとして、Mozilla の Firefox データ プラットフォームに携わっています。以前は、支店を持たないクラウド銀行 Simple のデータ プラットフォーム担当テクニカル リードを務めていました。