このページでは、署名付きの IAP ヘッダーを使用してアプリを保護する方法について説明します。Identity-Aware Proxy(IAP)を構成すると、JSON Web Token(JWT)によってアプリに対するリクエストが承認されます。アプリは次のようなリスクから保護されます。
- IAP が誤って無効化される
- ファイアウォールの構成が誤っている
- プロジェクト内からの不正アクセス
アプリを保護するには、すべてのアプリの種類に対して署名付きヘッダーを使用する必要があります。
また、App Engine スタンダード環境アプリを使用している場合は、Users API を使用できます。
Compute Engine と GKE のヘルスチェックには JWT ヘッダーが含まれていません。このため、IAP はヘルスチェックを処理しません。ヘルスチェックでアクセスエラーが返された場合は、ヘルスチェックが Google Cloud コンソールで正しく構成されていることと、JWT ヘッダーの検証でヘルスチェックのパスが許可されていることを確認してください。詳細については、ヘルスチェックの例外を作成するをご覧ください。
始める前に
署名済みヘッダーを使用してアプリを保護するには、次のものが必要になります。
- ユーザーが接続するアプリケーション。
ES256アルゴリズムをサポートする、使用言語に対応するサードパーティの JWT ライブラリ。
IAP ヘッダーによるアプリの保護
IAP JWT でアプリを保護するには、JWT のヘッダー、ペイロード、署名を確認します。JWT は、HTTP リクエスト ヘッダー x-goog-iap-jwt-assertion にあります。IAP をバイパスすると、IAP の署名がない ID ヘッダー x-goog-authenticated-user-{email,id} の偽造が可能になります。IAP JWT はより安全な代替手段を提供します。
IAP がバイパスされた場合に備えて、署名付きヘッダーを使用することで補助的なセキュリティを提供します。IAP が有効になっている場合、リクエストが IAP の処理インフラストラクチャを通過するときに、クライアントが設定した x-goog-* ヘッダーが削除されます。
JWT ヘッダーの検証
JWT ヘッダーが次の制約に従っていることを確認します。
| JWT ヘッダー クレーム | ||
|---|---|---|
alg |
アルゴリズム | ES256 |
kid |
キー ID | IAP キーファイルにある公開鍵のいずれかに対応する必要があります。https://www.gstatic.com/iap/verify/public_key または https://www.gstatic.com/iap/verify/public_key-jwk のいずれかの形式で使用できます。 |
JWT が、トークンの kid クレームに対応する秘密鍵で署名されていることを確認します。まず、次のいずれかから公開鍵を取得します。
https://www.gstatic.com/iap/verify/public_key: この URL には、kidクレームを公開鍵の値にマッピングする JSON 辞書が含まれています。https://www.gstatic.com/iap/verify/public_key-jwk: この URL には、JWK 形式の IAP 公開鍵が含まれています。
公開鍵を取得したら、JWT ライブラリを使用して署名を検証します。
IAP は公開鍵を定期的にローテーションします。JWT を常に検証できるようにするには、公開鍵のキャッシュ保存を自動化するをご覧ください。
JWT ペイロードの検証
JWT ペイロードが次の制約に従っていることを確認します。
| JWT ペイロード クレーム | ||
|---|---|---|
exp |
有効期限 | 将来の時点にする必要があります。この時間は、UNIX エポック時刻からの秒数です。スキューには 30 秒かかります。トークンの最大有効期間は、10 分 + 2 * スキューです。 |
iat |
発行時 | 過去の時点にする必要があります。この時間は、UNIX エポック時刻からの秒数です。スキューには 30 秒かかります。 |
aud |
対象 | 次の値を持つ文字列にする必要があります。
|
iss |
発行元 | https://cloud.google.com/iap にする必要があります。 |
hd |
アカウント ドメイン | アカウントがホスト型ドメインに属する場合、アカウントが関連付けられているドメインを区別するために hd クレームが設定されます。 |
google |
Google クレーム |
リクエストにアクセスレベルが 1 つ以上適用されている場合、その名前は google クレームの JSON オブジェクトの access_levels キーの下に、文字列配列として格納されます。
デバイス ポリシーを指定していて、組織がデバイスデータにアクセスできる場合は、 |
上記の aud 文字列の値を取得するには、Google Cloud コンソールまたは gcloud コマンドライン ツールを使用します。
Google Cloud コンソールから aud 文字列値を取得するには、プロジェクトの Identity-Aware Proxy の設定に移動し、ロードバランサ リソースの横にある [さらに表示] をクリックして、[署名済みヘッダー JWT のユーザー] を選択します。[署名済みヘッダー JWT] ダイアログが開き、選択したリソースの aud クレームが表示されます。
gcloud CLI の gcloud コマンドライン ツールを使用して aud 文字列の値を取得する場合は、プロジェクト ID を知っている必要があります。Google Cloud コンソールまたはプロジェクト情報カードでプロジェクト ID を確認し、その値ごとに指定されたコマンドを実行します。
プロジェクト番号
gcloud コマンドライン ツールを使用してプロジェクト番号を取得するには、次のコマンドを実行します。
gcloud projects describe PROJECT_ID
コマンドから次の出力が返されます。
createTime: '2016-10-13T16:44:28.170Z' lifecycleState: ACTIVE name: project_name parent: id: '433637338589' type: organization projectId: PROJECT_ID projectNumber: 'PROJECT_NUMBER'
サービス ID
gcloud コマンドライン ツールを使用してサービス ID を取得するには、次のコマンドを実行します。
gcloud compute backend-services describe SERVICE_NAME --project=PROJECT_ID --global
コマンドから次の出力が返されます。
affinityCookieTtlSec: 0 backends: - balancingMode: UTILIZATION capacityScaler: 1.0 group: https://www.googleapis.com/compute/v1/projects/project_name/regions/us-central1/instanceGroups/my-group connectionDraining: drainingTimeoutSec: 0 creationTimestamp: '2017-04-03T14:01:35.687-07:00' description: '' enableCDN: false fingerprint: zaOnO4k56Cw= healthChecks: - https://www.googleapis.com/compute/v1/projects/project_name/global/httpsHealthChecks/my-hc id: 'SERVICE_ID' kind: compute#backendService loadBalancingScheme: EXTERNAL name: my-service port: 8443 portName: https protocol: HTTPS selfLink: https://www.googleapis.com/compute/v1/projects/project_name/global/backendServices/my-service sessionAffinity: NONE timeoutSec: 3610
ユーザー ID の取得
上記の検証がすべて成功したら、ユーザー ID を取得します。ID トークンのペイロードには次のユーザー情報が含まれています。
| ID トークン ペイロードのユーザー ID | ||
|---|---|---|
sub |
件名 |
ユーザー固有の安定した識別子。この値は、x-goog-authenticated-user-id ヘッダーの代わりに使用します。 |
email |
ユーザーのメール | ユーザーのメールアドレス。
|
次のサンプルコードでは、署名付き IAP ヘッダーを使用してアプリを保護しています。
C#
Go
Java
Node.js
PHP
Python
Ruby
検証コードのテスト
secure_token_test クエリ パラメータを使用してアプリにアクセスすると、IAP に無効な JWT が設定されます。これを使用して、JWT 検証ロジックがさまざまな失敗事例に対応し、無効な JWT を受信したときのアプリの動作を確認します。
ヘルスチェックの例外の作成
前述のように、Compute Engine と GKE のヘルスチェックには JWT ヘッダーが含まれていません。このため、IAP はヘルスチェックを処理しません。ヘルスチェックへのアクセスを許可するように、ヘルスチェックとアプリを構成する必要があります。
ヘルスチェックの構成
ヘルスチェックのパスが未設定の場合は、Google Cloud コンソールでヘルスチェックの非機密パスを設定します。このパスは他のリソースと共有しないでください。
- Google Cloud コンソールの [ヘルスチェック] ページに移動します。
[ヘルスチェック] ページに移動 - アプリに使用しているヘルスチェックをクリックしてから、[編集] をクリックします。
- [リクエストパス] で、機密でないパス名を追加します。ヘルスチェック リクエストの送信時に Google Cloud が使用する URL パスを指定します。省略すると、ヘルスチェック リクエストは
/に送信されます。 - [保存] をクリックします。
JWT 検証の構成
JWT 検証ルーチンを呼び出すコードに、ヘルスチェック パスに対して HTTP ステータス 200 を返す条件を追加します。次に例を示します。
if HttpRequest.path_info = '/HEALTH_CHECK_REQUEST_PATH' return HttpResponse(status=200) else VALIDATION_FUNCTION
公開鍵のキャッシュ保存を自動化する
IAP は公開鍵を定期的にローテーションします。IAP JWT を常に検証できるようにするには、各リクエストで公開 URL から鍵を取得しないように鍵をキャッシュに保存し、キャッシュに保存された鍵を更新するプロセスを自動化することをおすすめします。このアプローチは、VPC Service Controls の境界など、ネットワーク制限のある環境で実行されるアプリケーションに特に役立ちます。
VPC Service Controls の境界により、鍵の一般公開 URL への直接アクセスを防止できます。Cloud Storage バケットに鍵をキャッシュに保存することで、アプリケーションは VPC-SC 境界内のロケーションから鍵を取得できます。
次の Terraform 構成は、https://www.gstatic.com/iap/verify/public_key-jwk から最新の IAP 公開鍵を取得し、Cloud Storage バケットに保存する関数を Cloud Run にデプロイします。Cloud Scheduler ジョブは、この関数を 12 時間ごとにトリガーして、鍵を最新の状態に保ちます。
この設定には、次の作業が含まれます。
- Cloud Run を使用してキーを保存し、キャッシュに保存するために必要な Google Cloud API が有効になっている
- 取得した IAP 公開鍵を保存する Cloud Storage バケット
- Cloud Run functions のソースコードをステージングする Cloud Storage バケット
- 適切な IAM 権限を持つ Cloud Run functions と Cloud Scheduler のサービス アカウント
- キーを取得して保存する Python 関数
- 関数を 12 時間ごとにトリガーする Cloud Scheduler ジョブ
ディレクトリ構造
├── function_source/ │ ├── main.py │ └── requirements.txt ├── main.tf ├── outputs.tf ├── variables.tf └── terraform.tfvars
function_source/main.py
import functions_framework import requests from google.cloud import storage import os # Environment variables to be set in the function configuration BUCKET_NAME = os.environ.get("BUCKET_NAME") OBJECT_NAME = os.environ.get("OBJECT_NAME", "iap_public_keys.jwk") IAP_KEYS_URL = "https://www.gstatic.com/iap/verify/public_key-jwk" @functions_framework.http def update_iap_keys(request): """Fetches IAP public keys from the public URL and stores them in a Cloud Storage bucket.""" if not BUCKET_NAME: print("Error: BUCKET_NAME environment variable not set.") return "BUCKET_NAME environment variable not set.", 500 try: # Fetch the keys response = requests.get(IAP_KEYS_URL) response.raise_for_status() # Raise an exception for bad status codes keys_content = response.text print(f"Successfully fetched keys from {IAP_KEYS_URL}") # Store in Cloud Storage storage_client = storage.Client() bucket = storage_client.bucket(BUCKET_NAME) blob = bucket.blob(OBJECT_NAME) blob.upload_from_string(keys_content, content_type='application/json') print(f"Successfully wrote IAP keys to gs://{BUCKET_NAME}/{OBJECT_NAME}") return f"Successfully updated {OBJECT_NAME} in bucket {BUCKET_NAME}", 200 except requests.exceptions.RequestException as e: print(f"Error fetching keys from {IAP_KEYS_URL}: {e}") return f"Error fetching keys: {e}", 500 except Exception as e: print(f"Error interacting with Cloud Storage: {e}") return f"Error interacting with Cloud Storage: {e}", 500
次のように置き換えます。
-
BUCKET_NAME: Cloud Storage バケットの名前 -
OBJECT_NAME: 鍵を保存するオブジェクトの名前
function_source/requirements.txt
functions-framework==3.* requests google-cloud-storage
variables.tf
variable "project_id" { description = "The Google Cloud project ID." type = string default = PROJECT_ID } variable "region" { description = "The Google Cloud region." type = string default = "REGION" } variable "iap_keys_bucket_name" { description = "The name of the Cloud Storage bucket to store IAP keys." type = string default = BUCKET_NAME" } variable "function_source_bucket_name" { description = "The name of the Cloud Storage bucket to store the function source code." type = string default = "BUCKET_NAME_FUNCTION" }
次のように置き換えます。
-
PROJECT_ID: 実際の Google Cloud プロジェクト ID -
REGION: リソースをデプロイするリージョン(例:us-central1) -
BUCKET_NAME: IAP 鍵を保存する Cloud Storage バケットの名前 -
BUCKET_NAME_FUNCTION: Cloud Run 関数のソースコードを格納する Cloud Storage バケットの名前
main.tf
terraform { required_providers { google = { source = "hashicorp/google" version = ">= 4.50.0" } google-beta = { source = "hashicorp/google-beta" version = ">= 4.50.0" } } } provider "google" { project = var.project_id region = var.region } provider "google-beta" { project = var.project_id region = var.region } # Enable necessary APIs resource "google_project_service" "services" { for_each = toset([ "storage.googleapis.com", "cloudfunctions.googleapis.com", "run.googleapis.com", # Cloud Functions v2 uses Cloud Run "cloudscheduler.googleapis.com", "iamcredentials.googleapis.com", "cloudbuild.googleapis.com" # Needed for Cloud Functions deployment ]) service = each.key disable_on_destroy = false } # Cloud Storage Bucket to store the IAP public keys resource "google_storage_bucket" "iap_keys_bucket" { name = var.iap_keys_bucket_name location = var.region uniform_bucket_level_access = true versioning { enabled = true } lifecycle { prevent_destroy = false # Set to true in production to prevent accidental deletion } } # Cloud Storage Bucket to store the Cloud Function source code resource "google_storage_bucket" "function_source_bucket" { name = var.function_source_bucket_name location = var.region uniform_bucket_level_access = true } # Archive the function source code data "archive_file" "function_source_zip" { type = "zip" source_dir = "${path.module}/function_source" output_path = "${path.module}/function_source.zip" } # Upload the zipped source code to the source bucket resource "google_storage_bucket_object" "function_source_object" { name = "function_source.zip" bucket = google_storage_bucket.function_source_bucket.name source = data.archive_file.function_source_zip.output_path } # Service Account for the Cloud Function resource "google_service_account" "iap_key_updater_sa" { account_id = "iap-key-updater" display_name = "IAP Key Updater Function SA" } # Grant the function's SA permission to write to the IAP keys bucket resource "google_storage_bucket_iam_member" "keys_bucket_writer" { bucket = google_storage_bucket.iap_keys_bucket.name role = "roles/storage.objectAdmin" member = "serviceAccount:${google_service_account.iap_key_updater_sa.email}" } # Cloud Function (v2) resource "google_cloudfunctions2_function" "update_iap_keys_func" { provider = google-beta # CFv2 often has newer features in google-beta name = "update-iap-keys-function" location = var.region build_config { runtime = "python312" entry_point = "update_iap_keys" source { storage_source { bucket = google_storage_bucket.function_source_bucket.name object = google_storage_bucket_object.function_source_object.name } } } service_config { max_instance_count = 1 available_memory = "256M" timeout_seconds = 60 ingress_settings = "ALLOW_ALL" service_account_email = google_service_account.iap_key_updater_sa.email environment_variables = { BUCKET_NAME = google_storage_bucket.iap_keys_bucket.name OBJECT_NAME = "iap_public_keys.jwk" } } depends_on = [ google_project_service.services, google_storage_bucket_iam_member.keys_bucket_writer ] } # Service Account for the Cloud Scheduler job resource "google_service_account" "iap_key_scheduler_sa" { account_id = "iap-key-scheduler" display_name = "IAP Key Update Scheduler SA" } # Grant the Scheduler SA permission to invoke the Cloud Function resource "google_cloudfunctions2_function_iam_member" "invoker" { provider = google-beta project = google_cloudfunctions2_function.update_iap_keys_func.project location = google_cloudfunctions2_function.update_iap_keys_func.location cloud_function = google_cloudfunctions2_function.update_iap_keys_func.name role = "roles/cloudfunctions.invoker" member = "serviceAccount:${google_service_account.iap_key_scheduler_sa.email}" } # Cloud Scheduler Job resource "google_cloud_scheduler_job" "iap_key_update_schedule" { name = "iap-key-update-schedule" description = "Fetches IAP public keys and stores them in Cloud Storage every 12 hours" schedule = "0 */12 * * *" # Every 12 hours time_zone = "Etc/UTC" region = var.region http_target { uri = google_cloudfunctions2_function.update_iap_keys_func.service_config[0].uri http_method = "POST" oidc_token { service_account_email = google_service_account.iap_key_scheduler_sa.email } } depends_on = [ google_cloudfunctions2_function_iam_member.invoker, google_project_service.services ] }
outputs.tf
output "iap_keys_bucket_url" { description = "The Cloud Storage bucket URL where IAP public keys are stored." value = "gs://${google_storage_bucket.iap_keys_bucket.name}" } output "cloud_function_url" { description = "The URL of the Cloud Function endpoint that triggers key updates." value = google_cloudfunctions2_function.update_iap_keys_func.service_config[0].uri }
terraform.tfvars
terraform.tfvars ファイルを作成して、プロジェクト ID を指定し、必要に応じてバケット名をカスタマイズします。
project_id = "your-gcp-project-id" # Optional: Customize bucket names # iap_keys_bucket_name = "custom-iap-keys-bucket" # function_source_bucket_name = "custom-func-src-bucket"
Terraform を使用してデプロイする
- ファイルを前述のディレクトリ構造に保存します。
- ターミナルでディレクトリに移動し、Terraform を初期化します。
terraform init - 変更を計画します。
terraform plan - 変更を適用します。
terraform apply
これにより、インフラストラクチャがデプロイされます。Cloud Scheduler ジョブは 12 時間ごとに関数をトリガーし、IAP 鍵を取得してデフォルトで gs://BUCKET_NAME/iap_public_keys.jwk に保存します。これで、アプリケーションはこのバケットから鍵を取得できるようになりました。
リソースのクリーンアップ
Terraform で作成されたリソースを削除するには、次のコマンドを実行します。
gsutil rm -a gs://BUCKET_NAME/** terraform destroy -auto-approve
BUCKET_NAME は、鍵の Cloud Storage バケットに置き換えます。
外部 ID 用の JWT
Google Identity と同様に、外部 ID を使用して IAP を使用すると、IAP は認証済みのリクエストごとに署名付き JWT を発行します。相違点もいくつかあります。
プロバイダ情報
外部 ID を使用する場合、JWT ペイロードには gcip という名前のクレームが含まれます。このクレームには、メール、写真の URL、プロバイダ固有の属性など、ユーザーに関する情報が含まれます。
次の例は、Facebook でログインしたユーザーの JWT を示します。
"gcip": '{
"auth_time": 1553219869,
"email": "facebook_user@gmail.com",
"email_verified": false,
"firebase": {
"identities": {
"email": [
"facebook_user@gmail.com"
],
"facebook.com": [
"1234567890"
]
},
"sign_in_provider": "facebook.com",
},
"name": "Facebook User",
"picture: "https://graph.facebook.com/1234567890/picture",
"sub": "gZG0yELPypZElTmAT9I55prjHg63"
}',
email と sub フィールド
ユーザーが Identity Platform で認証されると、JWT の email フィールドと sub フィールドの接頭辞として Identity Platform トークン発行者または使用中のテナント ID(存在する場合)が追加されます。次に例を示します。
"email": "securetoken.google.com/PROJECT-ID/TENANT-ID:demo_user@gmail.com", "sub": "securetoken.google.com/PROJECT-ID/TENANT-ID:gZG0yELPypZElTmAT9I55prjHg63"
sign_in_attributes によるアクセスの制御
IAM は外部 ID をサポートしていませんが、sign_in_attributes フィールドに埋め込まれたクレームを使用してアクセスを制御できます。たとえば、SAML プロバイダを使用してユーザーがログインしたとします。
{
"aud": "/projects/project_number/apps/my_project_id",
"gcip": '{
"auth_time": 1553219869,
"email": "demo_user@gmail.com",
"email_verified": true,
"firebase": {
"identities": {
"email": [
"demo_user@gmail.com"
],
"saml.myProvider": [
"demo_user@gmail.com"
]
},
"sign_in_attributes": {
"firstname": "John",
"group": "test group",
"role": "admin",
"lastname": "Doe"
},
"sign_in_provider": "saml.myProvider",
"tenant": "my_tenant_id"
},
"sub": "gZG0yELPypZElTmAT9I55prjHg63"
}',
"email": "securetoken.google.com/my_project_id/my_tenant_id:demo_user@gmail.com",
"exp": 1553220470,
"iat": 1553219870,
"iss": "https://cloud.google.com/iap",
"sub": "securetoken.google.com/my_project_id/my_tenant_id:gZG0yELPypZElTmAT9I55prjHg63"
}
アプリケーションに次のようなロジックを追加し、有効な役割を持つユーザーによるアクセスを制限します。
const gcipClaims = JSON.parse(decodedIapJwtClaims.gcip);
if (gcipClaims &&
gcipClaims.firebase &&
gcipClaims.firebase.sign_in_attributes &&
gcipClaims.firebase.sign_in_attribute.role === 'admin') {
// Allow access to admin restricted resource.
} else {
// Block access.
}
Identity Platform SAML および OIDC プロバイダからの追加のユーザー属性には、gcipClaims.gcip.firebase.sign_in_attributes のネストクレームを使用してアクセスできます。
IdP クレームによるサイズ制限
ユーザーが Identity Platform でログインすると、追加のユーザー属性がステートレス Identity Platform ID トークン ペイロードに反映され、IAP に安全に渡されます。IAP は独自の不透明なステートレス Cookie を発行し、この Cookie にも同じクレームが含まれます。IAP は、Cookie の内容に基づいて署名付き JWT ヘッダーを生成します。
そのため、多数のクレームを使用してセッションが開始されると、Cookie の最大許容サイズ(ほとんどのブラウザでは通常 4 KB)を超えることがあります。これにより、ログイン操作が失敗します。
IdP SAML 属性または OIDC 属性に必要なクレームのみが伝播されることを確認します。もう 1 つの方法は、ブロッキング関数を使用して、承認チェックに不要なクレームを除外することです。
const gcipCloudFunctions = require('gcip-cloud-functions');
const authFunctions = new gcipCloudFunctions.Auth().functions();
// This function runs before any sign-in operation.
exports.beforeSignIn = authFunctions.beforeSignInHandler((user, context) => {
if (context.credential &&
context.credential.providerId === 'saml.my-provider') {
// Get the original claims.
const claims = context.credential.claims;
// Define this function to filter out the unnecessary claims.
claims.groups = keepNeededClaims(claims.groups);
// Return only the needed claims. The claims will be propagated to the token
// payload.
return {
sessionClaims: claims,
};
}
});