社内用GitHub Actionsのセキュリティガイドラインを公開します

この記事は、Merpay Tech Openness Month 2023 の4日目の記事です。

こんにちは。メルコインのバックエンドエンジニアの@goroです。

はじめに

このGitHub Actionsのセキュリティガイドラインは、社内でGithub Actionsの利用に先駆け、社内有志によって検討されました。「GitHub Actionsを使うにあたりどういった点に留意すれば最低限の安全性を確保できるか学習してもらいたい」「定期的に本ドキュメントを見返してもらい自分たちのリポジトリーが安全な状態になっているか点検する際に役立ててもらいたい」という思いに基づいて作成されています。

今回はそんなガイドラインの一部を、社外の方々にも役立つと思い公開することにしました。

ガイドラインにおける目標

このガイドラインは事前に2段階の目標を設定して作成されています。まず第1に「常に達成したいこと」として「外部の攻撃者からの攻撃を防ぐ」こと。そして、第2に「可能であれば考慮したいこと」として「内部と同等の権限を持つ攻撃者からの攻撃を防ぐ」ことを目標としています。

ガイドラインの構成

このガイドラインは3部で構成されています。まず1部でGitHub Actionsにおいて起こりうる脅威を紹介しています。2部ではその脅威に対する対策を記載しています。そして最後の3部ではより実践的な対策を講じられるようにセルフチェックリストを用意しました。

それでは実際のガイドラインをお楽しみください。

GitHub Actions Guideline

脅威を知る

権限設定の不備を突く攻撃

Pull Requestを契機に起動するトリガー

トリガーの基本的な仕組みについては参考情報の「ワークフローのトリガー」のセクションに記載した。

PRを契機に起動するトリガーは攻撃者がなにかを仕掛ける余地が大きい。不注意にワークフローを構築するとシークレットを外部に送信されて攻撃を受ける可能性がある。

  • シークレットなどを外部に送信される可能性
    • ビルドスクリプトに細工をする
    • 依存関係にあるライブラリを悪意のあるものに差し替えられる
    • 自動実行の仕組みに相乗りされる(npmのpreinstall, postinstallなど)

本ドキュメントにおけるシークレットという用語は、GitHub Organization、リポジトリ、またはリポジトリ環境で作成する暗号化された環境変数を意味する。詳しくはGitHubの「Encrypted secrets」を参照。

上記の攻撃の結果、次のような被害が発生する可能性がある。

  • 攻撃者に、悪意のあるアクションまたは侵害されたアクションによってGitHub Actionsの計算リソースを不正に利用される可能性がある
  • 侵害された、または悪意のあるアクションによって、リポジトリの自動ワークフローが中断される可能性がある
  • Deployment Keyやアクセストークンなどのシークレットへの読み取りアクセスは、攻撃者が他のリソースを侵害するために利用される可能性がある

インジェクションによる攻撃

一見安全に見えるワークフローにおいてもコードやコマンドインジェクションを引き起こす可能性がある。

インジェクションによる攻撃例1

例えば、以下のようなコードにはインジェクションの脆弱性がある。

uses: foo/bar@2.0.1
with:
    comment: |
    Comment created by {{ event.comment.user.login }}
    {{ event.comment.body }}

コメントに {{ 1 + 1 }} のような二重中括弧が含まれていた場合、Actionは内部で{{ }}の値を補間するためにlodashを使っているため、node.jsコードが実行され出力が2になる。

ワークフローのインラインスクリプトに直接インジェクションを配置するシナリオもある。また、ブランチ名やメールアドレスへのコマンドインジェクションもできる。

インジェクションによる攻撃例2

次のようなコードを例にする。

- name: Check PR title
  run: |
    title="${{ github.event.pull_request.title }}"
    if [[ $title =~ ^octocat ]]; then
      echo "PR title starts with 'octocat'"
      exit 0
    else
      echo "PR title did not start with 'octocat'"
      exit 1
    fi

内部の式 ${{ }} が評価され、結果の値に置き換えられるため、コマンドインジェクションに対して脆弱になる可能性がある。

攻撃者は a"; ls $GITHUB_WORKSPACE" といったタイトルのPRを作成する可能性がある

出典: Security hardening for GitHub Action

この例では " を使用して title="${{ github.event.pull_request.title }}" ステートメントを中断し、ランナーでコマンドを実行できるようにする。lsコマンドの出力を確認できる。

> Run title="a"; ls $GITHUB_WORKSPACE""
README.md
code.yml
example.js

インジェクションによる攻撃の影響

インジェクションをされると攻撃者は任意のコマンドを実行できるため、単純に攻撃者が管理する外部のサーバーにシークレットを送信するHTTPリクエストを行うことが可能になる

リポジトリへのアクセストークンを取得してもワークフローが完了すると失効するので攻撃は簡単ではない。しかし、攻撃者が自動化し、管理するサーバーにトークンを呼び出して、コンマ数秒で攻撃を実行することは可能となる。その場合GitHub APIを利用してリリースを含むリポジトリのコンテンツを変更することが可能になる。

攻撃者は悪意のあるコンテンツをGitHub Context経由で追加できる

  • 潜在的に信頼できない入力として扱う必要がある
  • これらのコンテキストは以下の文字列をinjectすることができる
    • body, default_branch, email, head_ref, label, message, name, page_name,ref, title
    • Ex: github.event.issue.title, github.event.pull_request.body

たとえば zzz";echo${IFS}"hello";# は有効なブランチ名であり、ターゲットリポジトリの攻撃となる可能性がある。

対策を考える

最小権限の原則に従う

最小権限の原則(Wikipedia: 最小権限の原則)は、ソフトウェアがタスクを達成するために必要な最小限の権限セットで実行されるべきであるというものになる。これは、ワークフローで利用可能な シークレット の権限と、ワークフロートリガーの種類に基づいて自動的に提供される一時的なリポジトリトークンの両方に当てはまる。

自動的に提供されるリポジトリトークンGITHUB_TOKENの権限は、フォークからのpull_requestイベントの場合には制限されている。GitHub の推奨するセキュリティ対策としては、ワークフローでは必要としないGITHUB_TOKEN の権限をすべて削減することとなっている。したがって組織やリポジトリのデフォルト設定を「読み取りと書き込み」権限から「読み取り専用」に変更すべきである。

設定はGitHubの対象リポジトリの Settings > Actions > General から変更できる。

出典: GitHub

必要であれば、特定のワークフローに対して個別に追加権限を付与することができる。権限はワークフロー単位でも設定できるが、Job単位で設定を行うことで権限を最小化できるケースが大半である。参考情報のGITHUB_TOKENの権限に権限の一覧と、Job単位での設定方法へのリンクを記載した。

jobs:
  job_name:
  ...
    permissions:
      issues: write

クロスリポジトリアクセスを考慮した、ワークフローが利用するべき推奨されるアプローチを優先度の高い順に説明する。

  • GITHUB_TOKEN
    • 可能な限りGITHUB_TOKENを利用する
  • Repository deploy key
  • GitHub App tokens
    • GitHub Appは、選択したリポジトリにインストールでき、リポジトリ内のリソースに対するきめ細かい権限がある
  • Personal access tokens
  • SSH keys on a personal account
    • 絶対に使わないこと

シークレットの利用について

シークレットを利用する場合は以下を考慮すること。シークレットの利用を避けられるのであれば、利用しない。

  • Long-Lived tokenを利用しない
  • Workload identity federationを用いたSecret Managerの利用を検討する
  • 構造化データ(JSON, XML, YAMLなど)をシークレットにしない
    • GitHub Actionsは全文をマスクデータとして扱ってくれるが部分文字列はマスクされないため
    • 構造化データ(JSON, XML, YAMLなど)のblobを使用してシークレットを登録しない
    • ひとつずつ個別にシークレットにする
  • ワークフロー内で使用されるすべてのシークレットをマスクするよう登録する
    • シークレットを使用してワークフロー内で別の機密値を生成する場合、その生成された値もシークレットとして登録する
    • たとえば、秘密鍵を使用して署名付きJWTを生成してWeb APIにアクセスする場合は、必ずそのJWTもシークレットとして登録する
  • シークレットに保存されたアクセストークンの利用状況を監査する
  • スコープが最小限のクレデンシャルを使用する
  • 登録されたシークレットを監査およびローテーションする
  • シークレットへのアクセスについてレビューを要求することを検討

イベントトリガー

  • PRの処理には pull_request イベントを使えるなら使う
    • リポジトリへのwriteはできないよう制限されている
    • Dependabotなどもシークレットにアクセスできない(社のorganizationの別リポジトリにアクセスできない)ためビルドできない可能性がある
  • すこし制限を緩めたものとしてpull_request_targetがある

    • GitHub Actionsのワークフロー自体は pull_request_target だと default branch のものが使われる

      • ワークフローのyamlに直接記載する場合は攻撃者によって上書きされない
      • チェックアウトしたコードに含まれるComposite Actionを使う場合注意が必要となる
        • Composite Actionについては参考情報に詳しく記載した
    • pull_request_target – Events that trigger workflows – GitHub Docsに記載されている以下の内容に注意すること

      警告: pull_request_target イベントによってトリガーされるワークフローでは、permissions キーが指定され、ワークフローがフォークからトリガーされてもシークレットにアクセスできる場合を除き、読み取り/書き込みリポジトリのアクセス許可が GITHUB_TOKEN に付与されます。 ワークフローはPull Requestのベースのコンテキストで実行されますが、このイベントでPull Requestから信頼できないコードをチェックアウトしたり、ビルドしたり、実行したりしないようにしなければなりません。 さらに、キャッシュではベース ブランチと同じスコープを共有します。 キャッシュ ポイズニングを防ぐために、キャッシュの内容が変更された可能性がある場合は、キャッシュを保存しないでください。 詳細については、GitHub Security Lab の Web サイトの GitHub Actions およびワークフローのセキュリティ保護の維持: pwn 要求の阻止に関するページを参照してください。

  • 信用できないPRが作成されることを想定する場合は pull_request を使うべき
    • ただし信用できないPRが作成される時点で、大きな問題となるため、これを防ぐべきである

Job / Stepの単位

  • シークレットの内容を露出する単位は可能な限り狭くする
    • Job単位 より Step単位 のほうがよりよい
  • Step間のファイルによるデータやりとりは全ステップから可視であると考える
  • Jobは処理の単位によって分ける
    • テスト & ビルド & デプロイ はそれぞれJobを分けたほうがよい
    • 必要なGitHub Actions上のPermissionやクラウドプロバイダー の権限を細かく制御するため
      • たとえばテストの時にデプロイできる権限は必要ない
        • 上記の場合、id-token: write は不要なはずである
        • id-token: writeについては参考情報に詳しく記載

Dependabot / Renovateを利用したGitHub Actionsの更新

Actionsはバグ修正や新機能によって頻繁に更新される。Dependabot、RenovateでGitHub Actionsの依存関係を最新の状態に保つことができるため、設定を行うこと。

サードパーティのActionを利用する場合の対応

サードパーティのActionを利用する場合、基本的にFull Changeset Hashに固定する。以下のようにFull Changeset Hashとバージョンコメントを記載することで、どのバージョンを使っているのかわかりやすくなる。

それぞれの指定の違いは以下の通り。

  • Full Changeset Hash
    • uses: owner/action-name@26968a09c0ea4f3e233fdddbafd1166051a095f6 # v1.0.0
    • 衝突の成功例はあるが困難
  • Short Changeset Hash
    • uses: owner/action-name@26968a0
    • 衝突に対して脆弱
  • Tag / Release
    • uses: owner/action-name@v1
    • タグを後で変更され、意図しない変更が混入してしまう可能性がある
  • Branch Name
    • uses: owner/action-name@main
    • 将来壊れる可能性がある
    • 意図しない変更が混入してしまう可能性がある

Actionのソースコードを監査する

  • サードパーティのホストにシークレットを送信するなどの疑わしいことがないか確認する

Managing GitHub Actions settings for a repositoryを参考に、ワークフロー内で利用している3rd Party ActionsのAction permissionsをセキュリティ観点で見直す。可能であれば、Allow enterprise, and select non-enterprise, actions and and reusable workflowsを設定する。

出典: GitHub

不要なワークフローやJobは削除する

  • 設定はしてあるが必要なくなったものは削除して依存を減らす

インジェクションを防ぐ

  • 信頼されない式の入力値を中間環境変数(intermediate environment variable)に設定する。
    • これによって${{ github.event.issue.title }}式の値はスクリプトの生成に影響するのではなく、メモリに保存されて変数として使用される
- name: print title
  env:
    TITLE: ${{ github.event.issue.title }}
  run: echo "$TITLE"
  • シェル変数をダブルクォートして単語の分割を避ける(シェルスクリプトを書く際の一般的な推奨事項)
  • GitHub Security Labの開発するCodeQL queriesを利用する
    • script_injections.qlは、記事で紹介されている式注入をカバーしており、精度が高い。しかしワークフローのステップ間のデータフローを追跡することはできない
    • pull_request_target.qlの結果は、pull requestからのコードが実際に安全でない方法で処理されているかどうかを特定するために、より多くの手動レビューが必要。
  • GitHub のカスタムアクションやワークフローを書くときは、信頼できない入力に対して書き込み権限でコードを実行することがよくあることを考慮する
actionlintによるインジェクションの検知

外部Actionとなるが、actionlintを利用することでインジェクション対策ができるので、導入を検討する。

また、reviewdog/action-actionlint を利用するとGitHub Actionsでactionlintを実行することも可能。

name: Actionlint
on:
  - pull_request_target
jobs:
  actionlint:
    runs-on: ubuntu-latest
    permissions:
      checks: "write"
      contents: "read"
      pull-requests: "write"
    steps:
      - uses: actions/checkout@v3.1.0
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - uses: reviewdog/action-actionlint@1fa528d6a483f3df85059e206eadea033044edd7
        with:
          fail_on_error: true
          filter_mode: nofilter
          level: error
          reporter: github-pr-review

その他

セルフチェックリスト

本章の内容を定期的にチェックすることでGitHub Actionsの安全な利用につなげる。ガイドラインで学習した内容が本チェックリストでカバーされることを目指す。

CODEOWNERSの設定を見直す

  1. CODEOWNERSファイルで .githubディレクトリ以下に対して適切にCode Ownerが設定されていることを確認する
  2. Protected BranchでDefault BranchへのPull Requestのマージには、Code Ownerによる承認が必須になっていることを確認する

GITHUB_TOKENのPermissionsを見直す

GITHUB_TOKENに付与される権限を見直す。

  1. デフォルトで付与されるGITHUB_TOKENの権限がReadのみになっているか確認する
    1. 「Read and write permissions」になっている場合は「Read repository contents permission」に変更する \
出典: GitHub

Managing GitHub Actions settings for a repository

  1. 可能であれば「Allow GitHub Actions to create and approve pull requests」を無効にする

    1. 設定方法などは以下を参照
      Disabling or limiting GitHub Actions for your organization
  2. permissions をJob単位で設定する

    1. permissionsはWorkflow全体かあるいはJob単位で設定できるが最小権限にするためにJob単位で設定する
      Workflow syntax for GitHub Actions – permissions
  3. permissionsの見直し

    1. 以下のリストを元に権限が最小になっているかを確認する
      1. Automatic token authentication – permissions-for-the-github_token
  4. ビルドやテストなどのジョブを分けることで、強い権限で実行されるステップが少なくなるのであれば分割を検討する

GitHub Actions Secretsを見直す

GitHub Actions用に設定されているシークレットを見直す。

  1. 使っていないシークレットはGitHub上から削除する
    1. シークレットの発行元でも無効化しておく
  2. 構造化データ(JSON, XML, YAMLなど)をシークレットに設定していないか確認する
    1. 個別登録するなどして設定し直す
  3. ワークフロー内で使用されるすべてのシークレットやログ出力すべきではない値をマスクするよう登録する
    1. シークレットを使用してワークフロー内で別の機密値を生成する場合、その生成された値もシークレットとして登録する
  4. 定期的(1年に1回など)にシークレットをローテーションする
    1. 新しいシークレットに置換し、それを終えたら古いシークレットは無効化する
    2. シークレットにTTL(Time To Live)を設定できる場合は適切な長さのTTLを設定する
  5. ローテーションと併せてシークレットに設定されている権限が最小限になっているのか確認する
    1. 必要以上に強い権限が付与されている場合は不要な権限を落とす

ワークフロートリガーを見直す

  1. コードプッシュをトリガとする場合、pull_request か、それが難しければ pull_request_target を使う
    1. on: pushをPR用に使っていたら見直す

サードパーティのActionsを見直す

  1. 不要なWorkflowやJobは削除する
    1. 設定はしてあるが必要なくなったものは削除して依存を減らす
  2. バージョン指定を確認する
    1. 基本的にFull changeset hashに固定し、Full Changeset Hashとバージョンコメントを記載する

Dependabot

- uses: actions/checkout@01aecc # v2.1.0

Dependabot now updates comments in GitHub Actions workflows referencing action versions | GitHub Changelog

Renovate

- uses: actions/checkout@af513c7a016048ae468971c52ed77d9562c7c819 # renovate: tag=v1.0.0

Automated Dependency Updates for Github Actions – Renovate Docs | Renovate Docs

  1. Managing GitHub Actions settings for a repositoryを参考に、ワークフロー内で利用している3rd Party ActionsのAction permissionsをセキュリティ観点で見直す。

インジェクション対策を見直す

  1. actionlintが導入済みであれば、actionlintで問題がないことを確認する
  2. actionlintを導入できない場合、最低限の対応として信頼されない式の入力値を中間環境変数(intermediate environment variable)に設定する

おわりに

今回は社内の有志メンバーによって作成された社内用GitHub Actionsのセキュリティガイドラインの一部を紹介しました。

GitHub Actionsは、開発者がよりスムーズで効率的な開発を行うための強力なツールであると言えますが、使用する際にはガイドラインに記載したようなさまざまな観点でセキュリティに十分注意する必要があります。常にセキュリティを考慮し、最適なプラクティスを意識して実践することの重要性をガイドラインを作成する中で強く感じました。GitHub Actionsにおけるセキュリティのベストプラクティスは今後も変化していくと思います。本ガイドラインはこれで完成ではなく、今後も適切に更新していき、よりスムーズで安全な開発をサポートできるよう努めていきたいと思います。

明日の記事は sapuriさんです。引き続きお楽しみください。

Appendix

参考情報

ワークフローのトリガー

ワークフローはイベントによってトリガーされる。イベントには、以下のものがある。

  • ワークフローのリポジトリで発生したイベント
  • GitHubの外部で発生し、GitHub上で repository_dispatch イベントを発生させるイベント
  • 時間指定での実行
  • 手動実行

たとえば、リポジトリのデフォルトブランチにプッシュが行われたときやリリースが作成されたとき、あるいはIssueがオープンされたときなどにワークフローを実行するように設定することができる。

詳しくは About workflows を参照すること。またイベントの一覧は Events that trigger workflows を参照すること。

GITHUB_TOKENの権限

GITHUB_TOKENの権限は以下にまとめている。

Jobごとに権限を変更する方法は以下に記載されている。

Composite Action

Composite ActionはカスタムActionの一つであり、使用することでワークフローの複数のステップを組み合わせて 1 つのアクションにすることができる。 たとえば、複数の run コマンドを 1 つのアクションにまとめて、そのアクションを 1 つのステップとしてワークフローから呼び出して実行することが可能。

作成方法に関しては以下に記載されている

シークレットのマスク

以下のような記述を行うことで値をマスキングすることが可能。マスキングされた単語は「*」 に置き換えられ、ログに出力されなくなる。マスク可能な値は環境変数または文字列である。

::add-mask::{value}

例:Stringをマスクする

以下のような設定を行った上でログに「Mona The Octocat」を出力すると「***」が表示される。

echo "::add-mask::Mona The Octocat"

例:環境変数をマスクする

以下のような設定を行った上でログに環境変数 MY_NAMEと”Mona The Octocat"を出力すると *** が表示される。

jobs:
  bash-example:
    runs-on: ubuntu-latest
    env:
      MY_NAME: "Masking on GitHub Action"
    steps:
      - name: bash-version
        run: echo "::add-mask::$MY_NAME"
      - run: run: |
          echo "Mona The Octocat"
          echo "::add-mask::Mona The Octocat"
          echo "Mona The Octocat"
          echo "$TITLE"
          echo "::add-mask::$TITLE"
          echo "$TITLE"

以下のように表示される。

Mona The Octocat
***
Masking on GitHub Action
***
actions/toolkitを利用する場合

actions/toolkitはGithub Actionsの作成を容易にする一連のパッケージを提供している。

toolkitの@actions/coreパッケージを利用することで、以下のような記述でシークレットのマスクを設定することも可能。

core.setSecret('Mona The Octocat')
id-token:writeで実現できること

id-token: write はGitHubによる署名が行われたOpenID ConnectのID Tokenが取得できるようになる権限。これを使うとどういうことができるかは公式ドキュメントを参照する。

例えばGCPのWorkload identity federationの機能を通じて、GitHub ActionsのID Tokenがあれば設定されたService AccountのAccess Tokenを手に入れることができる。つまり、id-token: write をGitHub Actions中で利用するということは短時間(デフォルトでは1時間)ながら、GCPプロジェクトへのアクセス権限を渡すのと同義となる。secretsに固定のcredentialをもたせるのに比べれば圧倒的にセキュリティが高いが、それでもID Tokenにアクセス可能な範囲を適切にコントロールすることは重要となる。

References

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加