この記事はMercari Advent Calendar 2024の1日目の記事です。
メルカリでは流出すると多大な影響を及ぼすような有効期限が長い認証情報を減らすために、有効期限が短い認証情報を発行する仕組みを様々な箇所で導入しています。そして、Platform Security Teamでは社内で運用されていたGitHubの認証情報を生成するToken Serverというサービスを拡張することで、Google Cloud上で動作しているGitHubの自動化サービスが保持していたGitHubへのアクセスに使用する認証情報を有効期限が短いものに切り替えました。
このToken Serverの拡張と移行で用いた技術や課題、解決策について紹介します。
概要
メルカリではGitHubを中心とした開発環境を利用しており、その中でGitHubの運用を自動化する様々なサービスを開発・運用しています。
これらのサービスはPAT(Personal Access Token)やGitHub Appの秘密鍵を使いGitHubへアクセスします。しかしこれらの認証情報は有効期限が設定されていない、もしくはとても長く設定されています。これらの認証情報がサプライチェーン攻撃などによって漏洩すると長期に渡り攻撃者から悪用されるリスクがあります。また、これらの認証情報は一度作成されてしまうと多くの場合、どの認証情報がどのサービスで使われているのか不透明になりがちで、かつ付与された権限が見直されることが少ないです。
これらの課題を解決するために、すでにメルカリ社内で運用されていた、有効期限が短いGitHub認証情報を発行するToken Serverというサービスを拡張し、Google Cloud上で動作しているサービスが有効期限が長い認証情報を使うことなくGitHubにアクセスできるようにしました。
これにより、以下のようなメリットを実現することができました。
- 有効期限が長い認証情報の削減
- 管理が不透明な部分が多かったPAT及びGitHub Appの削減
- 認証情報を付与する対象・必要な権限を1つにまとめて管理することで、認証情報の対象の特定と権限の定期的な見直しを簡略化
また、既存のPATや秘密鍵を用いるサービスの実装を大きく変えることなく、Token Serverに移行できるようなGo言語のライブラリも合わせて開発することで移行をすぐに行うことができました。
Token Server
メルカリでは様々な形態でGitHubを運用しており、特にGitHub内での自動化においてはあるレポジトリの変更を別のレポジトリに同時に適用するといった複数レポジトリをまたぐ自動化が頻繁に行われています。
特に社内で標準的に使われているCIであるGitHub Actionsにおいて、複数レポジトリをまたぐ自動化を行うための認証情報はデフォルトでは提供されておらず、一般的にはRepository SecretにPATを保存するか、GitHub Appの秘密鍵を保存しcreate-github-app-token actionなどで生成する必要があります。しかし、これらはPATおよびGitHub Appの秘密鍵という有効期限の長い認証情報が必要です。
そこで以前からメルカリは、GitHub Actionsのワークフロー内部で取得できる、GitHubが署名し偽造が困難なOIDC Tokenを受信し検証することで、特定の権限を持つInstallation Access Tokenを提供するToken Serverサービスを運用しています。
Installation Access TokenはGitHub Appの機能で、GitHub Appで事前に設定されている権限(contentsのread権限・Pull Requestのwrite権限など)のサブセットを特定のレポジトリに限定し発行できるトークンです。またInstallation Access Tokenには1時間の有効期限があり、さらにGitHub API経由で有効期限を待たずに失効させることも可能です。これにより最小権限の原則に従った、限定された権限・アクセス範囲・有効期限を持つ認証情報を提供することができます。
Token Serverは事前に設定されたGitHub Appから対象のレポジトリとブランチごとに設定された権限を持つInstallation Access Tokenを作成し各レポジトリ上でのGitHub ActionsのJobに提供します。この際、レポジトリ名及びブランチ名を特定するため、GitHub ActionsのJob上で取得できるOIDC Tokenを利用します。Job上でOIDC Tokenを取得、Token Serverへ送信、Token Server内でOIDC Tokenの検証、レポジトリとブランチごとに設定された権限を検索、Installation Access Tokenを作成・発行を行います。
Token Serverから発行されるInstallation Access Tokenは複数レポジトリ間のGitHub運用の自動化(コミットの追加・Issueの自動作成・Pull Requestの自動作成など)だけでなく、CIでのビルドにおける内部のライブラリのダウンロードなどにも広く使われています。
(注) 2024年4月にChainguard社からOcto STSがリリースされました。基本的な動作原理はToken Serverと同じですが、Token Serverではより統一された権限管理をサポートしている他、後述するGoogle Cloud上へのワークロードへの組み込み・GitHub AppのLoad Balancingなどよりエンタープライズ環境に適したアーキテクチャが採用されています。
Token ServerのGoogle Cloudへの拡張
メルカリでは多くのサービスをGoogle Cloudで運用しています。これはお客さまへのサービス提供を目的としているマイクロサービス郡だけではなく、内部向けの自動化サービスも同様です。これらのサービスはGitHubにアクセスするためにPATやGitHub Appの秘密鍵を使用していました。
Google Cloudの各リソースには他のリソースを操作する権限を付与するためのService Accountが付与されており、更にroles/iam.serviceAccountTokenCreatorが付与されている場合、Service Accountが付与されているGoogle CloudリソースはAPIを通してGoogleが署名したOIDC Tokenを取得することができます。このOIDC TokenをGitHubの場合と同じようにToken Serverで検証し、事前に設定された権限を持つInstallation Access Tokenを発行するよう、Token Serverを拡張することにしました。
これにより、ある特定のGoogle Cloudリソース上で動作しているサービスが、Token ServerへOIDC Tokenを送信し、Installation Access Tokenを発行してもらうことで、GitHubへのアクセスができるようになり、今までGoogle Cloud上で使用していたPATやGitHub Appの秘密鍵をなくすことができます。
Google Cloud上のワークロードへのToken Serverの適用
Token Serverの拡張により、Google Cloud上で動作しているサービスがGitHubへのアクセスに使用する認証情報を有効期限が短いものに切り替えることができるようになりました。
これらの新規機能をGoogle Cloud上に新しく作成するサービスに適用することは比較的容易です。しかし今までGitHub PATやGitHub Appの秘密鍵を使っていたサービスは多くの場合、機能が作り込まれており、Token ServerへInstallation Access Tokenをリクエストし、取得したInstallation Access Tokenを使うように変更することが難しい場合があります。
そして、GitHub AppにはAPIの利用数の制限があります。これはGitHub Appごとに設定されており、GitHub Enterprise Cloudを利用している場合、1時間あたり15000回のAPIリクエストが可能です。この制限を超えるとAPIリクエストが失敗するため、APIリクエストの制限を超えないように、Token Serverに対してのリクエストをなるべく減らす必要があります。
このAPIリクエストはInstallation Access Tokenの発行回数のみではなく、発行されたInstallation Access TokenからのAPIリクエストも含まれます。特にToken Serverにおいては複数のGoogle Cloud上のワークロード、GitHubレポジトリの分のAPIリクエストを発行するため、Token Serverに対してのリクエストをなるべく減らすことが重要です。GitHub APIのリクエストごとにInstallation Access Tokenを発行するのではなく、1時間の有効期限を持つInstallation Access Tokenを発行し、それを有効期限内に使い回すことでAPIリクエストを減らすことができます。
そこで、PATやGitHub Appの秘密鍵を使っていたサービスの実装を大きく変えず、また自動でInstallation Access Tokenを取得し有効期限内で再使用するライブラリを開発しました。メルカリではGo言語を標準的に使用していることから、Go言語でGitHub関連のサービスを作成する代表的なライブラリであるgoogle/go-githubをベースにGitHub APIを使うためのクライアントを取得できるようにしました。なお、既にgo-githubライブラリを使っているサービスの場合、Service Accountの設定とライブラリの入れ替えのみでToken Serverに移行できるようにしました。
Token Server用ライブラリの構成
go-githubライブラリは初期化の際、任意のhttp Clientを指定することができます。このhttp Clientには、RoundTripperインターフェイスに任意のRoundTripメソッドを実装することでリクエスト送信前にリクエストの内容を変更することができます。そこでこのRoundTripメソッドを使い、リクエストを送信する前にキャッシュされているInstallation Access Tokenもしくは有効期限が切れている場合はToken Serverから新たなInstallation Access Tokenを取得し使用するようにしました。
これにより、go-githubライブラリを使用している既存のサービスのコードを1行変更するだけで、Token Serverに移行することができました。
GitHub AppのLoad Balancing
前述の通り、GitHub Appには1時間あたり15000回のAPIリクエスト制限があります。しかし、Token Serverは複数のGoogle Cloud上のワークロード、GitHubレポジトリのAPIリクエストを発行し、また今後の自動化サービスの増加を想定するとこの制限を超えることが予想されます。そこで、GitHub Appを複数作成し、それぞれのGitHub Appに対してInstallation Access Tokenの発行を分散することで、APIリクエストの制限を超えることなく、多くのAPI処理を提供することができます。
しかし、この分散は複数のGitHub AppをそれぞれのToken ServerのPodに一つずつ読み込んでLoad Balancerによってランダムにリクエストの分散を行い、あるInstallation Access Tokenの利用者が受け取るInstallation Access Tokenの発行元が複数のGitHub Appとなってしまうと、コミットステータスの書き込みを行うサービスにおいて、問題が発生します。
GitHubにおいてはCIの動作結果等をあるコミットに対しerror, failure, pending, successのいずれかを書き込むことができます。しかし、この書き込みはそれぞれ書き込んだGitHub Appごとに記録されます。つまり異なるGitHub Appによってコミットステータスが書き込まれると、コミットステータスが混在してしまいます。この場合、以前失敗したステータスが上書きされることなく、別の新しいステータスのみが書き込まれ、コミットステータスがすべて成功しないとマージできないようなBranch Protectionを設定している場合、マージができなくなるという問題があります。
ある自動化処理において最初は失敗ステータスを書き込み、その後成功ステータスを書き込むような処理を行う場合、同じGitHub Appによって行われる必要があります。もしToken Serverが複数のGitHub Appを持つ場合、最初の失敗ステータスをGitHub App 1で書き込んでしまうと、その後の成功ステータスをGitHub App 2で上書きすることができず、GitHub App 1からの失敗ステータスとGitHub App 2からの成功ステータスが混在してしまいます。
そこで、Installation Access Tokenを発行する対象ごとに常に同じGitHub Appを使うようにするために、1つのToken ServerのPodで複数のGitHub Appを読み込み、GitHubにおいてはリポジトリとブランチ名・Google CloudにおいてはサービスアカウントによってGitHub Appを割り当てるようにしました。
このように、GitHub Appのインデックスをリポジトリとブランチ名、サービスアカウントによって割り当てることで、同じリポジトリとブランチ名、サービスアカウントに同じGitHub Appが割り当てられるようにしました。
まとめ
Token ServerをGoogle Cloudに拡張し、より多くのサービスがGitHubにアクセスする際に有効期限の短い認証情報を使うようにすることで、有効期限が長い認証情報を減らすことができました。そして既存のサービスを最小限の変更でToken Serverに移行できるようなライブラリを開発することで、移行を容易にしました。また、実運用の中で発見した問題も解決し、セキュアかつ快適なGitHubの自動化サービスの運用をサポートしました。
メルカリのSecurity Teamでは、このような認証情報の有効期限の短いものへの切り替えを推進しており、今後もこのような取り組みを進めていく予定です。
Security Teamにおける採用情報についてはMercari Careerをご覧ください。