Authentification de service à service

Si votre architecture utilise plusieurs services, ceux-ci devront probablement communiquer entre eux de manière asynchrone ou synchrone. Plusieurs de ces services peuvent être privés et nécessitent des identifiants d'accès.

Pour une communication asynchrone, vous pouvez utiliser les services Google Cloud suivants :

  • Cloud Tasks pour une communication asynchrone "un à un"
  • Pub/Sub pour une communication asynchrone "un à plusieurs", "un à un" et "plusieurs à un"
  • Cloud Scheduler pour une communication asynchrone planifiée régulièrement
  • Eventarc pour une communication basée sur des événements

Dans tous ces cas, le service utilisé gère l'interaction avec le service destinataire, en fonction de la configuration que vous avez paramétrée.

Pour la communication synchrone, votre service appelle directement un autre service via HTTP en utilisant son URL de point de terminaison. Dans ce cas d'utilisation, vous devez vous assurer que chaque service ne peut envoyer des requêtes qu'à des services spécifiques. Par exemple, si vous avez un service login, il devrait pouvoir accéder au service user-profiles, mais pas au service search.

Dans cette situation, Google vous recommande d'utiliser IAM avec une identité de service basée sur un compte de service géré par l'utilisateur par service bénéficiant d'un ensemble minimal d'autorisations requises pour effectuer son travail.

De plus, la requête doit présenter une preuve de l'identité du service appelant. Pour ce faire, configurez votre service appelant de manière à ajouter un jeton d'ID OpenID Connect signé par Google dans le cadre de la requête.

Configurer le compte de service

Pour configurer un compte de service, vous devez configurer le service destinataire pour accepter les requêtes du service appelant, en faisant du compte de service appelant un compte principal du service destinataire. Ensuite, vous accordez à ce compte de service le rôle de demandeur Cloud Run (roles/run.invoker). Pour effectuer ces deux tâches, suivez les instructions fournies dans l'onglet correspondant :

Console (UI)

  1. Accédez à Google Cloud Console :

    Accéder à Google Cloud Console

  2. Sélectionnez le service destinataire.

  3. Cliquez sur Afficher le panneau d'informations en haut à droite pour afficher l'onglet Autorisations.

  4. Cliquez sur Ajouter un compte principal.

    1. Saisissez l'identité du service appelant. Il s'agit généralement d'une adresse e-mail (PROJECT_NUMBER-compute@developer.gserviceaccount.com par défaut).

    2. Sélectionnez le rôle Cloud Run Invoker dans le menu déroulant Rôle.

    3. Cliquez sur Enregistrer.

gcloud

Exécutez la commande gcloud run services add-iam-policy-binding :

gcloud run services add-iam-policy-binding RECEIVING_SERVICE \
  --member='serviceAccount:CALLING_SERVICE_IDENTITY' \
  --role='roles/run.invoker'

RECEIVING_SERVICE est le nom du service destinataire et CALLING_SERVICE_IDENTITY l'adresse e-mail du compte de service (PROJECT_NUMBER-compute@developer.gserviceaccount.com par défaut).

Terraform

Pour savoir comment appliquer ou supprimer une configuration Terraform, consultez la page Commandes Terraform de base.

Le code Terraform suivant crée un service Cloud Run initial destiné à être public.

resource "google_cloud_run_v2_service" "public" {
  name     = "public-service"
  location = "us-central1"

  deletion_protection = false # set to "true" in production

  template {
    containers {
      # TODO<developer>: replace this with a public service container
      # (This service can be invoked by anyone on the internet)
      image = "us-docker.pkg.dev/cloudrun/container/hello"

      # Include a reference to the private Cloud Run
      # service's URL as an environment variable.
      env {
        name  = "URL"
        value = google_cloud_run_v2_service.private.uri
      }
    }
    # Give the "public" Cloud Run service
    # a service account's identity
    service_account = google_service_account.default.email
  }
}

Remplacez us-docker.pkg.dev/cloudrun/container/hello par une référence à votre image de conteneur.

Le code Terraform suivant rend le service initial public.

data "google_iam_policy" "public" {
  binding {
    role = "roles/run.invoker"
    members = [
      "allUsers",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "public" {
  location = google_cloud_run_v2_service.public.location
  project  = google_cloud_run_v2_service.public.project
  service  = google_cloud_run_v2_service.public.name

  policy_data = data.google_iam_policy.public.policy_data
}

Le code Terraform suivant crée un deuxième service Cloud Run destiné à être privé.

resource "google_cloud_run_v2_service" "private" {
  name     = "private-service"
  location = "us-central1"

  deletion_protection = false # set to "true" in production

  template {
    containers {
      // TODO<developer>: replace this with a private service container
      // (This service should only be invocable by the public service)
      image = "us-docker.pkg.dev/cloudrun/container/hello"
    }
  }
}

Remplacez us-docker.pkg.dev/cloudrun/container/hello par une référence à votre image de conteneur.

Le code Terraform suivant rend le deuxième service privé.

data "google_iam_policy" "private" {
  binding {
    role = "roles/run.invoker"
    members = [
      "serviceAccount:${google_service_account.default.email}",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "private" {
  location = google_cloud_run_v2_service.private.location
  project  = google_cloud_run_v2_service.private.project
  service  = google_cloud_run_v2_service.private.name

  policy_data = data.google_iam_policy.private.policy_data
}

Le code Terraform suivant crée un compte de service.

resource "google_service_account" "default" {
  account_id   = "cloud-run-interservice-id"
  description  = "Identity used by a public Cloud Run service to call private Cloud Run services."
  display_name = "cloud-run-interservice-id"
}

Le code Terraform suivant permet aux services associés au compte de service d'appeler le service Cloud Run privé initial.

data "google_iam_policy" "private" {
  binding {
    role = "roles/run.invoker"
    members = [
      "serviceAccount:${google_service_account.default.email}",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "private" {
  location = google_cloud_run_v2_service.private.location
  project  = google_cloud_run_v2_service.private.project
  service  = google_cloud_run_v2_service.private.name

  policy_data = data.google_iam_policy.private.policy_data
}

Acquérir et configurer le jeton d'ID

Une fois que vous avez attribué le rôle approprié au compte de service appelant, procédez comme suit :

  1. Récupérez un jeton d'ID signé par Google en appliquant l'une des méthodes décrites dans la section suivante. Définissez la revendication d'audience (aud) sur l'URL du service destinataire ou sur une audience personnalisée configurée. Si vous n'utilisez pas d'audience personnalisée, la valeur aud doit rester l'URL du service, même lors de l'envoi de requêtes à un tag de trafic spécifique.

  2. Ajoutez le jeton d'ID récupéré à l'étape précédente à l'un des en-têtes suivants de la requête adressée au service destinataire :

    • Un en-tête Authorization: Bearer ID_TOKEN.
    • Un en-tête X-Serverless-Authorization: Bearer ID_TOKEN. Vous pouvez utiliser cet en-tête si votre application utilise déjà l'en-tête Authorization pour l'autorisation personnalisée. Cette action supprime la signature avant de transmettre le jeton au conteneur utilisateur.

Pour découvrir d'autres moyens d'obtenir un jeton d'ID, consultez la section Méthodes permettant d'obtenir un jeton d'ID.

Utiliser les bibliothèques d'authentification

Le moyen le plus simple et le plus fiable d'acquérir et de configurer le processus lié aux jetons d'ID consiste à utiliser les bibliothèques d'authentification. Ce code fonctionne dans n'importe quel environnement, même en dehors de Google Cloud, dans lequel les bibliothèques sont en mesure d'obtenir des identifiants d'authentification, y compris les environnements compatibles avec la version locale des Identifiants par défaut de l'application. Pour définir les identifiants par défaut de l'application, téléchargez un fichier de clé de compte de service et définissez la variable d'environnement GOOGLE_APPLICATION_CREDENTIALS sur le chemin d'accès au fichier de clé de compte de service. Pour en savoir plus, consultez la page Fonctionnement des identifiants par défaut de l'application.

Ce code ne permet pas d'obtenir des identifiants d'authentification pour un compte utilisateur.

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// Example: https://my-cloud-run-service.run.app/books/delete/12345
// const url = 'https://TARGET_HOSTNAME/TARGET_URL';

// Example (Cloud Run): https://my-cloud-run-service.run.app/
// const targetAudience = 'https://TARGET_AUDIENCE/';

const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();

async function request() {
  console.info(`request ${url} with target audience ${targetAudience}`);
  const client = await auth.getIdTokenClient(targetAudience);

  // Alternatively, one can use `client.idTokenProvider.fetchIdToken`
  // to return the ID Token.
  const res = await client.request({url});
  console.info(res.data);
}

request().catch(err => {
  console.error(err.message);
  process.exitCode = 1;
});

Python

import urllib

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


def make_authorized_get_request(endpoint, audience):
    """
    make_authorized_get_request makes a GET request to the specified HTTP endpoint
    by authenticating with the ID token obtained from the google-auth client library
    using the specified audience value.
    """

    # Cloud Run uses your service's hostname as the `audience` value
    # audience = 'https://my-cloud-run-service.run.app/'
    # For Cloud Run, `endpoint` is the URL (hostname + path) receiving the request
    # endpoint = 'https://my-cloud-run-service.run.app/my/awesome/url'

    req = urllib.request.Request(endpoint)

    auth_req = google.auth.transport.requests.Request()
    id_token = google.oauth2.id_token.fetch_id_token(auth_req, audience)

    req.add_header("Authorization", f"Bearer {id_token}")
    response = urllib.request.urlopen(req)

    return response.read()

Go


import (
	"context"
	"fmt"
	"io"

	"google.golang.org/api/idtoken"
)

// `makeGetRequest` makes a request to the provided `targetURL`
// with an authenticated client using audience `audience`.
func makeGetRequest(w io.Writer, targetURL string, audience string) error {
	// Example `audience` value (Cloud Run): https://my-cloud-run-service.run.app/
	// (`targetURL` and `audience` will differ for non-root URLs and GET parameters)
	ctx := context.Background()

	// client is a http.Client that automatically adds an "Authorization" header
	// to any requests made.
	client, err := idtoken.NewClient(ctx, audience)
	if err != nil {
		return fmt.Errorf("idtoken.NewClient: %w", err)
	}

	resp, err := client.Get(targetURL)
	if err != nil {
		return fmt.Errorf("client.Get: %w", err)
	}
	defer resp.Body.Close()
	if _, err := io.Copy(w, resp.Body); err != nil {
		return fmt.Errorf("io.Copy: %w", err)
	}

	return nil
}

Java

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenCredentials;
import com.google.auth.oauth2.IdTokenProvider;
import java.io.IOException;

public class Authentication {

  // makeGetRequest makes a GET request to the specified Cloud Run or
  // Cloud Functions endpoint `serviceUrl` (must be a complete URL), by
  // authenticating with an ID token retrieved from Application Default
  // Credentials using the specified `audience`.
  //
  // Example `audience` value (Cloud Run): https://my-cloud-run-service.run.app/
  public static HttpResponse makeGetRequest(String serviceUrl, String audience) throws IOException {
    GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
    if (!(credentials instanceof IdTokenProvider)) {
      throw new IllegalArgumentException("Credentials are not an instance of IdTokenProvider.");
    }
    IdTokenCredentials tokenCredential =
        IdTokenCredentials.newBuilder()
            .setIdTokenProvider((IdTokenProvider) credentials)
            .setTargetAudience(audience)
            .build();

    GenericUrl genericUrl = new GenericUrl(serviceUrl);
    HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(tokenCredential);
    HttpTransport transport = new NetHttpTransport();
    HttpRequest request = transport.createRequestFactory(adapter).buildGetRequest(genericUrl);
    return request.execute();
  }
}

Utiliser le serveur de métadonnées

Si, pour une raison quelconque, vous ne pouvez pas utiliser les bibliothèques d'authentification, vous pouvez récupérer un jeton d'ID à partir du serveur de métadonnées Compute lorsque votre conteneur est exécuté sur Cloud Run. Notez que cette méthode ne fonctionne pas en dehors de Google Cloud (cela inclut votre machine locale).

curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=[AUDIENCE]" \
     -H "Metadata-Flavor: Google"

Où AUDIENCE correspond à l'URL du service que vous appelez ou à une audience personnalisée configurée.

Le tableau suivant récapitule les principaux composants d'une requête de métadonnées :

Composants Description
URL racine

Toutes les valeurs de métadonnées sont définies comme des sous-chemins d'accès situés sous l'URL racine suivante :

http://metadata.google.internal/computeMetadata/v1
En-tête de requête

L'en-tête suivant doit figurer dans chaque requête :

Metadata-Flavor: Google

Cet en-tête indique que la requête a été envoyée avec l'intention de récupérer des valeurs de métadonnées, et non involontairement à partir d'une source non sécurisée. De plus, il permet au serveur de métadonnées de renvoyer les données demandées. Si vous ne fournissez pas cet en-tête, le serveur de métadonnées refuse votre requête.

Pour découvrir un tutoriel détaillé portant sur une application utilisant cette technique d'authentification de service à service, suivez le tutoriel sur la sécurisation des services Cloud Run.

Utiliser la fédération d'identité de charge de travail hors de Google Cloud

Si votre environnement utilise un fournisseur d'identité compatible avec la fédération d'identité de charge de travail, vous pouvez utiliser la méthode suivante pour vous authentifier de manière sécurisée auprès de votre service Cloud Run depuis un environnement extérieur à Google Cloud :

  1. Configurez votre compte de service comme décrit dans la section Configurer le compte de service sur cette page.

  2. Configurez la fédération d'identité de charge de travail pour votre fournisseur d'identité, comme décrit dans la section Configurer la fédération d'identité de charge de travail.

  3. Suivez les instructions de la page Autoriser des identités externes à emprunter l'identité d'un compte de service.

  4. Utilisez l'API REST pour acquérir un jeton de courte durée, mais au lieu d'appeler generateAccessToken pour obtenir un jeton d'accès, appelez generateIdToken pour obtenir un jeton d'ID.

    Par exemple, à l'aide de cURL :

    ID_TOKEN=$(curl -0 -X POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/SERVICE_ACCOUNT:generateIdToken \
      -H "Content-Type: text/json; charset=utf-8" \
      -H "Authorization: Bearer $STS_TOKEN" \
      -d @- <&ltEOF | jq -r .token
      {
          "audience": "SERVICE_URL"
      }
    EOF
    )
    echo $ID_TOKEN

    SERVICE_ACCOUNT correspond à l'adresse e-mail du compte de service auquel le pool d'identités de charge de travail est configuré pour l'accès, et SERVICE_URL à l'URL du service Cloud Run que vous appelez. Cette valeur doit rester l'URL du service, même pour les requêtes adressées à un tag de trafic spécifique. $STS_TOKEN correspond au jeton du service de jetons de sécurité que vous avez reçu à l'étape précédente dans les instructions de fédération d'identité de charge de travail.

Vous pouvez inclure le jeton d'ID de l'étape précédente dans la requête adressée au service à l'aide d'un en-tête Authorization: Bearer ID_TOKEN ou X-Serverless-Authorization: Bearer ID_TOKEN. Si les deux en-têtes sont fournis, seul l'en-tête X-Serverless-Authorization est vérifié.

Utiliser une clé de compte de service téléchargée hors de Google Cloud

Si la fédération d'identité de charge de travail n'est pas appropriée pour votre environnement, vous pouvez utiliser une clé de compte de service téléchargée pour vous authentifier depuis un environnement extérieur à Google Cloud. Mettez à jour le code client pour utiliser les bibliothèques d'authentification, comme décrit ci-dessus, avec les Identifiants par défaut de l'application.

Vous pouvez acquérir un jeton d'ID signé par Google à l'aide d'un jeton JWT autosigné, mais cela est assez compliqué et potentiellement source d'erreurs. Les grandes étapes de ce processus sont les suivantes :

  1. Autosignez un jeton JWT de compte de service avec la revendication target_audience définie sur l'URL du service destinataire ou une audience personnalisée configurée. Si vous n'utilisez pas de domaines personnalisés, la valeur target_audience doit rester l'URL du service, même lors de l'envoi de requêtes à un tag de trafic spécifique.

  2. Remplacez le jeton JWT autosigné par un jeton d'ID signé par Google, dont la revendication aud doit correspondre à l'URL précédente.

  3. Incluez le jeton d'ID dans la requête adressée au service à l'aide d'un en-tête Authorization: Bearer ID_TOKEN ou X-Serverless-Authorization: Bearer ID_TOKEN. Si les deux en-têtes sont fournis, seul l'en-tête X-Serverless-Authorization est vérifié.

Recevoir des requêtes authentifiées

Dans le service privé de réception, vous pouvez analyser l'en-tête d'autorisation pour recevoir les informations envoyées par le jeton de support.

Python


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


def receive_authorized_get_request(request):
    """Parse the authorization header and decode the information
    being sent by the Bearer token.

    Args:
        request: Flask request object

    Returns:
        The email from the request's Authorization header.
    """
    auth_header = request.headers.get("Authorization")
    if auth_header:
        # split the auth type and value from the header.
        auth_type, creds = auth_header.split(" ", 1)

        if auth_type.lower() == "bearer":
            claims = id_token.verify_token(creds, requests.Request())
            return f"Hello, {claims['email']}!\n"

        else:
            return f"Unhandled header format ({auth_type}).\n"
    return "Hello, anonymous user.\n"

Étape suivante