リモート開発メインのソフトウェア開発企業のエンジニアブログです

[GitHub Actions] Secrets や書き込み権限が必要な Workflow を Dependabot からも使えるようにする

先月、とある GitHub リポジトリで使っている Dependabot が送ってくる Pull request の CI が軒並み落ちるようになり、状況を見てみるとどうやらリポジトリの Secrets がうまく Workflow のジョブに渡せていない事が原因の様でした。

公式ブログによると 2021 年 3 月 1 日以降、 Dependabot が特定のいくつかのトリガーにより開始した Workflow (※) では Secrets が読み込めなくなり、また GITHUB_TOKEN についても書き込み権限が剥奪され、リポジトリへの書き込み操作ができなくなってしまった様です。

具体的には、 Dependabot からの Pull request は書き込み権限を有さないユーザーからの物と同様 (Fork 経由と同様) になっている様で、元々このポリシーについては去年の記事 Keeping your GitHub Actions and workflows secure: Preventing pwn requests でも言及されています。
またこの件は、 Dependabot の公式リポジトリでも Issue が立てられて議論されています。

今回はこの問題に対処した方法を紹介します。

※記事にもある通り、 pull_requestpull_request_reviewpull_request_review_commentpush が対象

免責

今回の記事ではセキュリティに関わる内容を取り扱っています。今回私が使った対処法はあくまで一例であり、セキュリティが完璧に担保される事は保証していませんので予めご了承の上、同様の手法を使う場合は自己責任にてよろしくお願い致します。

Moba Pro

前提

さて、次節より具体的な解決法を記しますがその前にいくつか前提をお知らせしたいと思います。

  • リポジトリgithub.com にホストされているプライベートリポジトリ
  • Dependabot: GitHub に統合された後の、github.com 上で動作している bot
    • 試していないけど dependabot.com 版はこれまで通り何もしなくても動いていると予想
  • Workflow について: ジョブ内において、予めリポジトリに登録していた AWS のクレデンシャルと、 Slack の Webhook URL を必要とし、これを Secret 経由で渡していた

どのように解決したか

先述した記事にも書いてあるのですが、具体的には pull_request_target を使って解決しました。

今回はサンプルリポジトリ (Public) を使って説明したいと思います。
https://github.com/issei-m/dependabot-secrets-test

今回、改修後の Workflow の設定は以下のようになっています:

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  pull_request_target:
    branches: [main]

jobs:
  main:
    name: Run main jobs
    runs-on: ubuntu-latest
    # push, pull_request は Dependabot 以外のユーザーのみ、
    # pull_request_target は Dependabot のみが実行できる
    if: |
      (github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]') ||
      (github.event_name != 'pull_request_target' && github.actor != 'dependabot[bot]')
    steps:
      - name: Checkout
        if: ${{ github.event_name != 'pull_request_target' }}
        uses: actions/checkout@v2

      # pull_request_target 駆動の場合、コンテキストは Pull request のターゲットブランチ (main ブランチ) になるので、
      # 当該 Pull request の HEAD コミットを明示的に指定しないと変更内容に対する CI を実行できない
      - name: Checkout PR
        if: ${{ github.event_name == 'pull_request_target' }}
        uses: actions/checkout@v2
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          
      # ...    

※ dependabot[bot] ユーザーは Dependabot が所有している bot ユーザーですので、サンプルリポジトリ上では issei-m2 が bot の代わりとして定義されていますので読み替えて下さい。

実際の挙動を見てみましょう。

まずは、書き込み権限を有するユーザー (多くのプライベートリポジトリの場合、開発者は全員これに該当すると思われる) からの Pull request:
[General] Pull request from a member who has writeable permission (or admin)

Pull request の description にも記載していますが、この Workflow では pull_requesss トリガー経由でのみジョブが実行され、 pull_request_target 経由の場合は Skip されています:

on: pull_request_target のジョブは Skip されている

また、実行された Workflow では SENSITIVE_INFO Secret が適切に渡っており、ジョブで動かしている run.js が内容を出力できてる事が分かります。これは今まで通りで特に問題がありません:

SENSITIVE_INFO の内容が出力されている

※余談ですが、通常ジョブに渡された Secret の内容は秘匿性保護の為コンソールには出力されないようになっていますが、単にコンソールに出力された内容から Secret の内容を空欄に置換しているだけの様なので、 run.js のようにすると内容を出力する事が可能です。

次に、 Dependabot からの Pull request (を模したもの): Pull request from a Dependabot

先程と異なり、 pull_request_target 経由のジョブのみ実行されており、反対に pull_request 経由での物は Skip となっています:

on: pull_request のジョブは Skip されている

しかし Workflow では先程と同様、 SENSITIVE_INFO が渡ってきている事が分かります:

SENSITIVE_INFO の内容が出力されている

これでやりたかった事は実現できました。

最後に、読み込み権限しか持たないユーザーからの Pull request です。これは Fork からの Pull request を含みます: Pull request from a member who has read-only permission

最初の Action 同様、 pull_request 経由の Workflow のみジョブが実行されており、そこでは SENSITIVE_INFO は渡ってきていない事が分かります:

SENSITIVE_INFO の内容は出力されていない

また、上に記述した YAML の設定にもコメントで記載したとおり、 pull_request_target 駆動の Workflow は Pull request のターゲットブランチでのコンテキストで実行されます。これは、 Workflow 自体の設定ファイルもこのブランチ上の物が使われます。従って、仮にこれらのユーザーに設定ファイルを (Dependabot ユーザーでなくても起動できる様) 書き換えられたとしても、 pull_request 駆動の Workflow しか起動できないので、 secret は守られます。

プライベートリポジトリではあまり使わないとは思いますが、このプラクティスはオープンソースの方で役立つかもしれません。

注意点

さて、今回の方法は Workflow を起動した人物によって挙動を変えています。つまり、 Dependabot が作った Pull request 用の Workflow を、Actions メニュー等で他者が再起動した場合、 1 個目や 3 個目の様に pull_request 駆動の物だけが実行されます。この場合でも Workflow には Secret が渡ってこないので注意が必要です。

幸い、 Dependabot には @dependabot recreate と言うコマンドが有り、この内容を Pull request で書き込み権限を有するユーザーがコメントすれば新しい Pull request が作られるのでこの方法で Workflow を再起動できます。

以上、 Secrets や書き込み権限が必要な Workflow を Dependabot でも使える方法でした。

おまけ: Dependabot の Pull request の CI がパスした時に自動でマージする

これも紹介した Issue に書かれていますが、 workflow_run トリガーを使うと、 Dependabot が CI をパスした時に自動的にマージを行う事ができます。具体的には以下のような Workflow を別途用意します:

name: Dependabot Auto-merge

on:
  workflow_run:
    workflows:
      - Main # CI を実行しているワークフローの名前を記載する
    types:
      - completed

jobs:
  auto_merge:
    runs-on: ubuntu-latest
    if: |
      github.actor == 'dependabot[bot]' &&
      github.event.workflow_run.conclusion == 'success'
    steps:
      - name: Automerge Dependabot
        uses: actions/github-script@v4.0.2
        with:
          # 予め WRITABLE_GITHUB_TOKEN Secret に書き込み権限を持つユーザーのアクセストークンを登録しておく
          github-token: ${{ secrets.WRITABLE_GITHUB_TOKEN }}
          script: |
            const output = `@dependabot squash and merge`;
            github.issues.createComment({
              issue_number: ${{ github.event.workflow_run.pull_requests[0].number }},
              owner: "${{ github.event.repository.owner.login }}",
              repo: "${{ github.event.repository.name }}",
              body: output
            })

on.workflow_run.workflows[0] には、 CI で使っている Workflow の name で指定している名前を指定します。
仕組みは単純で、単に Dependabot が作った Pull request の CI がパスした際に、その Pull request に @dependabot squash and merge とコメントするだけです。これは Dependabot のコマンドの 1 つで、内容の通り Pull request のマージを指示します:

Dependabot に自動的にマージの指示をしている様子

尚、この workflow_run による Workflow はデフォルトブランチ (main) のコンテキストで実行される為安全なので、 Secrets が有効かつ、 GITHUB_TOKEN も書き込み権限になっています。
但し、 Dependabot へのマージ指示コマンドは、コメント書き込み者が当該リポジトリに push する権限を持っていなければならず、渡される GITHUB_TOKEN はその権限を持たないので、 actions/github-script に渡す github-token には別途用意した書き込み権限を持つユーザーのアクセストークンを Secret 経由で渡す必要があります:

Dependabot への指示が失敗している様子

← 前の投稿

AnsibleのEC2インベントリ機能を使って自動でIPアドレスを割り当てる

次の投稿 →

Terraform で秘密情報を扱う

コメントを残す