⚠️

Terraformデータソースgoogle_client_configはうかつに使うと危険

2024/08/12に公開

概要

  • google_client_config は適切な運用を行わないとセキュリティリスクがあります。
  • リージョンやゾーンの設定を取得するのに便利そうですが、実際のところはほとんどの場合、使わないほうが良いでしょう。

背景

  • Terraform を使用している時、プロバイダーで指定したパラメーターと同じ値をリソースで使用するケースがあります:

    provider "google" {
      project = "your-project-id"
    }
    
    resource "google_project_iam_member" "project" {
      # provider で指定したのと同じ値を指定することが多い
      project = "your-project-id"
      role    = "roles/editor"
      member  = "user:jane@example.com"
    }
    
  • こういったパラメーターは1回だけ指定するように実装したくなりますが (いわゆる DRY)、残念ながら Terraform ではプロバイダーで指定したパラメーターを参照して再利用する機能がありません。

  • 他の方法の一つとして、データソースを使用することで provider に設定した値を取得する方法があります。プロジェクトIDの場合、 google_project が利用できます:

    provider "google" {
      project = "your-project-id"
    }
    
    data "google_project" "project" {
    }
    
    resource "google_project_iam_member" "project" {
      project = data.google_project.project.project_id
      role    = "roles/editor"
      member  = "user:jane@example.com"
    }
    
    • プロジェクトIDをプロバイダーで設定せずに環境変数 CLOUDSDK_CORE_PROJECT で設定する場面でも通用するのもこの方法の魅力です。

google_client_config はリージョンやゾーンの参照の解決になるか?

リージョンやゾーンについても同様にデータソースで参照して再利用したい…という場合に、 一見すると google_client_config が使えそうに見えます。

実装例: https://github.com/ikedam/zenn_snippets/tree/google_client_config

provider "google" {
  region = "asia-northeast2"
  zone   = "asia-northeast2-a"
}

data "google_client_config" "current" {
}


output "region" {
  value = data.google_client_config.current.region
}

output "zone" {
  value = data.google_client_config.current.zone
}

けれども実際には google_client_config はこれらの値の他に アクセストークンも保存する ため、セキュリティ的にかなり扱いづらいものになっています。

上記のような Terraform で作成されるステートファイルの中身を見ると、以下のようなデータが保存されてしまっていることが確認できます:

$ jq -r '.resources[]|select(.type=="google_client_config")|.instances[].attributes' terraform.tfstate
{
  "access_token": "ya29.(以下省略)",
  "id": "projects/\"your-project-id\"/regions/\"asia-northeast2\"/zones/\"asia-northeast2-a\"",
  "project": "your-project-id",
  "region": "asia-northeast2",
  "zone": "asia-northeast2-a"
}

アクセストークンってなあに

アクセストークンを使用することで、この Terraform を apply するときに使用した Google アカウントやサービスアカウントの権限を行使することができます。

特に、 もとの Terraform テンプレートで対象とした Google Cloud プロジェクトとは別の Google Cloud プロジェクト の API も呼び出し可能です。
例えば以下のように、他の Google Cloud プロジェクトのGCSバケットの一覧取得)を行えます:

$ curl -s \
    -H "Authorization: Bearer $(jq -r '.resources[]|select(.type=="google_client_config") |.instances[].attributes.access_token' terraform.tfstate)" \
    https://storage.googleapis.com/storage/v1/b?project=your-another-project-id \
    |jq -r .items[].name
asia.artifacts.your-another-project-id.appspot.com
your-another-project-id.appspot.com
staging.your-another-project-id.appspot.com

アクセストークンの情報を取得する と以下のことが分かります:

  • スコープはユーザー情報と Google Cloud 関連だけになっていること。
    • このため、 Google Cloud 以外のサービス(例えば Google Drive など) にこのトークンを使うことはできません。
  • トークンの有効期限が約1時間であること
$ curl -s "https://oauth2.googleapis.com/tokeninfo?access_token=$(jq -r '.resources[]|select(.type=="google_client_config")|.instances[].attributes.access_token' terraform.tfstate)"
{
  "azp": "xxxxxxxxxx.apps.googleusercontent.com",
  "aud": "xxxxxxxxxx.apps.googleusercontent.com",
  "sub": "xxxxxxxxxx",
  "scope": "openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.login https://www.googleapis.com/auth/accounts.reauth",
  "exp": "1723423915",
  "expires_in": "3594",
  "email": "xxxxxxx@gmail.com",
  "email_verified": "true",
  "access_type": "offline"
}

google_client_config を使うと困るケース

google_client_config はアクセストークンを保存してしまうため、以下の両方を満たすケースで問題が起きます:

  • Terraform テンプレートをチーム内で共有しており、ステートファイルをリモート(例えば GCS など)に保存している場合。
    • ステートファイルを参照することでアクセストークンを入手できます。
    • 参照権限さえあれば取得できるので、プロジェクトの Viewer 権限があれば閲覧できます。
  • Terraform の実行をユーザーの Google アカウントの権限で行う場合がある場合。
    • CI/CD で実行する場合はサービスアカウントの権限で実行しているケースが多く、基本的にサービスアカウントの権限は範囲が制限されているため影響範囲が抑えられます。
      • 特に、他の Google Cloud プロジェクトへの影響が出ないケースが多い。
      • ただし Terraform 用のサービスアカウントの権限は強く設定している場合も多く(実質該当 Google Cloud プロジェクトの全権限を保有している場合も多い)、影響度合いは個別の判断が必要。
    • 更新頻度が少ないので CI/CD を構築しておらず適用を手動で行っているケースや、Terraform の動作確認のためにローカルで実行する場合があるケースだと、個人の Google アカウントのアクセストークンがチーム内で共有される状態になってしまい問題になる。

リスクの度合いを整理すると以下のようになるでしょう:

Googleアカウントで実行することがある サービスアカウントでのみ実行している
Terraformテンプレート/ステートを共有していない リスクなし リスクなし
Terraformテンプレート/ステートを共有している
ゆるいアクセス制限のみかけている
リスク高 リスク中
Terraformテンプレート/ステートを共有していない
極めて厳格なアクセス制限をしている
リスク高 リスクなし

対策

google_client_config は使わないようにするのが簡単です。

今後の展望としては以下のようなチケットが切られているので、 Terraform や Google プロバイダー側で解決される可能性もあります:

DRY を実現するには、リージョンやゾーンの設定は変数や、ローカル変数で行うようにするのが良いでしょう:

variable "region" {
  value       = "ap-northeast-2"
  description = "region of Google Cloud"
}

provider "google" {
  region = var.region
}
locals {
  region = "ap-northeast-2"
}

provider "google" {
  region = local.region
}

また、環境変数 CLOUDSDK_COMPUTE_REGIONCLOUDSDK_COMPUTE_ZONE を用いた Terraform の運用はできない場面が出てきます。 CI/CD や Makefile などで TF_VAR_region などに転記するなどの対処が必要です。

Discussion