Lento con forza

大学生気分のIT系エンジニアが色々書いてく何か。ブログ名決めました。

CI/CDサービスのOpenID Connect対応 Dive Into

これははてなエンジニアアドベントカレンダー2022 39日目の記事です。

昨日は id:nakaoka3ミーティングの時間になると勝手に議事録を開いてほしいでした

先日あった、CircleCIのインシデントのAdditional security recommendationsとして、OIDC Tokenを使うことが推奨されていました。GitHub ActionsやCircleCIなどのCI/CDサービスでは外部サービスへの認証を行うために、OpenID Connectに対応しています。OpenID Connect対応がされていることは知っていたのですが、OpenID Connectといえば、外部サービス連携をしてログインに使うイメージだと思います。たとえば、Googleの認証情報で、はてなアカウントにログインするなどといったようにです。僕の中で、ユーザー認証に使うOpenID ConnectとCI/CDサービスにおけるOpenID Connectの間で線が繋がらなかったので、少し調べてみました。

OpenID Connectの概要 外部サービスログインの場合

OpenID Connectでの登場人物を3人紹介します。まずは、IdP(Identity Provider)です。IdPは、認証情報を提供する側で、認証を行い、ID Tokenを発行します。Googleアカウントではてなにログインしたいケースにおける、Googleです。

次に紹介するのは、RP(Relying Party)です。RPは、信頼するIdPから発行されたID Tokenを検証します。ID Tokenが正しいものだと検証できた場合、ユーザーがIdPから正しく認証されていることになり、ユーザーをログインさせたりします。Googleアカウントではてなにログインしたいケースにおける、はてなです。

最後に紹介するのはユーザーです。これは、あなたのことです。IdPの認証情報を持っていて、RPにログインしたいと思っている、あなたです。ブラウザを操作して、RPのサイト上でIdPでログインするボタンを押し、リダイレクトされた後認証を行い、さらにリダイレクトされID TokenをRPに戻し、RPが検証した後RPにもログインできることになります。(実際のこのフローは自動的に行われているので、ユーザーが意識することはないはずです)

実際はもっと複雑なことをやっているのですが、今は流れだけを理解すれば先に進めるので、最低限の流れを絵にしてみました。

ID Tokenについての説明もしておきます。ID Tokenは署名付きのJWTで、認証するために必要なクレームをOpenID Connectがいくつか定義したものになっています。JWTの署名を検証することで、IdPが作成したものだということが証明でき、RPはその署名の検証をもってユーザーを認証することになります。

GitHub Actionsの場合

さて、GitHub Actionsの場合はどうなるのでしょう。例の如く、流れを絵にしてみました。

まず、GitHubがなんらかのトリガーにより、ActionsのJobを起動します(①)。 この時Jobには、ID Tokenを付与できます。これがGitHubのOpenID Connect対応の、一番肝になる部分です。

ここで発行されるID Tokenには、OpenID Connectで規定されているクレームの他に、リポジトリの情報などが追加のクレームとして付与されています。

ActionsのJobでは、AWSやGoogle Cloud Platformにデプロイしたいというユースケースが考えられます。そのような外部サービスを今回は、Cloud Providerと定義しました。Cloud ProviderはOpenID ConnectにおけるRPとしての振る舞いと、APIリクエストを処理するAPIサーバーとしての振る舞いの両方をすることになります*1 。Jobが外部に対してAPIリクエストなどを行う主体であるため、先述した流れにおける、ユーザーのような役割をします。

ID TokenでCloud Providerのアクセストークンを取得する

JobはデプロイをするためにCloud Providerにリクエストをしたいのですが、アクセストークンを持っていません。OpenID Connectを使わない場合だと、サービスアカウントを作ったり、事前に発行したAPIキーをSecretに保存しておいて使ったりすると思います。OpenID Connectの場合は、先ほど取得したID Tokenでアクセストークンを取得することになります。

まず、JobはCloud Providerのアクセストークンを取得しようとします。そのために、Cloud Providerに対してID Tokenを送信します(②)。RPはID Tokenを検証し、正しいものだった場合、一時的に使用できるアクセストークンを発行し、Jobに返します(③)。

ここまでくると、Secretに保存した場合のAPIキーと、何も変わりません。APIのリクエストに③で取得したアクセストークンを付与し、リソースに対する更新を行います(④, ⑤)

このようなフローで外部のクラウドサービスに対してAPIリクエストを行う際、以前のようなAPIキーをSecretに保存する必要はなくなりました。必要なのは、GitHubなどのサービスが発行するID Tokenだけで、これはJob起動時にGitHubが付与するものになります。一般的にSecretに保存しておくようなAPIキーは、有効期限が長く設定されていることが多いため、定期的に、もしくは漏洩時にローテーションが必要です。ID Tokenや、ID Tokenから取得したアクセストークンは、有効期限が短く設定されているため、仮に漏洩してもすぐ使えなくなってしまいます。

ID Tokenを元にアクセストークンを発行する仕組み

さて、先ほどは軽く流しましたが、②と③の、ID Tokenを元にアクセストークンを発行するとはどういうことなのでしょうか。簡単に言ってしまうと、Cloud ProviderがRPとして振る舞う、ということになります。Cloud ProviderがRPとして、GitHubなどのIdPが発行したID Tokenを検証します。ID Tokenの署名を検証するだけでアクセストークンの発行をしてしまうのは脆弱です*2。GitHubが発行するID Tokenには追加のクレームとして、リポジトリの情報を付与していると説明したことを覚えているでしょうか。AWSやGoogle Cloud Platformでは、これらの追加のクレームを検証してアクセストークンの発行可否を判断するサービスが用意されているので、それを使って解決します。このサービスにはアクセストークンに付与する権限も定義できるので、不必要な権限を持ったトークンを発行されてしまうことも防げます。

AWSにおけるIdentity providers and federation

AWSでは、以下のドキュメントで説明されています。

docs.aws.amazon.com

外部のIdentity Providerを登録する仕組みと、IAMロールに対するカスタム信頼ポリシーの組み合わせで実現します。詳細はドキュメントに譲りますが、以下のような流れです。

  1. CI/CDサービスをIdentity Providerとして登録する
  2. カスタム信頼ポリシーを持ったロールを作成する
  3. カスタム信頼ポリシーに、1で設定したIdentity Providerと、IDトークンの検証の条件を記述する
    • この条件を記述する際に、IDトークンの情報が使えるので、正しいリポジトリかどうか、Jobを発行したユーザーが正しいかどうか、などの条件をユースケースごとに記述できます
    • この条件が甘いと、不正利用ができてしまうので注意して記述する必要があります

Google Cloud PlatformにおけるWorkload Identity連携

Google Cloud Platformでは以下のドキュメントで説明されています。

cloud.google.com

Google Cloud Platformでは、Google Cloudリソースへのアクセス権が必要な環境ごとにIDプールを作ります。IDプールにはIdentity Providerと、サービスアカウントを紐づけられます。詳細はドキュメントに譲りますが、流れは以下の通りです。

  1. IDプールを作る
  2. CI/CDサービスをIdentity Providerとして追加する
  3. プロバイダの検証の条件を記述する
    • この条件を記述する際に、IDトークンの情報が使えるので、正しいリポジトリかどうか、Jobを発行したユーザーが正しいかどうか、などの条件をユースケースごとに記述できます
    • この条件が甘いと、不正利用ができてしまうので注意して記述する必要があります
  4. 発行できる権限を持ったサービスアカウントを作成してIDプールに紐づける

CI/CDサービスのOpenID Connect対応

ここまでで説明してきた通り、CI/CDサービスのOpenID Connect対応は、外部のOpenID ConnectをIdentity Providerとして使うことができるクラウドサービスとセットで使うことになります。

  • CI/CDサービスは、Jobに対してOpenID ConnectのID Tokenを付与する
  • クラウドサービスは、ID Tokenをもとに短命なアクセストークンを発行する

この組み合わせにより、SecretにAPIキーなどの秘匿情報を保存する必要がなくなるということですね。

AWSやGoogle Cloud Platformでは、外部のIdentity Providerを自由に定義できる仕組みが用意されているので、その仕組みで実現することになります。IDトークンが正しいものかどうか、はJWTの仕組みにより担保されていますが、IDトークンの内容が不正なものではないかを検証する責任はユーザーにあります。この検証を雑にやってしまうと脆弱性になりかねないので注意が必要ですね。

まとめ

外部サービス連携をする時はAPIキーなどを事前に共有しておくことが一般的だったと思います。OpenID Connect対応により、認証情報を事前に共有しておかなくても良くなるのはとても良いですね。APIの認証情報の共有は攻撃者にとって狙いやすい部分であるので、認証情報を共有しない、というのは良いアプローチに思えます。

今回の例ではGitHub Actionsを使って説明しましたが、CircleCIでも同じ仕組みが使えるようでした。積極的に使っていきたいですね!

はてなのアドベントカレンダー2022 はいつまで続くのでしょうか!?明日は id:nakiwo です!

*1:Cloud ProviderがIdPなのか、RPなのかはだいぶ悩んだのですが、整理していくとRPとして考える方が考えやすいのではないか、と判断したためRPにしています。GitHubとCloud Providerを合わせてIdPとしても考えられるとも思ったのですが、一般的なOpenID Connectのフローとは違うものになってしまうのが、こうした理由です。

*2:たとえば、適当なJobに対して発行されたID Tokenを転用することで、簡単にアクセストークンを取得できてしまいます。