本頁面說明如何使用已簽署的 IAP 標頭保護應用程式。設定之後,Identity-Aware Proxy (IAP) 會使用 JSON Web Tokens (JWT) 確認對應用程式的要求已獲得授權。這樣可保護應用程式不受下列風險的影響:
- IAP 意外停用
- 防火牆設定有誤
- 未經授權從專案內存取
為確保應用程式安全無虞,所有應用程式類型都必須使用簽署標頭。
或者,若您有 App Engine 標準環境應用程式,則可使用 Users API。
Compute Engine 和 GKE 健康狀態檢查不包含 JWT 標頭,且 IAP 不會處理健康狀態檢查。如果健康狀態檢查傳回存取錯誤,請確認是否在 Google Cloud 控制台中正確設定健康狀態檢查,以及 JWT 標頭驗證是否允許健康狀態檢查路徑。詳情請參閱「建立健康狀態檢查例外狀況」。
事前準備
如要使用簽署標頭保護應用程式,請先準備好下列項目:
- 您想讓使用者連線的應用程式。
- 適用於您語言的第三方 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 金鑰下。如果您指定裝置政策,且機構有權存取裝置資料,系統也會將 |
您可以存取Google Cloud 主控台,或使用 gcloud 指令列工具,取得上述 aud 字串的值。
如要從 Google Cloud 主控台取得 aud 字串值,請前往專案的 Identity-Aware Proxy 設定,按一下負載平衡器資源旁邊的「更多」,然後選取「已簽署標頭的 JWT 目標對象」。這時出現的「Signed Header JWT」(已簽署標頭的 JWT) 對話方塊會顯示所選資源的 aud 憑證附加資訊。
如要使用 gcloud CLI gcloud 指令列工具取得 aud 字串值,則需要知道專案 ID。您可以在Google Cloud 控制台的「Project info」(專案資訊) 卡片中找到專案 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 範圍可防止直接存取金鑰的公開網址。將金鑰快取到 Cloud Storage bucket 後,應用程式就能從 VPC-SC 範圍內的位置擷取金鑰。
下列 Terraform 設定會將函式部署至 Cloud Run,從 https://www.gstatic.com/iap/verify/public_key-jwk 擷取最新的 IAP 公開金鑰,並儲存在 Cloud Storage bucket 中。Cloud Scheduler 工作每 12 小時會觸發一次這項函式,確保金鑰保持在最新狀態。
這項設定包括:
- 啟用使用 Cloud Run 和儲存/快取金鑰所需的 API Google Cloud
- 用來儲存擷取到的 IAP 公開金鑰的 Cloud Storage bucket
- 用來暫存 Cloud Run 函式原始碼的 Cloud Storage bucket
- Cloud Run 函式和 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_NAME_FUNCTION:Cloud Storage bucket 的名稱,用於儲存 Cloud Run 函式原始碼
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 值區。
外部身分的 JWT
如果您搭配外部身分使用 IAP,IAP 仍會在每個經過驗證的請求中核發簽署的 JWT,就像使用 Google 身分一樣。不過,兩者之間還是有幾項差異。
提供者資訊
使用外部身分時,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,
};
}
});