本頁說明如何使用已簽署的 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 憑證附加資訊 |
如果要求適用於一或多個存取層級,這些層級的名稱會以字串陣列的形式,儲存在聲明 JSON 物件的 googleaccess_levels 鍵值下。如果您指定裝置政策,且機構有權存取裝置資料, |
如要取得上述 aud 字串的值,請存取Google Cloud 控制台,或使用 gcloud 指令列工具。
如要從 Google Cloud 控制台取得 aud 字串值,請前往專案的Identity-Aware Proxy 設定,點選負載平衡器資源旁的「更多」,然後選取「Signed Header JWT Audience」。隨即顯示「已簽署標頭的 JWT」對話方塊,其中會顯示所選資源的 aud 聲明。
如要使用 gcloud CLI 指令列工具取得 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 權杖酬載使用者身分 | ||
|---|---|---|
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 控制台的「健康狀態檢查」頁面。
前往「健康狀態檢查」頁面 - 按一下您要針對應用程式使用的健康狀態檢查,然後按一下 [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 並儲存及快取鍵
- 用來儲存擷取到的 IAP 公開金鑰的 Cloud Storage bucket
- 用於暫存 Cloud Run functions 原始碼的 Cloud Storage bucket
- Cloud Run functions 和 Cloud Scheduler 的服務帳戶,並具備適當的 IAM 權限
- 用於擷取及儲存金鑰的 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 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,
};
}
});