本頁面說明如何使用已簽署的 IAP 標頭保護應用程式。設定完成後,Identity-Aware Proxy (IAP) 會使用 JSON Web Token (JWT),確保傳送至應用程式的要求已獲得授權。這可保護應用程式免受下列風險:
- 不小心停用 IAP
- 防火牆設定錯誤
- 專案內未經授權的存取
為確保應用程式安全,所有應用程式類型都必須使用經過簽署的標頭。
或者,如果您有 App Engine 標準環境應用程式,可以使用 Users API。Compute Engine 和 GKE 健康狀態檢查不含 JWT 標頭,且 IAP 不會處理健康狀態檢查。如果健康狀態檢查傳回存取錯誤,請確認您已在 Google Cloud 控制台中正確設定健康狀態檢查,且 JWT 標頭驗證允許健康狀態檢查路徑。詳情請參閱「建立健康狀態檢查例外狀況」。
事前準備
如要使用經過簽署的標頭保護應用程式,您需要:
- 您希望使用者連線的應用程式。
- 支援您所用語言的第三方 JWT 程式庫,且該程式庫支援
ES256演算法。
使用 IAP 標頭保護應用程式
如要使用 IAP JWT 保護應用程式,請驗證 JWT 的標頭、酬載和簽章。JWT 位於 HTTP 要求標頭 x-goog-iap-jwt-assertion 中。如果攻擊者繞過 IAP,就能偽造 IAP 未簽署的身分標頭 x-goog-authenticated-user-{email,id}。IAP JWT 則提供更安全的替代方案。
如果有人略過 IAP,經過簽署的標頭可提供次要安全性。啟用 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。這個網址包含 JSON 字典,可將kid聲明對應至公開金鑰值。https://www.gstatic.com/iap/verify/public_key-jwk。這個網址包含 JWK 格式的 IAP 公開金鑰。
取得公開金鑰後,請使用 JWT 程式庫驗證簽章。
IAP 會定期輪替公開金鑰。為確保您隨時可以驗證 JWT,請參閱「自動快取公開金鑰」。
驗證 JWT 酬載
驗證 JWT 的酬載是否符合下列限制:
| JWT 酬載憑證附加資訊 | ||
|---|---|---|
exp |
到期時間 | 必須是未來的時間。時間以秒為單位,從世界標準時間指定期間開始計算。允許 30 秒的時間誤差。 權杖的有效期限上限為 10 分鐘 + 2 * 誤差。 |
iat |
核發時間 | 必須是過去的日期。時間以秒為單位,從世界標準時間指定期間開始計算。允許 30 秒的時間誤差。 |
aud |
目標對象 |
必須是包含下列值的字串:
|
iss |
核發單位 |
必須為 https://cloud.google.com/iap。
|
hd |
帳戶網域 |
如果帳戶屬於代管網域,系統會提供 hd 宣告,區分帳戶關聯的網域。 |
google |
Google 憑證附加資訊 |
如果要求適用一或多個存取層級,系統會將這些層級的名稱儲存在 google 宣告的 JSON 物件中,並放在 access_levels 鍵下方,做為字串陣列。
指定裝置政策且機構可存取裝置資料時, |
aud 字串的值。
如要從 Google Cloud 控制台取得aud字串值,請前往專案的「Identity-Aware Proxy settings」(Identity-Aware Proxy 設定),按一下負載平衡器資源旁的「More」(更多),然後選取「Signed Header JWT Audience」(已簽署標頭 JWT 目標對象)。隨即顯示的「Signed Header JWT」(經過簽署的標頭 JWT) 對話方塊中,會顯示所選資源的 aud 權杖附加資訊。
如要使用 gcloud CLI 取得 aud 字串值,您必須知道專案 ID。您可以在Google Cloud 控制台的「Project info」(專案資訊) 資訊卡中找到專案 ID,然後針對每個值執行指定指令。
專案編號
如要使用 gcloud CLI 取得專案編號,請執行下列指令:
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 CLI 取得服務 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 權杖酬載使用者身分 | ||
|---|---|---|
sub |
主旨 |
使用者的專屬固定 ID。請使用這個值,而非 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 控制台的「Health checks」(健康狀態檢查) 頁面。
前往「Health checks」(健康狀態檢查) 頁面 - 按一下您要針對應用程式使用的健康狀態檢查,然後按一下 [Edit] (編輯)。
- 在「Request path」(要求路徑) 下,新增非機密路徑名稱。這會指定 Google Cloud 傳送健康狀態檢查要求時使用的網址路徑。如果省略,健康狀態檢查要求會傳送至
/。 - 按一下 [儲存]。
設定 JWT 驗證
在呼叫 JWT 驗證常式的程式碼中,新增條件來為健康狀態檢查路徑提供 200 HTTP 狀態。例如:
if HttpRequest.path_info = '/HEALTH_CHECK_REQUEST_PATH' return HttpResponse(status=200) else VALIDATION_FUNCTION
自動快取公開金鑰
IAP 會定期輪替公開金鑰。為確保您隨時可以驗證 IAP JWT,建議您快取金鑰,避免每次要求都從公開網址擷取金鑰,並自動更新快取金鑰。如果應用程式在網路受限的環境中執行 (例如 VPC Service Controls perimeter),這種方法特別實用。
VPC Service Controls perimeter 可防止直接存取金鑰的公開網址。將金鑰快取到 Cloud Storage bucket 後,應用程式就能從 VPC-SC 範圍內的位置擷取金鑰。
下列 Terraform 設定會將函式部署至 Cloud Run,從 https://www.gstatic.com/iap/verify/public_key-jwk 擷取最新的 IAP 公開金鑰,並儲存在 Cloud Storage bucket。Cloud Scheduler 工作每 12 小時會觸發這項函式,確保金鑰保持最新狀態。
這項設定包含下列項目:
- 已啟用必要的 Google Cloud API,可使用 Cloud Run 並儲存及快取鍵
- Cloud Storage bucket:用於儲存擷取的 IAP 公開金鑰
- 用於暫存 Cloud Run functions 原始碼的 Cloud Storage bucket
- Cloud Run functions 和 Cloud Scheduler 的服務帳戶,具備適當的 IAM 權限
- 用於擷取及儲存金鑰的 Python 函式
- 一項 Cloud Scheduler 工作,每 12 小時觸發一次函式
目錄結構
├── 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 bucket 的名稱 -
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 名稱 -
BUCKET_NAME_FUNCTION:儲存 Cloud Run functions 原始碼的 Cloud Storage bucket 名稱
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 並視需要自訂 bucket 名稱:
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 bucket。
外部身分的 JWT
如果您將 IAP 與外部身分搭配使用,IAP 仍會針對每個已驗證的要求核發已簽署的 JWT,就像使用 Google Identity 一樣。不過,兩者間還是有一些差異。
供應商資訊
使用外部身分時,JWT 酬載會包含名為 gcip 的要求。這項要求包含使用者資訊,例如電子郵件、相片網址,以及任何其他供應商專屬屬性。
以下是使用 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 不支援外部身分,但您可以使用 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.
}
您可以使用 gcipClaims.gcip.firebase.sign_in_attributes 巢狀宣告,從 Identity Platform SAML 和 OIDC 提供者存取其他使用者屬性。
IdP 憑證大小限制
使用者透過 Identity Platform 登入後,額外的使用者屬性會傳播至無狀態 Identity Platform ID 權杖酬載,並安全地傳遞至 IAP。接著,IAP 會發出自己的無狀態不透明 Cookie,其中也包含相同的憑證。IAP 會根據 Cookie 內容產生已簽署的 JWT 標頭。
因此,如果工作階段啟動時含有許多要求,可能會超過 Cookie 大小上限 (在大多數瀏覽器中通常約為 4 KB)。這會導致登入作業失敗。
請確保只在 IdP SAML 或 OIDC 屬性中傳播必要的憑證。另一個方法是使用封鎖函式,篩除授權檢查不需要的聲明。
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,
};
}
});