本页面介绍了如何使用签名的 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 服务基础架构时删除客户端提供的 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 命令行工具。
如需通过aud控制台获取 Google Cloud 字符串值,请转到项目的
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 以及存储 和缓存密钥所需的 Google Cloud API
- 用于存储提取的 IAP 公钥的 Cloud Storage 存储桶
- 用于暂存 Cloud Run 函数源代码的 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,
};
}
});