この記事ではGoogle CloudのIdentity Aware Policyにて、Webアプリケーションを保護するためにWorkforce Identity Federationを使用した認証を設定していきます。
- この記事は人の手 + AIで書かれています。AIでレビューし改善案を確認した上で採用しています
- コードはAIで書き、実際にデプロイした上で動作確認したものを掲載しています
- 図表はAI生成です
- まとめの項目は100%AI生成です
Workforce Identity Federationとは
外部IdPを使用して、ユーザーを認証するための仕組みです。
これによりIdPをGoogleの認証に紐づけることなくIdPのユーザーにGoogle Cloudへのアクセス権を付与できます。
GWSやCloud IdentityではIdPのユーザーをGoogleアカウントに同期させSSOでログインさせる方法がありますが、その方法を取ることなくアクセス権を付与する際に使用します。
似た名前のサービスとしてWorkload Identity Federationがありますが、そちらはユーザーではなくてワークロード、つまり機械的なアクセスが主軸です。
Workforce Identity Federationは組織単位での設定になります。
組織でPoolとProviderの設定を行い、そのPoolに対して各プロジェクトでIAMロールを紐付けていきます。
IAMのprincipal識別子はPool単位になっており、以下の形式でIAMポリシーから参照されます。
principal://iam.googleapis.com/locations/global/workforcePools/POOL_ID/subject/VALUE
principalSet://iam.googleapis.com/locations/global/workforcePools/POOL_ID/group/VALUE
リソース構成としては、1つの組織の中に複数のPoolがあり、それぞれのPoolが複数のProviderを持つことができます。
ただし、IAPを使用する際にはPoolに含めるProviderは1つに制限されます。
IAPはユーザーを自動的にIdPへリダイレクトする仕組みのため、PoolにProviderが複数あると認証できなくなります。
Workforce Identity Federationの設定
今回は外部IdPとしてOkta + SAMLを使用します。
Entra IDを個人で持ち合わせていなかったので、ドキュメントがあり開発目的の利用が簡単に始められるOktaを選んでいます。
OktaでのWorkforce Identity Federationの設定方法のドキュメントは以下になります。
Poolの作成
Terraformでは以下のコードになります。
resource "google_iam_workforce_pool" "pool" {
workforce_pool_id = var.workforce_pool_id
parent = "organizations/${var.organization_id}"
location = var.location
display_name = var.display_name
description = var.description
disabled = false
}
location は global です。
Workforce Identity Federationはグローバルのリソースです。
workforce_pool_id はAPIのパラメータにも書かれている通り、下記の条件を満たす必要があります。
- グローバルでユニークであること
- 6-63文字の、英小文字、数字、ハイフンで構成されること (先頭は英小文字、最後はハイフン以外)
-
gcp-から始まらないこと
ここで指定した workforce_pool_id は今後連携コンソールへのアクセスやIdPのリダイレクトURIに含まれます。
外から見られるリダイレクトのURLに含まれるため、秘匿する情報を含めないように注意してください。
他の項目の説明はAPIリファレンスを参照してください。
Terraformのドキュメントはこちらです
Providerの作成
前述の通り、Okta + SAMLの構成で作成します。Terraformのコードは以下の通りになります。
resource "google_iam_workforce_pool_provider" "okta_saml" {
workforce_pool_id = google_iam_workforce_pool.pool.workforce_pool_id
location = var.location
provider_id = var.provider_id
display_name = "Okta SAML Provider"
description = "SAML provider backed by Okta Integrator Free Plan"
attribute_mapping = {
"google.subject" = "assertion.subject"
"google.groups" = "assertion.attributes.groups"
}
saml {
idp_metadata_xml = var.idp_metadata_xml
}
}
ここでの provider_id もリダイレクト先のURLに含まれる値になります。
Poolの中でユニークであればいいため、okta-saml のようなシンプルな名前でも問題ありません。
attribute_mapping はIdP側の属性をGoogle Cloudで扱う際のマッピング設定になります。
ここでは必須項目である google.subject と、IAM管理のために google.groups を設定してOkta側のグループをGoogle Cloudで使用できるようにしています。
google.groups はSAMLの複数値属性をそのままマッピングするため添え字は不要です。個々の設定可能な値については下記ドキュメントを参照ください。
今回 attribute_condition は設定していませんが、IdPがマルチテナントである場合など複数のユーザーが同じIdPを使用していて特定のユーザーに絞り込みたい際には attribute_condition を設定してください。
意図しないユーザーがアクセス可能になる場合があります。
また、Google Cloudにアクセスできるユーザーを絞り込みたいとか、グループに制限をかけたい際には attribute_condition や類似機能を使用してください。
samlの idp_metadata_xml はIdPごとに取得方法が異なるので、それぞれ使用しているサービスのドキュメントを確認してください。
今回はOkta設定もTerraformで構築しているため、下記のコードで取得できています。
SSOのURL設定などはTerraformでリソース参照にすると循環参照で作成できなくなってしまうので、ドキュメントに記載の値を事前に計算して設定しています。
Okta側の設定に必要な値は以下の形式で算出できます。
- ACS URL:
https://auth.cloud.google/signin-callback/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID - Audience:
https://iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID
terraform {
required_providers {
okta = {
source = "okta/okta"
version = "~> 6.15.0"
}
}
}
resource "okta_app_saml" "wif" {
label = var.label
status = "ACTIVE"
sso_url = var.acs_url
recipient = var.acs_url
destination = var.acs_url
audience = var.audience
subject_name_id_template = "$${user.email}"
subject_name_id_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
response_signed = true
assertion_signed = true
signature_algorithm = "RSA_SHA256"
digest_algorithm = "SHA256"
authn_context_class_ref = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
attribute_statements {
type = "EXPRESSION"
name = "email"
namespace = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
values = ["user.email"]
}
attribute_statements {
type = "GROUP"
name = "groups"
namespace = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
filter_type = "REGEX"
filter_value = ".*"
}
}
output "idp_metadata_xml" {
description = "SAML IdP metadata XML from the Okta app resource, ready to pass to a workforce identity pool provider"
value = okta_app_saml.wif.metadata
sensitive = true
}
idp_metadata_xml の値の箇所が取得するためのコードです。
Poolを作成できると以下のように表示されます。
連携コンソールでの動作確認
ここまで実装すると、下記の形式のURLからWorkforce Identity Federationの連携コンソールにログインできます。
ProviderのIdPの認証ページにリダイレクトされ、ログインすると連携コンソールにアクセスできます。
Poolのコンソール画面にも、各プロバイダーでのコンソールへのログインURLが表示されています。
https://auth.cloud.google/signin/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID?continueUrl=https://console.cloud.google/
連携コンソールにアクセスできてもIAMロールが設定されていなければリソースの確認や変更はできません。
ドメインは console.cloud.google であるため、通常のコンソールである https://console.cloud.google.com とは異なります。
連携コンソールは通常のコンソールよりも機能が制限されています。Cloud Shellが使えないといったことをはじめ、コンソールの機能であったり設定可能な項目が一部限られます。
通常のコンソールと見た目が大きく異なると言ったことはありません。
https://docs.cloud.google.com/iam/docs/workforce-console-sso?hl=ja
https://docs.cloud.google.com/iam/docs/federated-identity-supported-services?hl=ja
単に https://console.cloud.google へアクセスすると、Workforce Identity FederationのProvider名の入力を求められます。
入力値の形式は locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID です。
適切な値を入力するとIdPへリダイレクトされ、認証を求められます。
CLIでのログインについては下記を参照ください。
下記で生成した設定ファイルを使ってCLIを呼び出すことでブラウザが立ち上がり認証できます。
Workforce Identity Federation編のまとめ
こちらのセクションではWorkforce Identity Federationの設定を行い、IdPの情報をGoogle Cloudへ登録し、連携コンソールでログインできるところまで設定しました。
IAPとは
IAPはCloud Runやアプリケーション ロードバランサ(以下LB)にて認証をかけることのできるマネージドサービスです。
SSHの認証などの機能もありますが、今回はウェブアプリケーションの保護の部分のみに着目します。
IAPを使用すると、IAPで保護したリソースに対して認証を求めることができます。
認証には下記の3つのうち1つを使用できます。
- Googleアカウント
- Workforce Identity Federation
- Identity Platform
今回はこのうちWorkforce Identity FederationでIAPを構築します。
IAPで保護するリソースの構築
今回はCloud RunをIAPで保護するリソースとして構築します。
また、Cloud Runをバックエンドとして持つ外部アプリケーションLBを構築し、そちらもIAPで保護します。
IAPをCloud RunとLBの両方で有効にすることはできません。
つまり、下記の3パターンで確認します。
- Cloud RunをIAPで保護、
run.appのドメインでアクセス - Cloud RunをIAPで保護、LB経由でアクセス
- LBをIAPで保護、LB経由でアクセス
- 背後のCloud RunではIngressを内部+LBの設定にするため、
run.appへの直接アクセスは404になります
- 背後のCloud RunではIngressを内部+LBの設定にするため、
Cloud Run上で動作するアプリケーションの性質はIAPの動作に影響しないので、デフォルトのイメージを使用します。
us-docker.pkg.dev/cloudrun/container/hello です。
IAPで付加されるヘッダーの確認には ghcr.io/k-kojima-yumemi/ecs-initial-image:latest のイメージを使用します。
https://github.com/k-kojima-yumemi/docker-images/tree/main/ecs-initial-image
で公開しているイメージで、APIサーバーに届いたリクエストの内容をそのまま返すようにしています。
IAP設定
OAuthの設定
まず、プロジェクトでのOAuthの設定をします。
ユーザーの種類は「内部」にしておきます。
これでも問題なく動作していたため、「外部」に設定する必要はなさそうです。
次にOAuthのクライアントを作成します。
の手順通りに作成します。この方法で作成したクライアントはコンソール上に表示されないので、CLIなどで作成できたかの確認などをしてください。
以下のTerraformで作成しています。
google_iam_oauth_client がOAuthのクライアントです。
上記ドキュメント上に、allowed_redirect_uris については google_iam_oauth_client のリソースが作成された後にそのclientID(リソース作成後に確定する値)を含むURLを設定すると記載があるため、terraform_data を使い無理やりCLIから更新しています。
Terraformのリソース定義だけでは素直に表現できないと思われます。
resource "google_iam_oauth_client" "this" {
project = var.project_id
location = "global"
oauth_client_id = var.oauth_client_id
display_name = var.display_name
client_type = "CONFIDENTIAL_CLIENT"
allowed_grant_types = ["AUTHORIZATION_CODE_GRANT"]
allowed_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
# こちらの値をclient_idが含まれるURLで更新する必要があります
allowed_redirect_uris = ["https://iap.googleapis.com/v1/oauth/clientIds/PLACEHOLDER:handleRedirect"]
lifecycle {
ignore_changes = [allowed_redirect_uris]
}
}
resource "google_iam_oauth_client_credential" "this" {
project = var.project_id
location = "global"
oauthclient = google_iam_oauth_client.this.oauth_client_id
oauth_client_credential_id = "default"
display_name = "Default credential"
}
resource "terraform_data" "fix_redirect_uri" {
triggers_replace = [google_iam_oauth_client.this.client_id]
provisioner "local-exec" {
command = "gcloud iam oauth-clients update ${google_iam_oauth_client.this.oauth_client_id} --project=${var.project_id} --location=global --allowed-redirect-uris=https://iap.googleapis.com/v1/oauth/clientIds/${google_iam_oauth_client.this.client_id}:handleRedirect"
}
}
google_iam_oauth_client_credential で作成されたクライアントシークレットはIAPの設定で使用します。
Terraformで作成すると値がstateファイルに記載されるので取り扱いに注意してください。
リソースへのIAP設定
Cloud RunへのIAP設定は以下のように設定しました。
この設定はコンソールではできず、CLIかAPIを使用して設定する必要があります。
resource "google_iap_settings" "this" {
name = "projects/${data.google_project.this.number}/iap_web/cloud_run-${var.region}/services/${google_cloud_run_v2_service.this.name}"
access_settings {
identity_sources = ["WORKFORCE_IDENTITY_FEDERATION"]
workforce_identity_settings {
workforce_pools = [var.workforce_pool_name]
oauth2 {
client_id = google_iam_oauth_client.this.client_id
client_secret = google_iam_oauth_client_credential.this.client_secret
}
}
}
}
access_settings でWorkforce Identity Federationを使用するように設定しています。
name の設定値は https://docs.cloud.google.com/iap/docs/managing-access?hl=ja を参照ください。
リソースごとに書式が決まっています。
LBのIAP設定は以下のようになります。
Cloud Runの時とnameが異なりますが、それ以外は同じです。
resource "google_iap_settings" "this" {
name = "projects/${data.google_project.this.number}/iap_web/compute/services/${local.backend_service_name}"
access_settings {
identity_sources = ["WORKFORCE_IDENTITY_FEDERATION"]
workforce_identity_settings {
workforce_pools = [var.workforce_pool_name]
oauth2 {
client_id = google_iam_oauth_client.this.client_id
client_secret = google_iam_oauth_client_credential.this.client_secret
}
}
}
}
Cloud Runには service-PROJECT_NUMBER@gcp-sa-iap.iam.gserviceaccount.com へのCloud Run 起動元の権限付与が必要です。
このサービスアカウントに起動元の権限を与えることでIAP経由でのCloud Run実行ができます。
Workforce Identity Federationのプリンシパルへは roles/iap.httpsResourceAccessor の権限を与える必要があります。
今回はOkta側で作成した google-cloud-iap というグループに属するメンバーに対して権限を付与しました。
そのためプリンシパルとして以下を指定しています。
principalSet://iam.googleapis.com/locations/global/workforcePools/POOL_ID/group/google-cloud-iap
プリンシパルとして指定するのはPoolであり、Providerではありません。
Cloud Runの呼び出し
今回確認しているのは前述のとおり下記の3パターンです。
URLは実際のものから変更していますが、独自ドメインを設定し適切な証明書を紐づけています。
| アクセス方法 | URL |
|---|---|
Cloud RunをIAPで保護、run.app のドメインでアクセス |
iap-test-direct-PROJECT_NUMBER.asia-northeast1.run.app |
| Cloud RunをIAPで保護、LB経由でアクセス | direct.iap-test.example.com |
| LBをIAPで保護、LB経由でアクセス | lb-backend.iap-test.example.com |
[未実施] LBをIAPで保護、run.app のドメインでアクセス(404) |
N/A |
アクセスすると下記のようなURLにリダイレクトされます。
https://auth.cloud.google/authorize
?client_id=CLIENT_ID
&response_type=code
&scope=https://www.googleapis.com/auth/cloud-platform
&redirect_uri=https://iap.googleapis.com/v1/oauth/clientIds/CLIENT_ID:handleRedirect
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
&state=STATE
&provider_name=locations/global/workforcePools/POOL_ID/providers/okta-saml
その後OktaなどのIdP側にリダイレクトされます。
試した際には以下のURLにリダイレクトされました。
https://xxx.okta.com/app/xxx_googlecloudworkforceidentityfederation_1/xxx/sso/saml
?SAMLRequest=<SAML XML>
&RelayState=<STATE>
結果としては、上記に示したアプセス方式のどのパターンでもIAPの保護がされます。
またアプリケーション側に渡ってくるヘッダーも方式による差はほぼありません。
Cloud Runに直接IAPをつけて直接 run.app のドメインでアクセスした場合には以下のような情報がアプリケーションに渡ります。
{
"method": "GET",
"path": "/__debug/request",
"url": "http://iap-test-direct-PROJECT_NUMBER.asia-northeast1.run.app/__debug/request",
"query": {},
"headers": {
"x-goog-authenticated-user-id": "sts.google.com:TOKEN",
"x-goog-iap-jwt-assertion": "ASSERTION_JWT",
"x-serverless-authorization": "bearer ACCESS_JWT"
},
"body": null
}
x-goog-authenticated-user-id の後半のトークンですが、Opaque tokenと思われます。
sts.google.com のプレフィックスのためそこからWorkforce Identity Federationとわかります。
x-goog-iap-jwt-assertion にはアクセスしたユーザーの情報が含まれるJWTになっています。
x-serverless-authorization にはCloud Runの起動元の認証を通したサービスアカウントの認証トークンが含まれます。
IAPの場合は service-PROJECT_NUMBER@gcp-sa-iap.iam.gserviceaccount.com の情報になります。
x-goog-iap-jwt-assertion の中身ですが、以下のようなデータになります。
{
"aud": "/projects/PROJECT_NUMBER/locations/asia-northeast1/services/iap-test-direct",
"azp": "/projects/PROJECT_NUMBER/locations/asia-northeast1/services/iap-test-direct",
"exp": 1786540746,
"iat": 1786540146,
"identity_source": "WORKFORCE_IDENTITY",
"iss": "https://cloud.google.com/iap",
"sub": "sts.google.com:TOKEN",
"workforce_identity": {
"iam_principal": "principal://iam.googleapis.com/locations/global/workforcePools/POOL_ID/subject/USER_EMAIL",
"workforce_pool_name": "locations/global/workforcePools/POOL_ID"
}
}
sub の値は x-goog-authenticated-user-id の値と同じです。
workforce_identity.iam_principal にアクセスしてきたユーザーの情報が含まれます。
特にグループなどの属性を判別する値は含まれていないようです。
上記はCloud Runでのヘッダーですが、LB経由でアクセスした際にも同じようなデータが取得できます。
LBを通した際の差分は x-forwarded-for のヘッダーにIPが増えていることくらいで、IAPに関わる部分については大きな差分はありませんでした。
x-goog-iap-jwt-assertion についても aud や azp の値が /projects/PROJECT_NUMBER/global/backendServices/BACKEND_SERVICE_ID のようになっているくらいです。
workforce_identity や sub の値については同じでした。
ここで注意したいのは、IAPで付与されるヘッダーの中にはユーザーの属性情報が含まれないことです。
例えばユーザーのグループはIAPでのIAMのプリンシパルとして設定できますが、属性情報としてアプリケーション側には渡されません。
そのためアプリケーション側の認可条件として属性情報を使用することが難しいです。
IAPには SAML attribute propagation という機能があり、x-goog-iap-attr-{属性名} ヘッダーやJWTの additional_claims にSAML属性を含める設定が存在します。
今回Workforce Identity Federation + SAMLの構成でも試みましたが、ヘッダーへの属性の付与は確認できませんでした。
本機能はGoogle Workspace SSOを前提とした機能であり、Workforce Identity Federationとの組み合わせでは現時点では動作しないようです。
IAP編のまとめ
こちらのセクションではOAuthクライアントの作成、Cloud RunとLBへのIAP設定、Workforce Identity Federationを使用した認証の動作確認を行いました。
Cloud RunへのIAP直接適用とLB経由のどのパターンでもIAPによる保護が機能することを確認しました。
またアプリケーションに渡されるヘッダーについて確認し、x-goog-iap-jwt-assertion にアクセスユーザーの情報が含まれる一方、グループなどの属性情報はアプリケーション側には渡されないことを確認しました。
まとめ
本記事では、OktaをIdPとしてWorkforce Identity FederationとIAPを組み合わせ、Google Cloud上のウェブアプリケーションをGoogleアカウントなしで保護する構成をTerraformで構築しました。
Workforce Identity Federationを使用することで、GWSやCloud Identityへのユーザー同期を行わずとも、外部IdPのユーザーにGoogle Cloudへのアクセス権を付与できます。
OktaとSAMLを組み合わせた設定はTerraformで管理でき、Pool・Provider・IAMロールの関係を整理することで、組織全体で一貫した認証基盤を構築できます。
IAPはCloud RunやLBに対してWorkforce Identity Federationによる認証を簡単に追加できるマネージドサービスです。
アクセス制御はIAMのプリンシパル(ユーザーやグループ)で管理でき、認証済みユーザーの情報は x-goog-iap-jwt-assertion ヘッダーを通じてアプリケーションに渡されます。
ただし、グループなどの属性情報はヘッダーに含まれず、IAPのSAML attribute propagation機能もWorkforce Identity Federationとの組み合わせでは動作しないことを確認しました。
属性ベースの認可をアプリケーション側で行う場合は別途IdP側からの情報取得が必要になる点に注意してください。


