Lesson 6 — Exposer, protéger, surveiller

Cloud Run privé derrière un load balancer et IAP

5 min read

This content is not translated yet. Showing the original version.

Cloud Run privé derrière un load balancer et IAP

Le chemin d'une requête

IAP (Identity-Aware Proxy) est un proxy géré par Google qui vérifie l'identité de l'utilisateur (compte Google Workspace ou Cloud Identity) et son droit roles/iap.httpsResourceAccessor avant de transmettre la requête. L'application n'a pas à coder d'authentification.

sequenceDiagram
  participant U as "Salarié"
  participant LB as "Load balancer HTTPS (IP publique)"
  participant IAP as "IAP"
  participant CR as "Cloud Run (ingress interne + LB)"
  U->>LB: "GET https://assistant.exemple.fr"
  LB->>IAP: "requête"
  IAP-->>U: "302 vers la connexion Google (si pas de session)"
  U->>LB: "requête + cookie IAP"
  IAP->>IAP: "groupe assistant-utilisateurs ? oui"
  IAP->>CR: "requête + en-tête x-goog-iap-jwt-assertion (agent IAP = invocateur)"
  CR-->>U: "réponse de l'assistant"
  U->>CR: "GET https://...run.app (accès direct)"
  CR-->>U: "404 / refus : ingress internal-and-cloud-load-balancing"

Trois verrous

VerrouParamètreEffet
Entrée réseauingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"l'URL run.app refuse le trafic internet direct
Identité utilisateuriap { enabled = true } sur le backendconnexion Google obligatoire
Autorisationroles/iap.httpsResourceAccessor à un groupeseuls les membres passent
Appel de Cloud Runroles/run.invoker à l'agent IAPCloud Run n'accepte que les requêtes relayées par IAP
files/app.tf (extrait)
resource "google_cloud_run_v2_service" "app" {
  name     = "${var.prefix}-assistant"
  location = var.region
  ingress  = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"

  template {
    service_account = google_service_account.app.email
    scaling { max_instance_count = 5 } # plafonne aussi la dépense
    # vpc_access { egress = "ALL_TRAFFIC" ... }  (leçon 2.1)
    containers {
      image = var.app_image
      env {
        name  = "MODEL_ARMOR_TEMPLATE"
        value = google_model_armor_template.assistant.id
      }
    }
  }
}

resource "google_cloud_run_v2_service_iam_member" "iap_invoker" {
  name   = google_cloud_run_v2_service.app.name
  role   = "roles/run.invoker"
  member = local.iap_agent # service-NUMERO@gcp-sa-iap.iam.gserviceaccount.com
}

Le load balancer se compose de six ressources, créées seulement si app_domain est renseigné :

files/app.tf (le backend avec IAP)
resource "google_compute_region_network_endpoint_group" "app" {
  network_endpoint_type = "SERVERLESS"     # un NEG sans serveur pointe vers Cloud Run
  cloud_run { service = google_cloud_run_v2_service.app.name }
}

resource "google_compute_backend_service" "app" {
  load_balancing_scheme = "EXTERNAL_MANAGED"
  backend { group = google_compute_region_network_endpoint_group.app[0].id }
  iap { enabled = true }                   # client OAuth géré par Google
  log_config { enable = true }
}
# + url_map -> target_https_proxy (certificat géré) -> forwarding_rule :443 sur une IP globale

Vous oubliez de donner roles/run.invoker à l'agent IAP. Que voit le salarié autorisé après s'être connecté ?

Le certificat et le DNS

Le certificat géré par Google (google_compute_managed_ssl_certificate) n'est émis qu'une fois que l'enregistrement DNS A de app_domain pointe vers l'IP du load balancer (sortie lb_ip). Comptez de quelques minutes à une heure ; en attendant, le HTTPS échoue, c'est normal.

Pourquoi accorder roles/iap.httpsResourceAccessor à un groupe plutôt qu'à des utilisateurs un par un ?

Exercice

L'application veut connaître l'adresse e-mail de l'utilisateur pour personnaliser les réponses. Comment l'obtenir de façon fiable derrière IAP ?

Voir la solution

IAP ajoute l'en-tête signé x-goog-iap-jwt-assertion. L'application vérifie ce JWT (signature par les clés publiques d'IAP, audience /projects/NUMERO/global/backendServices/ID_BACKEND) puis lit la revendication email :

from google.auth.transport import requests
from google.oauth2 import id_token

def utilisateur(request_headers, audience):
    jwt = request_headers["x-goog-iap-jwt-assertion"]
    claims = id_token.verify_token(jwt, requests.Request(), audience=audience,
                                   certs_url="https://www.gstatic.com/iap/verify/public_key")
    return claims["email"]

L'en-tête non signé x-goog-authenticated-user-email existe aussi, mais il ne faut pas s'y fier seul : la vérification du JWT garantit que la requête vient bien d'IAP.

Résultat obtenu

terraform plan réel le 2026-10-15 avec app_domain=assistant.exemple.fr (domaine fictif ; extrait) :

  # google_cloud_run_v2_service.app will be created
  + resource "google_cloud_run_v2_service" "app" {
      + deletion_protection     = false
      + effective_labels        = {
          + "formation-lab"              = "architecture-ia"
          + "goog-terraform-provisioned" = "true"
        }
      + ingress                 = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
  # google_cloud_run_v2_service_iam_member.iap_invoker[0] will be created
  # google_compute_backend_service.app[0] will be created
  # google_compute_global_address.lb[0] will be created
  # google_compute_global_forwarding_rule.https[0] will be created
  # google_compute_managed_ssl_certificate.app[0] will be created
  # google_compute_region_network_endpoint_group.app[0] will be created
  # google_compute_target_https_proxy.app[0] will be created
  # google_compute_url_map.app[0] will be created
  # google_iap_web_backend_service_iam_member.users["group:assistant@exemple.fr"] will be created

Je sais faire

  • Fermer l'accès direct à Cloud Run avec le bon paramètre d'ingress
  • Assembler un load balancer HTTPS avec NEG sans serveur et certificat géré
  • Activer IAP et donner les deux rôles nécessaires (utilisateurs, agent IAP)
  • Vérifier l'identité de l'utilisateur dans l'application avec le JWT IAP
Next