本页面介绍了如何使用签名的 IAP 标头保护应用的安全。配置 Identity-Aware Proxy (IAP) 后,该服务会使用 JSON Web 令牌 (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 身份标头 x-goog-authenticated-user-{email,id}。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 |
到期时间 | 必须是将来的时间。时间从 UNIX 计时原点开始计算,以秒为单位。允许 30 秒偏差。 令牌的最长生命周期为 10 分钟 + 2 * 偏差。 |
iat |
颁发时间 | 必须是过去的时间。时间从 UNIX 计时原点开始计算,以秒为单位。允许 30 秒偏差。 |
aud |
受众 |
必须是一个含有以下值的字符串:
|
iss |
颁发者 | 必须为 https://cloud.google.com/iap。
|
hd |
账号网域 | 如果账号属于一个托管网域,则会提供 hd 声明来区分与该账号关联的网域。 |
google |
Google 声明 |
如果一个或多个访问权限级别适用于请求,则这些级别的名称将以字符串数组形式存储在 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 令牌载荷用户身份 | ||
|---|---|---|
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 使用的网址路径。如果省略,则健康检查请求将发送到
/。 - 点击保存。
配置 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 边界)中运行的应用特别有用。
VPC Service Controls 边界可以防止直接访问密钥的公开网址。通过将密钥缓存在 Cloud Storage 存储桶中,您的应用可以从 VPC-SC 边界内的位置提取密钥。
以下 Terraform 配置会将一个函数部署到 Cloud Run,该函数会从 https://www.gstatic.com/iap/verify/public_key-jwk 中提取最新的 IAP 公钥,并将它们存储在 Cloud Storage 存储桶中。Cloud Scheduler 作业每 12 小时触发一次此函数,以确保密钥是最新的。
此设置包括以下内容:
- 已启用使用 Cloud Run 和存储/缓存密钥所需的 API Google Cloud
- 用于存储提取的 IAP 公钥的 Cloud Storage 存储桶
- 用于暂存 Cloud Run functions 源代码的 Cloud Storage 存储桶
- 具有适当 IAM 权限的 Cloud Run 函数和服务账号和 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 存储桶。
外部身份的 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 大小上限(在大多数浏览器中,此大小通常约为 4KB)。这将导致登录操作失败。
确保仅将必要的声明传播到 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,
};
}
});