🆔

GitHub ActionsとGoogle CloudのOIDCの仕組みを理解する

2023/10/30に公開

GitHub Actions から AWS や GCP などのクラウドリソースを操作するときは、 OIDC を使用することが主流だと思いますが、手順に沿って設定はできるもの仕組みがよく分かっていない方も多いと思います。

この問題は厄介で、様々な"分からない"が絡まりあって生まれている問題だと思います。

例えば、

  • どんな仕組み・流れでAWS・GCPを操作できるようになっているのか分からない(私)
  • そもそもなぜOIDCを設定すると嬉しいのか分からない(私)
  • 色々な設定をしたけど何をしているのか分からない(私)

などが挙げられると思います。
これらを解消し、OIDCを利用したGitHub ActionsとGCPの連携の流れ・仕組みを探求するのがこの記事の目的です。

※Google CloudのことはGCPと書きます。

※記事で触れないこと

GitHub Actions - Google Cloud間のOIDCによる認証の設定手順には触れません。こちらの記事で詳細な設定手順が書かれてありましたので、掲載させていただきます。
https://zenn.dev/kou_pg_0131/articles/gh-actions-oidc-gcp

OIDC連携の仕組みを色々な観点から考えてみる

今回のトピックを色々な観点から考えて、疑問を解消していきたいと思います。

a. そもそも何を実現・解決したかった?

OIDCを利用するよう設定していますが、そもそもなぜこのような設定しているのでしょうか?実現したかったこと・解決したかったことをベースに考えてみます。

実現したかったことは、もちろん「GitHub Actionsからクラウドリソースを操作すること」です。そのためにやるべきことは簡単です。

  • GCP: サービスアカウントキーを発行して使用すれば良い
  • AWS: アクセスキーとシークレットキーを発行して使用すれば良い

ただし、これらのキーは有効期限が無期限であるため、漏洩時のリスクが高くなる問題があります。もっと安心してGitHub Actionsからクラウドリソースを操作したい…。そこでこれを可能にするのが Workload Identity連携 の利用です。

このWorkload Identity連携を使うと、OIDCの仕組みを利用して先ほどの無期限キーを発行することなく、GCPリソースにアクセスできるようになります。この、安全な方法でGitHub Actionsからクラウドリソースを操作することが実現したいことです。

※Workload Identity連携とは?

https://cloud.google.com/iam/docs/workload-identity-federation?hl=ja

Workload Identity連携とは、文字通り外部ワークロードのIDをGCPと連携する機能です。つまり、外部のIDを使ってGCPを操作することを可能にします。上記のドキュメントにもありますが、外部のIDというのは、

  • AWS
  • OIDCをサポートするIdP
  • SAML2.0

などで発行されたIDを指します。今回の場合は、「GitHubという外部ワークロードで発行されたID」を連携し、GCPを操作することを可能にする流れになります。GitHubはOIDCをサポートするIdPです。


b. どんな仕組み・流れでAWS・GCPを操作できるようになっている?

Workload Identity連携が使われていることは分かりましたが、実際にはどのような流れでGCPの操作を可能にしているのでしょうか。それを簡単にまとめたのがこの図です。

「2」以降はGCPの話です。流れとしては以下になります。

  1. GitHub OIDCトークンを発行する
  2. 発行したOIDCトークンをもとにSTS APIを叩き、STSトークンを入手する
  3. STSトークンを使ってService Account Credentials APIを叩き、サービスアカウントのIDトークンを入手する
  4. 発行したサービスアカウントのIDトークンでリソースを操作する

これらの流れは、google-github-actions/auth アクション(GitHub ActionsからGCPを扱うときはだいたいこのアクションを使うはず。)の内部を読み進めていくことで分かります。それぞれを詳しく見ていきます。

1. GitHub OIDCトークンを発行する

まず、main()関数内の121行目の箇所で getIDToken()関数が呼ばれていることが分かります。

この関数は @actions/coreで定義されています。中を見ると、ACTIONS_ID_TOKEN_REQUEST_TOKENをBearerトークンとしてACTIONS_ID_TOKEN_REQUEST_URL にHTTPリクエストを投げていることが分かります。

この2つの環境変数の意味はこちらのドキュメントで定義されていますが、内容は

  • GitHub OIDCプロバイダーのURL
  • GitHub OIDCプロバイダーを利用するためのトークン

です。このプロバイダーにアクセスし、OIDCトークン(正体はJWT)を取得しています。もちろんこのトークンはshort-livedで安全です。

https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#updating-your-actions-for-oidc

ところで、OIDCトークンを取得しているのはわかったのですが、このトークンは結局何者なのか、何のためにあるのか、を再度確認しておきます。

http://openid-foundation-japan.github.io/openid-connect-core-1_0.ja.html#Authentication

こちらのドキュメントの3章を少しだけ見ると、この発行されたトークンはAuthentication結果を含んでいるということが分かります。つまり、OIDCトークンは何かを認証したものです。今回の場合は何を認証したのかと言うと、この今まさに実行中のGitHub Actionsワークフローは確かに存在することを証明(認証)したことになります。

2. クレデンシャルファイルの生成

続いて、main()関数の183行目でクレデンシャルファイルを生成しているのが分かります。実装はこちらで、色々書かれたjsonのファイルを作成しています。

ドキュメントを見ると、このクレデンシャルファイルは「認証情報構成ファイル」と呼ばれています。
https://cloud.google.com/iam/docs/using-workload-identity-federation?hl=ja#generate-automatic

ドキュメントにあるように、この認証情報構成ファイルがあればgcloudterraformなどのツールがそのファイルの設定を読み、自動的にサービスアカウントの権限を借用するようになるようです。以下のような情報が入っていることが分かります。

  • 外部認証情報の取得元
  • 使用する Workload Identity プールとプロバイダ
  • 権限を借用するサービス アカウント

3. 発行したOIDCトークンをもとにSTS APIを叩き、STSトークンを取得する

認証情報構成ファイルを作成した後はswitch文に入るのですが、google-github-actions/authをデフォルトの設定で使う場合はtokenFormatは空なので、何も起きず全体の処理はここで終了します。

switch (tokenFormat) {
  case '': {
    break;
  }

つまり、意外にもOIDCトークンを発行して認証情報構成ファイルを生成するまでがアクションの仕事内容であり、GCPと実際にやりとりしてサービスアカウントの権限を借用する流れはアクションの中で行いません(gcloudterraformなどの後のツールが認証情報構成ファイルを元にその流れを行います)。

とはいえ、そちらのツールの実装を見に行くのは骨が折れそうなのと、google-github-actions/authの中でもtokenFormatid_tokenの場合のswitch分岐の中にサービスアカウントの借用まで行うコードは書かれてあるので、このままコードを参考に解説していきます。

続きとして274行目を見ると、AuthTokenを取得していることが分かります。この関数の本体はこちらで、中を見るとどうやらhttps://sts.googleapis.com/v1/tokenにリクエストを投げているようです。

https://cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token

ドキュメントによると、それは Google Security Token Service のAPIであり、Exchanges a credential for a Google OAuth 2.0 access tokenと書かれていることから、先ほど発行したOIDCトークンをGoogle OAuth 2.0アクセストークンに交換していることが分かります。

交換するときにはGitHubのOIDCトークンの検証も行っています。検証の方法についてはここでは触れませんが、GCPは「もらったOIDCトークン(GitHubのIdP発行)が確かに本物だと検証すること」ができていて、安心してアクセストークン(=STSトークン)を渡しています。

検証方法はこちらの記事が大変参考になったので、掲載させていただきます。
https://zenn.dev/miyajan/articles/github-actions-support-openid-connect#クラウドプロバイダは-oidc-トークンをどうやって検証してるの?

とにかくこれでGCPを操作できそうなトークンを取得することができました。しかし、実はまだリソースを操作するための十分な権限を持っていません。トークン発行時scopehttps://www.googleapis.com/auth/cloud-platformとしているので、このスコープ範囲の操作までは境界として許可されているので、割となんでもできそうな気はしますが、実際のリソースへのアクセス許可はIAMポリシーで定義されるためです。

このWorkload Identityで管理されているトークンにアタッチされているIAMポリシーといえば、OIDCフローの設定時に付与したroles/iam.workloadIdentityUserロールの権限ですが、権限の内容を確認してみてもGitHub Actionsからやりたいことであるterraform planでほとんどのリソースを参照することや、CloudRunに何かをデプロイすることはできなさそうだと分かります。

https://www.googleapis.com/auth/cloud-platformスコープができることはおそらくこのドキュメント。
https://developers.google.com/identity/protocols/oauth2/scopes?hl=ja

4. STSトークンを使ってService Account Credentials APIを叩き、サービスアカウントのIDトークンを入手する

コードを読むと、続いてgoogleIDToken()という関数を呼び出していることがわかります。この関数の定義はこちらで、中を読むと先ほど発行したSTSのauthTokenを使ってhttps://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateIdTokenというエンドポイントにPOSTを投げていることが分かります。

https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken

ドキュメントにはGenerates an OpenID Connect ID token for a service accountと書かれてあることから、このAPIを使ってサービスアカウントのトークンを発行していることが分かります。なおこれもshort-livedです。

今回こそ対象のサービスアカウントのトークンを取得し、いわゆる権限借用を行うことができました。(ちなみに、このAPIを使ってサービスアカウントのトークンを取得することはroles/iam.workloadIdentityUserでちゃんと許可されています。)

これでようやくリソースの操作が行えるようになったわけです。

5. 発行したサービスアカウントのIDトークンでリソースを操作する

…特に説明はないです。

これでGitHub ActionsがOIDCを利用してGCPのサービスアカウントの権限を取得する大体の流れは押さえることができたと思います。


c. 色々な設定をしたけど何をしているのか

これまでの流れを押さえた上で、GitHub ActionsやGCP側に行った色々な設定の中身を見ていきます。

1. Workload Identityプールの作成

GCP側でWorkload Identityプールを作成しました。これは、各種プロバイダから発行される外部IDを管理する場所です。(※GitHubのIDトークンならhttps://token.actions.githubusercontent.comで発行される)

そのため、プールの作成と同時にプールにプロバイダ設定を追加する必要があったと思います。これにより、今回作成したWorkload Identityプールで管理するIDのプロバイダを指定・制限していたのです。

2. 属性マッピング

google.subject = assertion.subなどの属性マッピングの設定をしたと思います。これは、GitHubのIdPから発行されるトークンをSTS APIで交換するときに、STSトークンの形と合わない部分や足りない部分があるため、設定が必要だったのです。

以下のドキュメントを見ると、google.subjectがSTSトークンでは必須なことが分かります。残りのマッピングしたものはドキュメントは見つけられませんでしたが、必要な作業だったのだと思います。
https://cloud.google.com/iam/docs/workload-identity-federation?hl=ja#mapping

3. サービスアカウントの権限設定

サービスアカウントに対してリソースへの操作権限を持ったIAM Roleをアタッチするのはもちろんですが、その他にもWorkload Identity PoolのIAMプリンシパルに対して、roles/iam.workloadIdentityUser権限を付与し、サービスアカウントに対してのアクセス権限を付与したと思います。

これは、Workload Identityで管理されている外部IDに対してサービスアカウントのトークンを発行する権限を与える設定です。

まとめ

GitHub ActionsとGoogle Cloud間のOIDCを利用した連携の仕組みについて、コードを読みつつ解明していきました。

どんな仕組み・流れでAWS・GCPを操作できるようになっているのか分からない
→ 以下のような流れで操作できるようになっていたことが分かりました。

  1. GitHub OIDCトークンを発行する
  2. 発行したOIDCトークンをもとにSTS APIを叩き、STSトークンを入手する
  3. STSトークンを使ってService Account Credentials APIを叩き、サービスアカウントのIDトークンを入手する
  4. 発行したサービスアカウントのIDトークンでリソースを操作する

ただし、google-github-actions/authアクションがデフォルトで行っているのは「GitHub OIDCトークンの発行」と「認証情報構成ファイルの生成」まで、STS APIとのやり取りはその後の各種ツールが行っています。

そもそもなぜOIDCを設定すると嬉しいのか分からない
→ 主には、発行するトークンがshort-livedになったり検証が加わったりと、漏洩リスクが減ってセキュリティが向上することがざっくり分かりました。

今回の記事がOIDCの流れを理解する助けとなっていれば幸いです。

Discussion