※本記事は2022年1月22日に公開された記事の翻訳版です。
この記事は、Developer Productivity Engineering Campブログシリーズの一環として、Platform Infraチームの Daisuke Fujita (@dtan4)がお届けします。
メルカリでは、すべてのクラウドインフラを宣言的構成で管理することがプラットフォームの中核となる考え方の一つです。メインのクラウドプロバイダーはGoogle Cloud Platform(GCP)であり、HashiCorp Terraformを使用してインフラをコードとして管理しています。Platform Infraチームは、すべてのTerraformワークフローを安全に管理するための社内CIサービスを提供しています。
Terraformはリソースプロビジョニングのためにクラウドプロバイダーのクレデンシャルを必要としています。システムをシンプルに保つために、これらのクレデンシャルを環境変数として保存し始めましたが、Terraformの使用量が増え始めると、これらのクレデンシャルの影響範囲も広がりました。
この記事では、Terraform環境で直面したセキュリティの問題と、状況をどのように改善したかについて説明します。
背景
Terraformモノレポ
すべてのインフラリソースを一元管理するために初期段階でおこなった決定事項の1つは、社内すべてのTerraform構成を一元化されたモノレポに保存することでした。以下、このリポジトリに関する重要なポイントをいくつか挙げています:
- メルカリグループ内のすべてのカンパニー(メルカリJP、メルカリUS、メルペイ、ソウゾウなど)で使用されている
- すべてのカンパニーの500以上のサービスと1,000以上のTerraform State (tfstate)を管理している
- 各サービスには専用のフォルダと、実際にサービス開発および管理をするサービス所有者が存在し、CODEOWNERであるサービス所有者のみがフォルダの変更を承認できる
次の図は、このTerraformモノレポのディレクトリ構造を示しています。各サービスには独自のディレクトリがあり、環境のディレクトリもあります。環境ディレクトリには、各サービスのGCP ProjectのTerraform構成が含まれています。
├── script
├── terraform
│ ├── microservices
│ │ ├── <SERVICE_ID>
│ │ │ ├── development # 「<SERVICE_ID>-dev」でGCP Projectのリソースを管理
│ │ │ └── production # 「<SERVICE_ID>-prod」でGCP Projectのリソースを管理
最初のCI実装はCircleCIに基づいていました。新しいコミットがfeatureブランチにプッシュされると、CircleCIはterraform plan
を実行し、結果をプルリクエストコメントで公開します。CIスクリプトは、変更(作成/更新/削除)されたファイルのフォルダ(サービス)に対してのみTerraformが実行するようになっています。CODEOWNERSによりコード変更が承認されると、プルリクエストをマージできます。CircleCIはマージ時にterraform apply
を実行します。
クラウドプロバイダーにアクセスするために、GCPサービスアカウントキーやSaaS APIキーなどのプロバイダーのクレデンシャルはCircleCIプロジェクトの環境変数に保存していました。
課題
このシステムを何年も使用していたものの、セキュリティの観点からはいくつか問題がありました。
永続的なサービスアカウントキー
TerraformはGCPリソースを管理するためにGCPクレデンシャルを必要としていますが、これをおこなう方法の1つは、GCPサービスアカウントキーを提供することです。物事をシンプルにするために、当初、TerraformプロバイダーはGCPサービスアカウントの静的クレデンシャルを使用していました。しかしこのクレデンシャルには有効期限がないため、漏えい等した場合、手動で無効にするまで悪意のある人物は何でもできてしまいます。
単一の非常に強固なサービスアカウント
複数のGCP ProjectでTerraformを実行するために1つのGCPサービスアカウントを使用していました。その結果、GCP組織内でサービスアカウントの権限が広範囲に渡りすぎていました。つまり、すべてのGCP Projectのプロジェクトオーナーの役割 (roles/owner
)がこのサービスアカウントに付与されていたのです。もし悪意のある人物がこのサービスアカウントにアクセスすれば、GCP組織内で何でもできるようになり、本番環境を壊すこともできます。
また、このアーキテクチャでは、プロジェクトオーナーの承認を得なくてもユーザーはさまざまなGCP Projectでリソースを作成することができました。各サービスのディレクトリはそのGCP Projectのリソースのみを管理することを想定していますが、すべてのサービスが同じTerraformサービスアカウントを共有しているため、他のプロジェクトIDを指定して、プロジェクトオーナーの承認なしにそこにリソースを作成することもできます。ファイルのCODEOWNERが所有者でないためです。システムで防止したかったものの、projectフィールドを確認するためにlintスクリプトを実装しました。
# terraform/microservices/mercari-xxx-jp/production/google_storage_bucket.tf
resource "google_storage_bucket" "test-bucket" {
# この値は"mercari-xxx-jp-prod"であることを予想したが、
# Terraformはまだこのリソースを作成できた
project = "mercari-yyy-jp-prod"
name = "test-bucket"
location = "US"
}
任意のコマンドが実行されるリスク
CIパイプライン構成ファイル(CircleCIの場合.circleci/config.yml
)は、Terraformモノレポと同じリポジトリにありました。このファイルはCODEOWNERSによって保護されており、変更はリポジトリ管理者によるレビューの後にmainブランチにマージされるようになっていましたが、管理者以外のユーザーによるfeatureブランチでの編集を妨げるものではありませんでした。たとえば、このリポジトリへの書き込みアクセス権を持つユーザーは、CircleCIで任意のコマンドを実行するようにCIパイプラインを編集し、管理者の承認なしにブランチで実行できます。
これとは別に、Terraformプロバイダーを介して任意のコマンドを実行することが可能な別のシナリオがありました。たとえば、External Providerを使用すれば、Terraform構成を介して任意のコマンドを実行できます。
セキュアなTerraform CI
これらの問題を解決するために、Terraform CIとその権限を一から再デザインしました。新しいデザインの要点を説明します。
Cloud Buildを使用したキーレスアーキテクチャ
永続的なサービスアカウントキーを排除するために、CIプラットフォームをCircleCIから Cloud Buildに移行することにしました。
各GCP Projectには独自のCloud Buildサービスアカウントがあります。Cloud Buildジョブは、各ビルドで発行された一時的なクレデンシャルを介してサービスアカウントとして認証できるため、永続的なサービスアカウントキーを作成してCI内に保存する必要がなくなりました。
SaaS APIキーなどの他のクレデンシャルについては、Secret Managerを使用して保存しています。
(ちなみに、GitHubは数か月前にGitHub ActionsでOpenID Connect(OIDC)をサポートすると発表しました (発表内容はこちら)。これにより、GitHub Actionsで永続的なキーを作成および保存せずにAWS/GCPなどのクラウドプロバイダーに接続できます。新しいキーレスCIを今構築するのなら、これも良いオプションになります。)
サービスごとのサービスアカウント
2つ目の問題を解決するために、Terraformのサービスアカウントをサービスとその環境ごとに分けるという最も権限の少ない形にしました。
各サービスには、Terraform用の独自のサービスアカウントがあります。 サービスプロジェクトで読み取り専用の権限を持つ「Terraform Plan」サービスアカウントと、サービスプロジェクトでプロジェクトオーナーの役割を持つ「Terraform Apply」サービスアカウントです。名前からわかるように、CIがterraform plan
を実行するときはGCPからデータを読み取るだけなので、ターゲットプロジェクトの「Terraform Plan」サービスアカウントを使用します。terraform apply
の場合、「Terraform Apply」サービスアカウントを使用します。
GCPでは「サービスアカウントの権限借用」機能がサポートされています。この機能を使用すると、サービスアカウントまたはユーザーアカウントが別のサービスアカウントに「なりすまし」、そのアカウントの権限を使用してGCP APIを呼び出すことができます。内部的には、ターゲットのサービスアカウントに対して有効期間が短いクレデンシャルを作成することでおこなわれます。
CIが新しいジョブを実行すると、(Cloud Buildサービスアカウントとして認証されている)Terraformは、ターゲットプロジェクトを更新するためにTerraformサービスアカウントになりすまします。たとえば、プロジェクトAのTerraform構成が変更された場合、Terraformはサービスアカウント「Terraform Plan/Apply for Project A」になりすまします。各サービスアカウントにはそのサービスのプロジェクトでのみ有効な権限があるため、システムによって他のプロジェクトでリソースを作成できなくなり、terraform apply
は、権限エラーによって失敗するようになります。
しかし、新しいGCP Projectを作成する場合、適切な権限でこれらのサービスアカウントを作るにはどうすればよいでしょうか。 新しいGCP Projectを作成するには、組織で「プロジェクト作成者」の役割を持つ別の専用サービスアカウントを使用します。プロジェクトが作成されると、プロジェクト作成者(この場合はサービスアカウント)は、デフォルトでプロジェクトにおけるプロジェクトオーナーの役割を持ちます。CIはサービスごとのTerraformサービスアカウントを作成し、一時的なプロジェクト所有者としてプロジェクトオーナー/閲覧者の役割をアカウントに付与します。
ただし、プロジェクト作成者サービスアカウントからプロジェクト所有者の役割を取り消さない場合、このアカウントはすべての新しいプロジェクトのプロジェクトオーナーになり、別の「単一の非常に強力なサービスアカウント」となってしまいます。この状況を防ぐために、プロジェクトの初期設定が完了すると、CIで自動的に権限が取り消されます。そうすると次回から、新しく作成されたサービスごとのTerraformサービスアカウントはCIによって使用されます。
モノレポ以外で管理されるCIスクリプト
3つ目の問題を解決するために、Terraformモノレポ外でCIパイプライン構成を管理することにしました。
他のCIサービスと同様、CIパイプラインを説明するためのビルド構成ファイルも準備する必要があります。新しいセキュアなCIでは、Terraformモノレポには入れず、プラットフォーム管理者のみが書き込みアクセス権を持つ別の専用リポジトリで管理します。構成ファイルに変更を加えると、専用のCloud Storage(GCS)バケットにアップロードされます。新しいコミットがTerraformモノレポにプッシュされると、CIトリガーはGCS bucketからビルド構成をフェッチし、新しいCloud Buildジョブを作成します。
欠点の1つは、Cloud Build構成をソースコードリポジトリの外に保存すると、Cloud Build GitHub Appを含むCloud Buildによって管理されているビルドトリガーを使用できないことです。Cloud Functionを使用し、CIトリガーとビルドステータス通知機能を実装しました。
事前に承認されたTerraformプロバイダー
ユーザーが不適切な動作や任意のコマンドを実行するTerraformプロバイダーを使用しないようにするために、管理者によって承認されたTerraformプラグインバイナリを事前にベースCIイメージにインストールし、すべてのTerraformプロセスにこれらのプレインストールされたプラグインのみを使用させることにしました。
terraform init
コマンドには-plugin-dir=PATH
オプションがあり、プラグインレジストリからプラグインバイナリをダウンロードせずに、指定されたディレクトリに配置されたプラグインバイナリで現在のディレクトリを初期化します。現在のディレクトリに未承認のプロバイダーを使用するTerraform構成が含まれている場合、terraform init -plugin-dir=PATH
は失敗し、それ以上の実行ができなくなります。
マイグレーション
このリポジトリは数百にわたるサービスのリソースを管理するため、CIシステムを同時に新しいシステムに移行することはほぼ不可能でした。一部のサービスには、新しいCIでサポートされないさまざまなGCP ProjectのTerraform構成や「構成ドリフト」(Terraform構成と手動操作によって作成された実際のインフラ状態との差異)が存在する場合があります。これらはマイグレーションの前に解決する必要があるため、次のようにCIを段階的に移行するアプローチを採用しました。
- サービスのTerraform構成に基づいてCIプラットフォーム(CircleCI・新しいセキュアなCI)を選択するようにCIスクリプトを更新
- サービスが「セキュアなCI使用準備済み」とマークされている場合はセキュアなCIを使用。それ以外の場合は、引き続きCircleCIを使用する
- 各サービスで以下をおこなう:
- 必要なリソースを準備(例:新規のTerraformサービスアカウントと権限)
- サービスに別のサービス(別のプロジェクトのリソースなど)に存在するはずのTerraform構成がある場合は、適切な場所に移動
- サービスを「セキュアなCI使用準備済み」に指定
このアプローチですべてのサービスを1つずつセキュアなCIに移行できました。
状況
昨年、この新しいCIシステムをメインのTerraformモノレポやその他の重要なTerraformリポジトリに導入しました。Terraformリポジトリだけでなく、すべてのKubernetesマニフェストを管理し、GKEクラスタに配備するKubernetesマニフェストモノレポにも同じメカニズムを導入しました。
おわりに
この記事では、Terraform環境で直面したセキュリティの問題と、新しいCIシステムを構築することによって状況をどのように改善したかについて説明しました。これらのアイデアが、Infrastructure as Code環境・サプライチェーンを管理し、そのセキュリティを向上させようとしている人々に役立つことを願っています。
関連記事
- Securityチームの@rungによるプレゼンテーション:Attacking and Securing CI/CD Pipeline – Speaker Deck
- @deeeetによるプレゼンテーション:How We Harden Platform Security at Mercari – Speaker Deck