🥢

Google Cloud との動的クレデンシャルをプライベートな Terraform Enterprise で利用する

2024/03/26に公開

Google Cloudとの動的クレデンシャルをGoogle Cloud環境に立てたプライベートなTerraform Enterprise(以下TFE)から利用する際の設定に関するメモです。

Prerequisites

TFEに関してはこちらに記載した内容をベースに構築しています。
https://zenn.dev/i10chu/articles/fcd8691c8d0194

TFE specific requirements with dynamic credentials

TFEで動的クレデンシャルを利用する際、以下の2つのエンドポイントに対して外部からのアクセスが必要とドキュメントに記載があります。

  • /.well-known/openid-configuration: 標準的な OIDC メタデータ
  • /.well-known/jwks: クラウドプラットフォームが、TFEからのものであると主張するトークンの真正性を検証するために使用する、TFEの公開鍵

これらのエンドポイントにアクセスするとそれぞれ以下の様なレスポンスを返します。

/.well-known/openid-configuration
{
  "issuer": "https://${TFE_HOSTNAME}",
  "jwks_uri": "https://${TFE_HOSTNAME}/.well-known/jwks",
  "response_types_supported": [
    "id_token"
  ],
  "claims_supported": [
    "sub",
    "aud",
    "exp",
    "iat",
    "iss",
    "jti",
    "nbf",
    "ref",
    "terraform_run_phase",
    "terraform_workspace_id",
    "terraform_workspace_name",
    "terraform_organization_id",
    "terraform_organization_name",
    "terraform_project_id",
    "terraform_project_name",
    "terraform_run_id",
    "terraform_full_workspace"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "scopes_supported": [
    "openid"
  ],
  "subject_types_supported": [
    "public"
  ]
}
/.well-known/jwks
{
  "keys": [
    {
      "kty": "RSA",
      "n": "nZdUb...dQTYQ",
      "e": "AQAB",
      "kid": "5f93d...c62b6",
      "use": "sig",
      "alg": "RS256"
    }
  ]
}

動的クレデンシャルをTFEで利用する場合、これらのエンドポイントに対してクラウドプラットフォームからアクセス出来る様にしておく必要があります。
TFEへのインバウンドアクセスをファイヤーウォール設定で絞っている中、Google Cloudとの動的クレデンシャルを利用しようとすると、terraform apply を実行するフェーズで以下の様なエラーメッセージが出力されました。

Error: Error creating service account: Post "https://iam.googleapis.com/v1/projects/xxxxx/serviceAccounts?alt=json&prettyPrint=false": oauth2/google: unable to generate access token: Post "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/yyyyy@xxxxx.iam.gserviceaccount.com:generateAccessToken": oauth2/google: status code 400: {"error":"invalid_grant","error_description":"Error connecting to the given credential's issuer."}

動的クレデンシャルを利用するために、Google Cloudで利用されているグローバルIPアドレスをTFEに対するインバウンドアクセスで許可するというファイヤーウォール設定を行うのはあまり現実的では無いと思います。

また、動的クレデンシャルをVault-backend dynamic credentialsを介して利用する形であれば、TFE <-> Vault -> Cloud Platform というネットワークアクセスになるため、TFEに対しては、Vaultからのインバウンドアクセスのみで、パブリックからアクセスさせる必要は無くなるのですが、この場合は別途Vaultをご用意いただく必要があります。
https://developer.hashicorp.com/terraform/enterprise/workspaces/dynamic-provider-credentials/vault-backed

Upload TFE OIDC JWK to Workload Identity Provider

Google Cloudにおいては、Workload Identity ProviderにOIDC JWKをアップロードするという事が出来ます。この機能を活用いただく事で、必要なインバウンドアクセスのみを許可したプライベートなTFEからGoogle Cloudとの動的クレデンシャルを利用する事が出来ます。
https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers?hl=ja#create-oidc-jwk

動的クレデンシャルの設定に関しては、以下のTerraformコードによって既に設定されている事を前提とします。
https://github.com/hashicorp-education/learn-terraform-dynamic-credentials

まずは、TFEのAPIエンドポイント/.well-known/jwksにアクセスし、レスポンスされた内容をファイルに保存します。

curl https://${TFE_HOSTNAME}/.well-known/jwks | jq . > tfe-jwks.json

Google CloudのコンソールでWorkload Identity Federationを選択し、動的クレデンシャル用に作成したIdentity Providerを選択し、JWK file(JSON)の設定項目に、保存したJSONファイルtfe-jwks.jsonをアップロードし保存します。


TFEのJWKS JSONファイルのアップロード

設定自体はこれで完了です。この設定をして頂ければ、Google Cloudで動的クレデンシャルを利用する際に、TFEへのインバウンドアクセスをパブリックに開放して頂く必要は無くなります。

Rotation period of TFE JWKS

Google CloudにアップロードしたTFEのJWKSですが、以下のAPIドキュメントに記載がある通り90日毎にローテーションされる様です。

This key automatically rotates every 90 days, hitting this endpoint resets this timer.

そのため、上記方法も一度設定した後はそのまま使い続けられるわけではなく、90日に一回は更新させる必要がありそうです。
Google Cloudのコンソールから行った設定に関しては、google_iam_workload_identity_pool_providerリソースのoidcブロックのjwks_jsonパラメータで設定出来そうなので、動的クレデンシャルの設定をTerraformで行い、TerraformのワークフローはTFEで管理する様にしておけば、ローテーション時の作業負荷を軽減出来そうです。

例えば、以下の様な形でgoogle_iam_workload_identity_pool_providerリソースを定義し、tfe_jwksTerraform変数を、TFEのVariableとして登録します。

登録する際、Keyはtfe_jwks、Valueは、TFEのエンドポイント/.well-known/jwksで取得したJSONファイルを1行に変換した値(VS CodeだとCtrl+Jで変換出来ます)を登録します。

google_iam_workload_identity_pool_provider.tf
resource "google_iam_workload_identity_pool_provider" "tfe_provider" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.tfe_pool.workload_identity_pool_id
  workload_identity_pool_provider_id = "my-tfe-provider-id"

  attribute_mapping = {
    "google.subject"                        = "assertion.sub",
    "attribute.aud"                         = "assertion.aud",
    "attribute.terraform_run_phase"         = "assertion.terraform_run_phase",
    "attribute.terraform_project_id"        = "assertion.terraform_project_id",
    "attribute.terraform_project_name"      = "assertion.terraform_project_name",
    "attribute.terraform_workspace_id"      = "assertion.terraform_workspace_id",
    "attribute.terraform_workspace_name"    = "assertion.terraform_workspace_name",
    "attribute.terraform_organization_id"   = "assertion.terraform_organization_id",
    "attribute.terraform_organization_name" = "assertion.terraform_organization_name",
    "attribute.terraform_run_id"            = "assertion.terraform_run_id",
    "attribute.terraform_full_workspace"    = "assertion.terraform_full_workspace",
  }
  oidc {
    issuer_uri = "https://${var.tfe_hostname}"
    jwks_json  = var.tfe_jwks
  }
  attribute_condition = "assertion.sub.startsWith(\"organization:${var.tfe_organization_name}:project:${var.tfe_project_name}:workspace:\")"
}

Rotate TFE JWKS

実際にこのAPIエンドポイントapi/v2/admin/oidc-settings/actions/rotate-keyを利用して、明示的にローテーションを行なってみると、.well-known/jwksエンドポイントから得られる情報が更新されます。

OIDC署名鍵のローテーション
curl \
  --header "Authorization: Bearer $TFE_API_TOKEN" \
  --header "Content-Type: application/vnd.api+json" \
  --request POST \
  https://${TFE_HOSTNAME}/api/v2/admin/oidc-settings/actions/rotate-key

ローテーションした後に、curl https://${TFE_HOSTNAME}/.well-known/jwks | jq . コマンドで確認してみると、以下の通り、2つのキー情報がある事が確認できます。

上のブロックが古いキー情報
{
  "keys": [
    {
      "kty": "RSA",
      "n": "nZdUb...dQTYQ",
      "e": "AQAB",
      "kid": "5f93d...c62b6",
      "use": "sig",
      "alg": "RS256"
    },
    {
      "kty": "RSA",
      "n": "v81lK...j5sBw",
      "e": "AQAB",
      "kid": "7f3ce...b19d8",
      "use": "sig",
      "alg": "RS256"
    }
  ]
}

古いバージョンの鍵情報は30日経過すると自動で削除される様ですが、APIエンドポイント/api/v2/admin/oidc-settings/actions/trim-keyから明示的に削除する事も可能です。

古いキーの削除
curl \
  --header "Authorization: Bearer $TFE_API_TOKEN" \
  --header "Content-Type: application/vnd.api+json" \
  --request POST \
  https://${TFE_HOSTNAME}/api/v2/admin/oidc-settings/actions/trim-key

改めて、curl https://${TFE_HOSTNAME}/.well-known/jwks | jq . コマンドで確認してみると、古いキーは削除され、新しいキーの情報のみ存在する事が確認できます。

{
  "keys": [
    {
      "kty": "RSA",
      "n": "v81lK...j5sBw",
      "e": "AQAB",
      "kid": "7f3ce...b19d8",
      "use": "sig",
      "alg": "RS256"
    }
  ]
}

参考になる部分があれば、ご活用下さい!

Discussion