Dienst-zu-Dienst authentifizieren

Wenn Ihre Architektur mehrere Dienste verwendet, müssen diese Dienste entweder asynchron oder synchron miteinander kommunizieren. Viele dieser Dienste sind möglicherweise privat und erfordern daher Anmeldedaten für den Zugriff.

Für die asynchrone Kommunikation können Sie die folgenden Google Cloud-Dienste verwenden:

  • Cloud Tasks für eine asynchrone 1:1-Kommunikation
  • Pub/Sub für eine asynchrone 1:n-, 1:1- und n:1-Kommunikation
  • Cloud Scheduler für eine regelmäßig geplante asynchrone Kommunikation
  • Eventarc für die ereignisbasierte Kommunikation

In allen diesen Fällen verwaltet der verwendete Dienst die Interaktion mit dem empfangenden Dienst basierend auf der von Ihnen eingerichteten Konfiguration.

Für die synchrone Kommunikation ruft Ihr Dienst jedoch einen anderen Dienst direkt über HTTP mit seiner Endpunkt-URL auf. Für diesen Anwendungsfall sollten Sie darauf achten, dass jeder Dienst nur Anfragen an bestimmte Dienste senden kann. Wenn Sie zum Beispiel einen login-Dienst haben, sollte dieser auf den user-profiles-Dienst, aber nicht auf den search-Dienst zugreifen können.

In diesem Fall empfiehlt Google, dass Sie IAM und eine Dienstidentität verwenden, die auf einem nutzerverwalteten Dienstkonto pro Dienst basiert, dem die minimalen Berechtigungen gewährt wurden, die für die Ausführung seiner Arbeit erforderlich sind.

Außerdem muss die Anfrage einen Nachweis der Identität des aufrufenden Dienstes liefern. Konfigurieren Sie dazu Ihren Aufrufdienst so, dass er ein von Google signiertes OpenID Connect-ID-Token als Teil der Anfrage hinzufügt.

Dienstkonto einrichten

Wenn Sie ein Dienstkonto einrichten möchten, konfigurieren Sie den empfangenden Dienst so, dass Anfragen vom aufrufenden Dienst akzeptiert werden. Legen Sie dazu das Dienstkonto des aufrufenden Dienstes als Hauptkonto im empfangenden Dienst fest. Anschließend gewähren Sie dem Dienstkonto die Rolle „Cloud Run-Aufrufer” (roles/run.invoker). Folgen Sie den Anleitungen auf dem entsprechenden Tab, um beide Aufgaben zu erledigen:

Console-UI

  1. Öffnen Sie die Google Cloud Console:

    Zur Google Cloud Console

  2. Wählen Sie den empfangenden Dienst aus.

  3. Klicken Sie oben rechts auf Infofeld ansehen, um den Tab Berechtigungen aufzurufen.

  4. Klicken Sie auf Hauptkonto hinzufügen.

    1. Geben Sie die Identität des aufrufenden Dienstes ein. Dies ist normalerweise eine E-Mail-Adresse, standardmäßig PROJECT_NUMBER-compute@developer.gserviceaccount.com.

    2. Wählen Sie im Drop-down-Menü Rolle auswählen die Rolle Cloud Run Invoker aus.

    3. Klicken Sie auf Speichern.

gcloud

Führen Sie den Befehl gcloud run services add-iam-policy-binding aus:

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

Dabei ist RECEIVING_SERVICE der Name des empfangenden Dienstes und CALLING_SERVICE_IDENTITY die E-Mail-Adresse des Dienstkontos, standardmäßig PROJECT_NUMBER-compute@developer.gserviceaccount.com.

Terraform

Informationen zum Anwenden oder Entfernen einer Terraform-Konfiguration finden Sie unter Grundlegende Terraform-Befehle.

Mit dem folgenden Terraform-Code wird ein anfänglicher Cloud Run-Dienst erstellt, der öffentlich sein soll.

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

  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
  }
}

Ersetzen Sie us-docker.pkg.dev/cloudrun/container/hello durch einen Verweis auf Ihr Container-Image.

Mit folgendem Terraform-Code wird der anfängliche Dienst öffentlich.

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
}

Mit dem folgenden Terraform-Code wird ein zweiter Cloud Run-Dienst erstellt, der privat sein soll.

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

  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"
    }
  }
}

Ersetzen Sie us-docker.pkg.dev/cloudrun/container/hello durch einen Verweis auf Ihr Container-Image.

Mit dem folgenden Terraform-Code wird der zweite Dienst privat.

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
}

Mit dem folgenden Terraform-Code wird ein Dienstkonto erstellt.

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"
}

Mit dem folgenden Terraform-Code können Dienste, die an das Dienstkonto angehängt sind, den ursprünglichen privaten Cloud Run-Dienst aufrufen.

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
}

ID-Token abrufen und konfigurieren

Nachdem Sie dem aufrufenden Dienstkonto die entsprechende Rolle zugewiesen haben, führen Sie die folgenden Schritte aus:

  1. Rufen Sie ein von Google signiertes ID-Token mit einer der im folgenden Abschnitt beschriebenen Methoden ab. Legen Sie die Zielgruppenanforderung (aud) auf die URL des empfangenden Dienstes oder eine konfigurierte benutzerdefinierte Zielgruppe fest. Wenn Sie keine benutzerdefinierte Zielgruppe verwenden, muss der Wert aud als URL des Dienstes bleiben, auch wenn Sie Anfragen an ein bestimmtes Traffic-Tag senden.

  2. Fügen Sie das ID-Token, das Sie im vorherigen Schritt abgerufen haben, in einen der folgenden Header in der Anfrage an den empfangenden Dienst ein:

    • Ein Authorization: Bearer ID_TOKEN-Header.
    • Ein X-Serverless-Authorization: Bearer ID_TOKEN-Header. Sie können diesen Header verwenden, wenn Ihre Anwendung bereits den Header Authorization für die benutzerdefinierte Autorisierung verwendet. Dadurch wird die Signatur entfernt, bevor das Token an den Nutzercontainer übergeben wird.

Weitere Möglichkeiten zum Abrufen eines ID-Tokens, das auf dieser Seite nicht beschrieben wird, finden Sie unter Methoden zum Abrufen eines ID-Tokens.

Authentifizierungsbibliotheken verwenden

Die einfachste und zuverlässigste Methode für die Verwaltung dieses Prozesses ist die Verwendung der unten aufgeführten Authentifizierungsbibliotheken, um dieses Token zu generieren und zu nutzen. Dieser Code funktioniert in jeder Umgebung, auch außerhalb von Google Cloud, in der die Bibliotheken Anmeldedaten zur Authentifizierung für ein Dienstkonto abrufen können, einschließlich Umgebungen, die lokale Standardanmeldedaten für Anwendungen unterstützen. Der Code funktioniert jedoch nicht zum Abrufen von Authentifizierungsdaten für ein Nutzerkonto.

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()

Einfach loslegen (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();
  }
}

Metadatenserver verwenden

Wenn Sie die Authentifizierungsbibliotheken nicht verwenden können, können Sie ein ID-Token vom Compute-Metadatenserver abrufen, während Ihr Container in Cloud Run ausgeführt wird. Beachten Sie, dass diese Methode nicht außerhalb von Google Cloud funktioniert, auch von Ihrem lokalen Computer.

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

Dabei ist ZIELGRUPPE die URL des Dienstes, den Sie aufrufen, oder eine konfigurierte benutzerdefinierte Zielgruppe.

In der folgenden Tabelle sind die wichtigsten Teile einer Metadatenabfrage zusammengefasst:

Komponenten Beschreibung
Stamm-URL

Alle Metadatenwerte werden als Subpfade unterhalb der folgenden Stamm-URL definiert:


http://metadata.google.internal/computeMetadata/v1
Anfrage-Header

Jede Anfrage muss den folgenden Header enthalten:


Metadata-Flavor: Google

Dieser Header weist darauf hin, dass eine Anfrage zum Aufruf von Metadatenwerten gesendet wurde und diese nicht unbeabsichtigt aus einer unsicheren Quelle kommen. Auf diese Weise kann der Metadatenserver Ihnen die entsprechenden Daten zurückgeben. Ohne diesen Header lehnt der Metadatenserver Ihre Anfrage ab.

Eine Schritt-für-Schritt-Anleitung einer Anwendung, die die Dienst-zu-Dienst-Authentifizierungsmethode verwendet, finden Sie in der Anleitung zum Sichern von Cloud Run-Diensten.

Workload Identity-Föderation von außerhalb von Google Cloud verwenden

Wenn Ihre Umgebung einen Identitätsanbieter verwendet, der von der Workload Identity-Föderation unterstützt wird, können Sie sich mit der folgenden Methode sicher von außerhalb von Google Cloud bei Ihrem Cloud Run-Dienst authentifizieren:

  1. Richten Sie Ihr Dienstkonto wie auf dieser Seite beschrieben unter Dienstkonto einrichten ein.

  2. Konfigurieren Sie die Workload Identity-Föderation für Ihren Identitätsanbieter, wie unter Workload Identity-Föderation konfigurieren beschrieben.

  3. Folgen Sie der Anleitung unter Externe Identitäten die Berechtigung erteilen, die Identität eines Dienstkontos zu übernehmen.

  4. Verwenden Sie die REST API, um ein kurzlebiges Token zu erhalten. Rufen Sie jedoch generateAccessToken auf, um ein Zugriffstoken abzurufen. generateIdToken abrufen, um ein ID-Token zu erhalten.

    Zum Beispiel mit 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 @- <<EOF | jq -r .token
      {
          "audience": "SERVICE_URL"
      }
    EOF
    )
    echo $ID_TOKEN
    

    Dabei ist SERVICE_ACCOUNT die E-Mail-Adresse des Dienstkontos, für das der Workload Identity-Pool konfiguriert ist, und SERVICE_URL die URL des Cloud Run-Dienstes. Sie rufen auf. Der Wert sollte die URL des Dienstes bleiben, auch wenn Sie Anfragen an ein bestimmtes Traffic-Tag senden. $STS_TOKEN ist das Token des Security Token Service, das Sie im vorherigen Schritt in der Anleitung zur Föderation von Workload Identity erhalten haben.

Sie können das ID-Token aus dem vorherigen Schritt in die Anfrage an den Dienst einfügen. Verwenden Sie dazu einen Authorization: Bearer ID_TOKEN-Header oder einen X-Serverless-Authorization: Bearer ID_TOKEN-Header. Wenn beide Header angegeben sind, wird nur der Header X-Serverless-Authorization aktiviert.

Heruntergeladene Dienstkontoschlüssel von außerhalb von Google Cloud verwenden

Wenn die Workload Identity-Föderation für Ihre Umgebung nicht geeignet ist, können Sie sich von außerhalb von Google Cloud mit einem heruntergeladenen Dienstkontoschlüssel authentifizieren. Aktualisieren Sie Ihren Clientcode so, dass die Authentifizierungsbibliotheken wie oben beschrieben mit Standardanmeldedaten für Anwendungen verwendet werden.

Sie können ein von Google signiertes ID-Token mit einem selbst signierten JWT abrufen. Dies ist jedoch kompliziert und möglicherweise fehleranfällig. Die grundlegenden Schritte sind:

  1. Signieren Sie ein Dienstkonto-JWT mit der Anforderung target_audience, die auf die URL des empfangenden Dienstes oder eine konfigurierte benutzerdefinierte Zielgruppe festgelegt ist. Wenn Sie keine benutzerdefinierten Domains verwenden, sollte der Wert target_audience als URL des Dienstes bleiben, auch wenn Sie Anfragen an ein bestimmtes Traffic-Tag senden.

  2. Tauschen Sie das selbst signierte JWT gegen ein von Google signiertes ID-Token aus, bei dem die Anforderung aud auf die oben verwendete URL festgelegt ist.

  3. Fügen Sie das ID-Token in der Anfrage an den Dienst mit einem Authorization: Bearer ID_TOKEN-Header oder einem X-Serverless-Authorization: Bearer ID_TOKEN-Header ein. Wenn beide Header angegeben sind, wird nur der Header X-Serverless-Authorization aktiviert.

In diesem Cloud Functions-Beispiel finden Sie ein Beispiel der oben genannten Schritte.

Authentifizierte Anfragen empfangen

Innerhalb des privaten empfangenden Dienstes können Sie den Autorisierungsheader parsen, um die vom Bearer-Token gesendeten Informationen zu empfangen.

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"