Cette page explique comment sécuriser votre application avec des en-têtes IAP signés. Une fois configuré, Identity-Aware Proxy (IAP) utilise des jetons Web JSON (JWT, JSON Web Token) pour s'assurer qu'une requête envoyée à votre application est autorisée. Cette démarche protège votre application contre les risques suivants :
- Désactivation accidentelle du service IAP
- Pare-feu mal configurés
- Accès non autorisé depuis le projet
Pour sécuriser votre application, vous devez utiliser des en-têtes signés pour tous les types d'application.
Si vous disposez d'une application d'environnement standard App Engine, vous pouvez également utiliser l'API Users.
Les vérifications d'état de Compute Engine et de GKE n'incluent pas les en-têtes JWT, et IAP ne traite pas les vérifications d'état. Si votre vérification de l'état d'état renvoie des erreurs d'accès, assurez-vous que vous l'avez correctement configurée dans la console Google Cloud et que votre validation d'en-tête JWT autorise le chemin de la vérification de l'état d'état. Pour en savoir plus, consultez Créer une vérification de l'état d'état.
Avant de commencer
Pour sécuriser votre application avec des en-têtes signés, vous avez besoin des éléments suivants :
- Une application à laquelle vos utilisateurs se connecteront
- Une bibliothèque JWT tierce correspondant à votre langage et compatible avec l'algorithme
ES256
Sécuriser votre application avec des en-têtes IAP
Pour sécuriser votre application avec le JWT IAP, validez l'en-tête, la charge utile et la signature de celui-ci. Le JWT se trouve dans l'en-tête de requête HTTP x-goog-iap-jwt-assertion. En contournant IAP, un pirate informatique peut falsifier les en-têtes d'identité IAP non signés : x-goog-authenticated-user-{email,id}. Le JWT IAP constitue une alternative plus sécurisée.
Les en-têtes signés offrent une sécurité secondaire dans le cas d'un éventuel contournement d'IAP. Lorsque IAP est activé, il retire les en-têtes x-goog-* fournis par le client quand la requête passe par l'infrastructure de diffusion du service.
Valider l'en-tête JWT
Assurez-vous que l'en-tête JWT respecte les contraintes suivantes :
| Revendications d'en-tête JWT | ||
|---|---|---|
alg |
Algorithme | ES256 |
kid |
ID de clé | Doit correspondre à l'une des clés publiques répertoriées dans le fichier de clé IAP, disponible dans deux formats différents : https://www.gstatic.com/iap/verify/public_key et https://www.gstatic.com/iap/verify/public_key-jwk |
Assurez-vous que le JWT a été signé par la clé privée correspondant à la revendication kid du jeton. Tout d'abord, récupérez la clé publique à l'un des deux emplacements suivants :
https://www.gstatic.com/iap/verify/public_key. Cette URL contient un dictionnaire JSON qui mappe les revendicationskidavec les valeurs de la clé publique.https://www.gstatic.com/iap/verify/public_key-jwk. Cette URL contient les clés publiques IAP au format JWK.
Une fois la clé publique récupérée, validez la signature à l'aide d'une bibliothèque JWT.
IAP alterne régulièrement ses clés publiques. Pour vous assurer de pouvoir toujours valider les jetons JWT, consultez Automatiser la mise en cache des clés publiques.
Valider la charge utile JWT
Assurez-vous que la charge utile JWT respecte les contraintes suivantes :
| Revendications de charge utile JWT | ||
|---|---|---|
exp |
Date/Heure d'expiration | Il doit s'agir d'une date future. Le temps est mesuré en secondes depuis l'époque UNIX. Prévoyez 30 secondes de décalage. La durée de vie maximale d'un jeton est de 10 minutes + 2 * le décalage. |
iat |
Date/Heure d'émission | Il doit s'agir d'une date antérieure. Le temps est mesuré en secondes depuis l'époque UNIX. Prévoyez 30 secondes de décalage. |
aud |
Cible | Doit être une chaîne comportant les valeurs suivantes :
|
iss |
Émetteur | Doit être https://cloud.google.com/iap. |
hd |
Domaine du compte | Si un compte appartient à un domaine hébergé, la revendication hd est fournie afin de différencier le domaine auquel le compte est associé. |
google |
Revendication Google |
Si un ou plusieurs niveaux d'accès s'appliquent à la requête, leur nom est stocké sous forme de tableau de chaînes dans l'objet JSON de la revendication google, sous la clé access_levels.
Lorsque vous spécifiez une règle relative aux appareils et que l'organisation a accès aux données de l'appareil, le |
Vous pouvez obtenir les valeurs de la chaîne aud mentionnée ci-dessus en accédant à la consoleGoogle Cloud , ou vous pouvez utiliser l'outil de ligne de commande gcloud.
Pour obtenir les valeurs de la chaîne aud depuis la console Google Cloud , accédez aux paramètres Identity-Aware Proxy de votre projet, cliquez sur Plus à côté de la ressource de l'équilibreur de charge, puis sélectionnez Signed Header JWT Audience (Audience de jeton JWT avec en-tête signé). La boîte de dialogue Jeton JWT avec en-tête signé qui apparaît affiche la revendication aud pour la ressource sélectionnée.
Pour obtenir les valeurs de la chaîne aud à l'aide de l'outil de ligne de commande gcloud de la gcloud CLI, vous devez connaître l'ID du projet. Vous le trouverez dans la fiche Informations sur le projet de la consoleGoogle Cloud . Exécutez ensuite les commandes indiquées pour chaque valeur.
Numéro du projet
Pour obtenir votre numéro de projet à l'aide de l'outil de ligne de commande gcloud, exécutez la commande suivante :
gcloud projects describe PROJECT_ID
La commande renvoie un résultat semblable à celui-ci :
createTime: '2016-10-13T16:44:28.170Z' lifecycleState: ACTIVE name: project_name parent: id: '433637338589' type: organization projectId: PROJECT_ID projectNumber: 'PROJECT_NUMBER'
ID du service
Pour obtenir votre ID de service à l'aide de l'outil de ligne de commande gcloud, exécutez la commande suivante :
gcloud compute backend-services describe SERVICE_NAME --project=PROJECT_ID --global
La commande renvoie un résultat semblable à celui-ci :
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
Récupérer l'identité de l'utilisateur
Si toutes les vérifications précédentes réussissent, récupérez l'identité de l'utilisateur. La charge utile du jeton d'ID contient les informations utilisateur suivantes :
| Identité de l'utilisateur de la charge utile du jeton d'ID | ||
|---|---|---|
sub |
Objet |
Identifiant unique et stable pour l'utilisateur. Utilisez cette valeur à la place de l'en-tête x-goog-authenticated-user-id.
|
email |
Adresse e-mail de l'utilisateur | Adresse e-mail de l'utilisateur.
|
Vous trouverez ci-dessous un exemple de code permettant de sécuriser une application avec des en-têtes IAP signés :
C#
Go
Java
Node.js
PHP
Python
Ruby
Tester le code de validation
Si vous accédez à votre application à l'aide des paramètres de requête secure_token_test, IAP inclut un JWT non valide. Utilisez-le pour vous assurer que la logique de validation des JWT gère tous les cas d'échec et pour connaître le comportement de votre application lorsqu'elle reçoit un JWT non valide.
Créer une exception de vérification d'état
Comme mentionné précédemment, les vérifications d'état de Compute Engine et de GKE n'utilisent pas les en-têtes JWT, et IAP ne gère pas ces vérifications. Vous devez configurer votre vérification d'état et votre application pour autoriser l'accès à ces vérifications.
Configurer la vérification d'état
Si vous n'avez pas encore défini de chemin pour votre vérification de l'état d'état, attribuez-lui un chemin non sensible à l'aide de la consoleGoogle Cloud . Veillez à ce qu'aucune autre ressource ne partage ce chemin.
- Accédez à la page Vérifications d'état de la console Google Cloud .
Accéder à la page "Vérifications d'état" - Cliquez sur la vérification d'état que vous utilisez pour votre application, puis sur Modifier.
- Dans le champ Chemin de requête, ajoutez un nom de chemin non sensible. Cela permet de spécifier le chemin d'URL utilisé par Google Cloud lors de l'envoi de requêtes de vérification de l'état'état.
En cas d'omission, la requête de vérification de l'état est envoyée à
/. - Cliquez sur Save.
Configurer la validation JWT
Dans votre code qui appelle le processus de validation JWT, ajoutez une condition pour renvoyer un code 200 à votre chemin de vérification d'état. Exemple :
if HttpRequest.path_info = '/HEALTH_CHECK_REQUEST_PATH' return HttpResponse(status=200) else VALIDATION_FUNCTION
Automatiser la mise en cache des clés publiques
IAP alterne régulièrement ses clés publiques. Pour vous assurer de toujours pouvoir valider le jeton JWT IAP, nous vous recommandons de mettre en cache les clés afin d'éviter de les récupérer à partir de l'URL publique pour chaque requête, et d'automatiser le processus de mise à jour de la clé mise en cache. Cette approche est particulièrement utile pour les applications qui s'exécutent dans un environnement avec des restrictions réseau, comme un périmètre VPC Service Controls.
Un périmètre VPC Service Controls peut empêcher l'accès direct à l'URL publique des clés. En mettant les clés en cache dans un bucket Cloud Storage, vos applications peuvent les récupérer à partir d'un emplacement situé dans votre périmètre VPC-SC.
La configuration Terraform suivante déploie une fonction sur Cloud Run qui récupère les dernières clés publiques IAP depuis https://www.gstatic.com/iap/verify/public_key-jwk et les stocke dans un bucket Cloud Storage. Une tâche Cloud Scheduler déclenche cette fonction toutes les 12 heures pour que les clés restent à jour.
Cette configuration comprend les éléments suivants :
- Les API Google Cloud nécessaires sont activées pour utiliser Cloud Run et stocker et mettre en cache les clés.
- Un bucket Cloud Storage pour stocker les clés publiques IAP récupérées
- Un bucket Cloud Storage pour préparer le code source des fonctions Cloud Run
- Comptes de service pour Cloud Run Functions et Cloud Scheduler avec les autorisations IAM appropriées
- Fonction Python permettant de récupérer et de stocker des clés
- Une tâche Cloud Scheduler pour déclencher la fonction toutes les 12 heures
Structure des répertoires
├── 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
Remplacez les éléments suivants :
-
BUCKET_NAME: nom de votre bucket Cloud Storage -
OBJECT_NAME: nom de l'objet dans lequel stocker vos clés
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" }
Remplacez les éléments suivants :
-
PROJECT_ID: ID de votre projet Google Cloud -
REGION: région dans laquelle déployer les ressources, par exempleus-central1 -
BUCKET_NAME: nom du bucket Cloud Storage qui stocke les clés IAP -
BUCKET_NAME_FUNCTION: nom du bucket Cloud Storage qui stocke le code source des fonctions 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
Créez un fichier terraform.tfvars pour spécifier l'ID de votre projet et personnaliser les noms de buckets si nécessaire :
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"
Déployer avec Terraform
- Enregistrez les fichiers dans la structure de répertoires décrite précédemment.
- Accédez au répertoire dans votre terminal et initialisez Terraform :
terraform init - Planifiez les modifications :
terraform plan - Appliquez les modifications :
terraform apply
L'infrastructure est alors déployée. La tâche Cloud Scheduler déclenche la fonction toutes les 12 heures, récupère les clés IAP et les stocke dans gs://BUCKET_NAME/iap_public_keys.jwk par défaut. Vos applications peuvent désormais récupérer les clés de ce bucket.
Effectuer un nettoyage des ressources
Pour supprimer les ressources créées par Terraform, exécutez les commandes suivantes :
gsutil rm -a gs://BUCKET_NAME/** terraform destroy -auto-approve
Remplacez BUCKET_NAME par le bucket Cloud Storage pour vos clés.
Les JWT pour les identités externes
Si vous utilisez IAP avec des identités externes, IAP émet tout de même un JWT signé à chaque requête authentifiée, tout comme avec les identités Google. Il existe toutefois quelques différences.
Informations sur le fournisseur
Lorsque vous utilisez des identités externes, la charge utile JWT contient une revendication nommée gcip. Cette revendication contient des informations sur l'utilisateur, telles que son adresse e-mail, l'URL de sa photo et tous les autres attributs propres au fournisseur.
Voici un exemple de JWT pour un utilisateur qui s'est connecté avec 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"
}',
Champs email et sub
Si un utilisateur a été authentifié par Identity Platform, les champs email et sub du JWT sont préfixés avec l'émetteur de jeton Identity Platform et l'ID de locataire utilisé (le cas échéant). Exemple :
"email": "securetoken.google.com/PROJECT-ID/TENANT-ID:demo_user@gmail.com", "sub": "securetoken.google.com/PROJECT-ID/TENANT-ID:gZG0yELPypZElTmAT9I55prjHg63"
Contrôler l'accès avec sign_in_attributes
IAM n'est pas compatible avec les identités externes, mais vous pouvez contrôler l'accès à l'aide de revendications intégrées dans le champ sign_in_attributes. Prenons l'exemple d'un utilisateur qui s'est connecté avec un fournisseur 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"
}
Vous pouvez ajouter à votre application une logique semblable au code ci-dessous pour restreindre l'accès aux utilisateurs disposant d'un rôle valide :
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.
}
Vous pouvez accéder à d'autres attributs utilisateur des fournisseurs SAML et OIDC Identity Platform à l'aide de la revendication imbriquée gcipClaims.gcip.firebase.sign_in_attributes.
Limites de taille des revendications IdP
Lorsqu'un utilisateur se connecte avec Identity Platform, les attributs utilisateur supplémentaires sont propagés à la charge utile du jeton d'ID Identity Platform sans état, qui est transmis de manière sécurisée à IAP. IAP émettra ensuite son propre cookie opaque sans état, qui contient également les mêmes revendications. IAP génère l'en-tête JWT signé en fonction du contenu du cookie.
Par conséquent, si une session est lancée avec de nombreuses revendications, elle peut dépasser la taille maximale autorisée pour les cookies, qui est généralement d'environ 4 Ko dans la plupart des navigateurs. L'opération de connexion échouera.
Assurez-vous que seules les revendications nécessaires sont propagées dans les attributs SAML ou OIDC du fournisseur d'identité. Une autre option consiste à utiliser des fonctions de blocage pour filtrer les revendications qui ne sont pas requises pour la vérification de l'autorisation.
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,
};
}
});