Esta página descreve como proteger a sua app com cabeçalhos de CAsI assinados. Quando configurado, o Identity-Aware Proxy (IAP) usa tokens Web JSON (JWT) para garantir que um pedido à sua app está autorizado. Isto protege a sua app dos seguintes riscos:
- A IAP está desativada acidentalmente
- Firewalls configuradas incorretamente
- Acesso não autorizado a partir do projeto
Para ajudar a proteger a sua app, tem de usar cabeçalhos assinados para todos os tipos de apps.
Em alternativa, se tiver uma app do ambiente padrão do App Engine, pode usar a API Users.
As verificações de estado do Compute Engine e do GKE não incluem cabeçalhos JWT, e o IAP não processa verificações de estado. Se a verificação de estado devolver erros de acesso, certifique-se de que tem a verificação de estado configurada corretamente na Google Cloud consola e que a validação do cabeçalho JWT permite o caminho de verificação de estado. Para mais informações, consulte Crie uma exceção de verificação de estado.
Antes de começar
Para proteger a sua app com cabeçalhos assinados, precisa do seguinte:
- Uma aplicação à qual quer que os utilizadores se liguem.
- Uma biblioteca JWT de terceiros para o seu idioma que suporte o algoritmo
ES256.
Proteger a sua app com cabeçalhos de IAP
Para proteger a sua app com o JWT de IAP, valide o cabeçalho, a carga útil e a assinatura do JWT. O JWT está no cabeçalho do pedido HTTP
x-goog-iap-jwt-assertion. Se um atacante contornar a IAP, pode falsificar os cabeçalhos de identidade não assinados da IAP, x-goog-authenticated-user-{email,id}. O JWT da IAP oferece uma alternativa mais segura.
Os cabeçalhos assinados oferecem segurança secundária caso alguém contorne o IAP. Quando a IAP está ativada, a IAP remove os cabeçalhos x-goog-* fornecidos pelo cliente quando o pedido passa pela infraestrutura de publicação da IAP.
Validar o cabeçalho JWT
Verifique se o cabeçalho do JWT está em conformidade com as seguintes restrições:
| Reivindicações do cabeçalho JWT | ||
|---|---|---|
alg |
Algoritmo | ES256 |
kid |
ID da chave |
Tem de corresponder a uma das chaves públicas indicadas no ficheiro de chaves de IAP, disponível em dois formatos diferentes:
https://www.gstatic.com/iap/verify/public_key
e
https://www.gstatic.com/iap/verify/public_key-jwk
|
Certifique-se de que o JWT foi assinado pela chave privada que corresponde à reivindicação kid do token. Primeiro, obtenha a chave pública de um dos dois locais:
https://www.gstatic.com/iap/verify/public_key. Este URL contém um dicionário JSON que mapeia as reivindicaçõeskidpara os valores da chave pública.https://www.gstatic.com/iap/verify/public_key-jwk. Este URL contém as chaves públicas da IAP no formato JWK.
Depois de ter a chave pública, use uma biblioteca JWT para validar a assinatura.
O IAP roda periodicamente as respetivas chaves públicas. Para se certificar de que pode sempre validar os JWTs, consulte o artigo Automatize o armazenamento em cache de chaves públicas.
Validar a carga útil do JWT
Verifique se a carga útil do JWT está em conformidade com as seguintes restrições:
| Reivindicações de payload JWT | ||
|---|---|---|
exp |
Período de validade | Tem de ser no futuro. O tempo é medido em segundos desde o início da época UNIX. Aguarde 30 segundos para a distorção. A duração máxima de um token é de 10 minutos + 2 * skew. |
iat |
Hora de emissão | Tem de ser no passado. O tempo é medido em segundos desde o início da época UNIX. Aguarde 30 segundos para a distorção. |
aud |
Público-alvo |
Tem de ser uma string com os seguintes valores:
|
iss |
Emissor |
Tem de ser https://cloud.google.com/iap.
|
hd |
Domínio da conta |
Se uma conta pertencer a um domínio alojado, a reivindicação é fornecida para diferenciar o domínio ao qual a conta está associada.hd
|
google |
Reivindicação da Google |
Se um ou mais níveis de acesso
se aplicarem ao pedido, os respetivos nomes são armazenados no objeto JSON
da reivindicação, na chave access_levels, como uma matriz
de strings.google
Quando especifica uma política de dispositivo e a organização tem acesso aos dados do dispositivo, o |
Pode obter os valores da string aud mencionada acima acedendo à
Google Cloud consola ou usar a ferramenta de linhas de comando gcloud.
Para obter valores de string audda Google Cloud consola, aceda às
definições do Identity-Aware Proxy
para o seu projeto, clique em Mais junto ao recurso do balanceador de carga e, de seguida,
selecione Público-alvo do JWT do cabeçalho assinado. A caixa de diálogo JWT de cabeçalho assinado apresentada mostra a reivindicação aud para o recurso selecionado.
Se quiser usar a CLI gcloud
ferramenta de linhas de comando gcloud para obter os valores de string aud, tem de saber
o ID do projeto. Pode encontrar o ID do projeto no cartão
Google Cloud consola
Informações do projeto e, em seguida, executar os comandos especificados para cada valor.
Número do projeto
Para obter o número do projeto através da ferramenta de linhas de comando gcloud, execute o seguinte comando:
gcloud projects describe PROJECT_ID
O comando devolve um resultado semelhante ao seguinte:
createTime: '2016-10-13T16:44:28.170Z' lifecycleState: ACTIVE name: project_name parent: id: '433637338589' type: organization projectId: PROJECT_ID projectNumber: 'PROJECT_NUMBER'
ID do serviço
Para obter o ID do serviço através da ferramenta de linhas de comando gcloud, execute o seguinte comando:
gcloud compute backend-services describe SERVICE_NAME --project=PROJECT_ID --global
O comando devolve um resultado semelhante ao seguinte:
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
Recolher a identidade do utilizador
Se todas as validações anteriores forem bem-sucedidas, obtenha a identidade do utilizador. O payload do token de ID contém as seguintes informações do utilizador:
| Identidade do utilizador da carga útil do token de ID | ||
|---|---|---|
sub |
Assunto |
O identificador exclusivo e estável do utilizador. Use este valor em vez do cabeçalho x-goog-authenticated-user-id.
|
email |
Email do utilizador | Endereço de email do utilizador.
|
Segue-se um exemplo de código para proteger uma app com cabeçalhos de IAP assinados:
C#
Go
Java
Node.js
PHP
Python
Ruby
Testar o código de validação
Se visitar a sua app através dos parâmetros de consulta secure_token_test, a IAP vai incluir um JWT inválido. Use esta opção para se certificar de que a lógica de validação de JWT está a processar todos os vários casos de falha e para ver como a sua app se comporta quando recebe um JWT inválido.
Criar uma exceção de verificação de funcionamento
Conforme mencionado anteriormente, as verificações de estado do Compute Engine e do GKE não usam cabeçalhos JWT, e o IAP não processa as verificações de estado. Tem de configurar a verificação de estado e a app para permitir o acesso à verificação de estado.
Configurar a verificação de funcionamento
Se ainda não tiver definido um caminho para a verificação de funcionamento, use a Google Cloud consola para definir um caminho não sensível para a verificação de funcionamento. Certifique-se de que este caminho não é partilhado por nenhum outro recurso.
- Aceda à página Google Cloud console
Verificações de estado.
Aceda à página Verificações de saúde - Clique na verificação de estado que está a usar para a sua app e, de seguida, clique em Editar.
- Em Caminho do pedido, adicione um nome de caminho não sensível. Especifica o caminho do URL que o serviço usa quando envia pedidos de verificação do estado de funcionamento. Google Cloud
Se for omitido, o pedido de verificação de funcionamento é enviado para
/. - Clique em Guardar.
Configurar a validação de JWT
No código que chama a rotina de validação JWT, adicione uma condição para publicar um estado HTTP 200 para o caminho do pedido de verificação do estado. Por exemplo:
if HttpRequest.path_info = '/HEALTH_CHECK_REQUEST_PATH' return HttpResponse(status=200) else VALIDATION_FUNCTION
Automatize o armazenamento em cache de chaves públicas
O IAP roda as respetivas chaves públicas periodicamente. Para garantir que pode sempre validar o JWT de IAP, recomendamos que coloque as chaves em cache para evitar obtê-las do URL público para cada pedido e que automatize o processo de atualização da chave em cache. Esta abordagem é particularmente útil para aplicações executadas num ambiente com restrições de rede, como um perímetro dos VPC Service Controls.
Um perímetro dos VPC Service Controls pode impedir o acesso direto ao URL público das chaves. Ao colocar as chaves em cache num contentor do Cloud Storage, as suas aplicações podem obtê-las a partir de uma localização dentro do perímetro do VPC-SC.
A seguinte configuração do Terraform implementa uma função no Cloud Run que obtém as chaves públicas da IAP mais recentes de https://www.gstatic.com/iap/verify/public_key-jwk e as armazena num contentor do Cloud Storage. Uma tarefa do Cloud Scheduler aciona esta função a cada 12 horas para manter as chaves atualizadas.
Esta configuração inclui o seguinte:
- APIs necessárias ativadas para usar o Cloud Run e armazenar e colocar em cache as chaves Google Cloud
- Um contentor do Cloud Storage para armazenar as chaves públicas da IAP obtidas
- Um contentor do Cloud Storage para preparar o código fonte das funções do Cloud Run
- Contas de serviço para funções do Cloud Run e Cloud Scheduler com autorizações do IAM adequadas
- Uma função Python para obter e armazenar chaves
- Uma tarefa do Cloud Scheduler para acionar a função a cada 12 horas
Estrutura do diretório
├── 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
Substitua o seguinte:
-
BUCKET_NAME: o nome do seu contentor do Cloud Storage -
OBJECT_NAME: o nome do objeto para armazenar as suas chaves
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" }
Substitua o seguinte:
-
PROJECT_ID: o ID do seu Google Cloud projeto -
REGION: a região na qual implementar recursos, por exemplo,us-central1 -
BUCKET_NAME: o nome do contentor do Cloud Storage que armazena as chaves de CNA -
BUCKET_NAME_FUNCTION: o nome do contentor do Cloud Storage que armazena o código-fonte das funções do 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
Crie um ficheiro terraform.tfvars para especificar o ID do projeto e personalizar os nomes dos contentores, se necessário:
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"
Implemente com o Terraform
- Guarde os ficheiros na estrutura de diretórios descrita anteriormente.
- Navegue até ao diretório no terminal e inicialize o Terraform:
terraform init - Planeie as alterações:
terraform plan - Aplique as alterações:
terraform apply
Esta ação implementa a infraestrutura. A tarefa do Cloud Scheduler aciona a função a cada 12 horas, obtendo as chaves da IAP e armazenando-as em gs://BUCKET_NAME/iap_public_keys.jwk por predefinição. As suas aplicações podem agora obter as chaves deste contentor.
Limpe recursos
Para remover os recursos criados pelo Terraform, execute os seguintes comandos:
gsutil rm -a gs://BUCKET_NAME/** terraform destroy -auto-approve
Substitua BUCKET_NAME pelo contentor do Cloud Storage para as suas chaves.
JWTs para identidades externas
Se estiver a usar a IAP com identidades externas, a IAP continua a emitir um JWT assinado em cada pedido autenticado, tal como faz com as identidades Google. No entanto, existem algumas diferenças.
Informações do fornecedor
Quando usar identidades externas, a carga útil do JWT vai conter uma reivindicação com o nome gcip. Esta reivindicação contém informações do utilizador, como o respetivo email, URL da foto e quaisquer atributos adicionais específicos do fornecedor.
Segue-se um exemplo de um JWT para um utilizador que iniciou sessão com o Facebook:
"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"
}',
Os campos email e sub
Se um utilizador foi autenticado pela Identity Platform, os campos email e sub do JWT vão ter o prefixo do emissor do token da Identity Platform e o ID do inquilino usado (se existir). Por exemplo:
"email": "securetoken.google.com/PROJECT-ID/TENANT-ID:demo_user@gmail.com", "sub": "securetoken.google.com/PROJECT-ID/TENANT-ID:gZG0yELPypZElTmAT9I55prjHg63"
Controlar o acesso com o sign_in_attributes
O IAM não suporta identidades externas, mas pode usar reivindicações incorporadas no campo sign_in_attributes para controlar o acesso. Por exemplo, considere um utilizador com sessão iniciada através de um fornecedor 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"
}
Pode adicionar lógica à sua aplicação semelhante ao código abaixo para restringir o acesso a utilizadores com uma função válida:
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.
}
Pode aceder a atributos do utilizador adicionais a partir de fornecedores SAML e OIDC da Identity Platform através da reivindicação aninhada gcipClaims.gcip.firebase.sign_in_attributes.
Limitações de tamanho das reivindicações do IdP
Depois de um utilizador iniciar sessão com a Identity Platform, os atributos do utilizador adicionais são propagados para a carga útil do token de ID da Identity Platform sem estado, que é transmitida de forma segura para o IAP. Em seguida, a IAP emite o seu próprio cookie opaco sem estado, que também contém as mesmas reivindicações. O IAP gera o cabeçalho JWT assinado com base no conteúdo do cookie.
Como resultado, se uma sessão for iniciada com muitas reivindicações, pode exceder o tamanho máximo permitido de cookies, que é normalmente de cerca de 4 KB na maioria dos navegadores. Esta ação faz com que a operação de início de sessão falhe.
Certifique-se de que apenas as reivindicações necessárias são propagadas nos atributos SAML ou OIDC do IdP. Outra opção é usar as funções de bloqueio para filtrar as reivindicações que não são necessárias para a verificação de autorização.
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,
};
}
});