Autentica servicio a servicio

Si en tu arquitectura usas varios servicios, es probable que estos deban comunicarse entre sí mediante medios asíncronos o síncronos. Es posible que muchos de estos servicios sean privados y requieran credenciales para el acceso.

Para la comunicación asíncrona, puedes usar los siguientes servicios de Google Cloud:

  • Cloud Tasks para la comunicación asíncrona uno a uno
  • Pub/Sub para una comunicación asíncrona uno a varios, uno a uno y varios a uno
  • Cloud Scheduler para la comunicación asíncrona programada con regularidad
  • Eventarc para la comunicación basada en eventos

En todos estos casos, el servicio que se usa administra la interacción con el servicio de recepción, en función de la configuración que estableciste.

Sin embargo, para la comunicación síncrona, tu servicio llama a otro servicio directamente, a través de HTTP, con su URL de extremo. Para este caso práctico, debes asegurarte de que cada servicio solo pueda realizar solicitudes a servicios específicos. Por ejemplo, si tienes un servicio login, debería poder acceder al servicio user-profiles, pero no al servicio search.

En esta situación, Google recomienda que uses IAM y una identidad de servicio basada en una cuenta de servicio administrada por el usuario de servicio a la que se le otorgó el conjunto mínimo de permisos necesarios para realizar su trabajo.

Además, la solicitud debe presentar un comprobante de la identidad del servicio de llamadas. Para ello, configura el servicio de llamadas a fin de que agregue un token de ID de OpenID Connect firmado por Google como parte de la solicitud.

Configura la cuenta de servicio

A fin de configurar una cuenta de servicio, configura el servicio de recepción para que acepte solicitudes del servicio de llamadas. Para ello, haz que la cuenta de servicio de llamada del miembro sea una principal en el servicio receptor. Luego, debes otorgar a esa cuenta de servicio la función de Invocador de Cloud Run (roles/run.invoker). Para realizar ambas tareas, sigue las instrucciones en la pestaña correspondiente:

IU de Console

  1. Ve a la consola de Google Cloud:

    Ir a la consola de Google Cloud

  2. Selecciona el servicio de recepción.

  3. Haz clic en Mostrar panel de información en la esquina superior derecha para que aparezca la pestaña Permisos.

  4. Haz clic en Agregar principal.

    1. Ingresa la identidad del servicio emisor. Por lo general, es una dirección de correo electrónico predeterminada PROJECT_NUMBER-compute@developer.gserviceaccount.com.

    2. Selecciona la función Cloud Run Invoker en el menú desplegable Selecciona una función.

    3. Haz clic en Guardar.

gcloud

Usa el comando 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'

donde RECEIVING_SERVICE es el nombre del servicio de recepción y CALLING_SERVICE_IDENTITY es la dirección de correo electrónico de la cuenta de servicio, según la configuración predeterminada PROJECT_NUMBER-compute@developer.gserviceaccount.com.

Terraform

Si deseas obtener más información para aplicar o quitar una configuración de Terraform, consulta los comandos básicos de Terraform.

El siguiente código de Terraform crea un servicio inicial de Cloud Run diseñado para ser público.

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

Reemplaza us-docker.pkg.dev/cloudrun/container/hello por una referencia a tu imagen de contenedor.

El siguiente código de Terraform hace que el servicio inicial sea público.

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
}

El siguiente código de Terraform crea un segundo servicio de Cloud Run diseñado para ser privado.

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

Reemplaza us-docker.pkg.dev/cloudrun/container/hello por una referencia a tu imagen de contenedor.

El siguiente código de Terraform hace que el segundo servicio sea privado.

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
}

Mediante el siguiente código de Terraform, se crea una cuenta de servicio.

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

El siguiente código de Terraform permite que los servicios adjuntos a la cuenta de servicio invoquen el servicio inicial privado de Cloud Run.

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
}

Adquiere y configura el token de ID

Después de otorgar el rol adecuado a la cuenta de servicio de llamadas, sigue estos pasos:

  1. Recupera un token de ID firmado por Google mediante uno de los métodos descritos en la siguiente sección. Establece la reclamación del público (aud) en la URL del servicio receptor o en un público personalizado configurado. Si no usas un público personalizado, el valor aud debe permanecer como la URL del servicio, incluso cuando se realizan solicitudes a una etiqueta de tráfico específica.

  2. Agrega el token de ID que recuperaste del paso anterior a uno de los siguientes encabezados en la solicitud al servicio de recepción:

    • Un encabezado Authorization: Bearer ID_TOKEN
    • Un encabezado X-Serverless-Authorization: Bearer ID_TOKEN Puedes usar este encabezado si tu aplicación ya usa el encabezado Authorization para la autorización personalizada. Esto quita la firma antes de pasar el token al contenedor del usuario.

Si deseas conocer otras formas de obtener un token de ID que no se describen en esta página, consulta Métodos para obtener un token de ID.

Usa las bibliotecas de autenticación

La forma más fácil y confiable de adquirir y configurar el proceso de token de ID es usar las bibliotecas de autenticación. Este código funciona en cualquier entorno, incluso fuera de Google Cloud, en el que las bibliotecas pueden obtener credenciales de autenticación para una cuenta de servicio, incluidos los entornos que admiten las credenciales predeterminadas de la aplicación locales. Para configurar las credenciales predeterminadas de la aplicación, descarga un archivo de claves de la cuenta de servicio y configura la variable de entorno GOOGLE_APPLICATION_CREDENTIALS en la ruta del archivo de claves de la cuenta de servicio. Para obtener más información, consulta Cómo funcionan las credenciales predeterminadas de la aplicación.

Este código no funciona para obtener credenciales de autenticación para una cuenta de usuario.

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

Usa el servidor de metadatos

Si, por algún motivo, no puedes usar las bibliotecas de autenticación, puedes recuperar un token de ID del servidor de metadatos de Compute mientras tu contenedor se ejecuta en Cloud Run. . Ten en cuenta que este método no funciona fuera de Google Cloud, incluso desde la máquina local.

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

Donde AUDIENCE es la URL del servicio que estás invocando o un público personalizado configurado.

En la siguiente tabla, se resumen las partes principales de una solicitud de consulta de metadatos:

Componentes Descripción
URL raíz

Todos los valores de metadatos se definen como subrutas de la siguiente URL raíz:

http://metadata.google.internal/computeMetadata/v1
Encabezado de la solicitud

El siguiente encabezado debe estar en cada solicitud:

Metadata-Flavor: Google

Este encabezado indica que la solicitud se envió con la intención de recuperar valores de metadatos, en lugar de hacerlo de forma involuntaria desde una fuente insegura, y permite que el servidor de metadatos muestre los datos que solicitaste. Si no proporcionas este encabezado, el servidor de metadatos rechaza tu solicitud.

Para obtener una explicación completa de una aplicación que use esta técnica de autenticación de servicio a servicio, sigue el instructivo sobre cómo proteger los servicios de Cloud Run.

Usa la federación de Workload Identity desde fuera de Google Cloud

Si tu entorno usa un proveedor de identidad compatible con la federación de Workload Identity, puedes usar el siguiente método para autenticarte de forma segura en el servicio de Cloud Run desde fuera de Google Cloud:

  1. Configura tu cuenta de servicio como se describe en Configura la cuenta de servicio en esta página.

  2. Configura la federación de Workload Identity para tu proveedor de identidad como se describe en Configura la federación de Workload Identity.

  3. Sigue las instrucciones en Otorga permisos a identidades externas para actuar en nombre de una cuenta de servicio.

  4. Usa la API de REST para adquirir un token de corta duración, pero en lugar de llamar a generateAccessToken a fin de obtener un token de acceso, llama a generateIdToken para obtener un token de ID.

    Por ejemplo, con 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 es la dirección de correo electrónico de la cuenta de servicio a la que está configurado el grupo de identidades de carga de trabajo, y SERVICE_URL es la URL del servicio de Cloud Run que invocas. Este valor debe seguir siendo la URL del servicio, incluso cuando se realizan solicitudes a una etiqueta de tráfico específica. $STS_TOKEN es el token de servicio del token de seguridad que recibiste en el paso anterior en las instrucciones de federación de Workload Identity.

Puedes incluir el token de ID del paso anterior en la solicitud al servicio mediante un encabezado Authorization: Bearer ID_TOKEN o X-Serverless-Authorization: Bearer ID_TOKEN. Si se proporcionan ambos encabezados, solo se verifica el encabezado X-Serverless-Authorization.

Usa una clave de cuenta de servicio descargada desde fuera de Google Cloud

Si la federación de Workload Identity no es adecuada para tu entorno, puedes usar una clave de cuenta de servicio descargada para autenticarte desde fuera de Google Cloud. Actualiza el código de cliente para usar las bibliotecas de autenticación como se describió anteriormente con las credenciales predeterminadas de la aplicación.

Puedes adquirir un token de ID firmado por Google con un JWT autofirmado, pero esto es bastante complicado y potencialmente propenso a errores. Los pasos básicos son los siguientes:

  1. Autofirma un JWT de cuenta de servicio con la reclamación target_audience configurada como la URL del servicio receptor o un público personalizado configurado. Si no usas dominios personalizados, el valor de target_audience debe seguir siendo la URL del servicio, incluso cuando se realizan solicitudes a una etiqueta de tráfico específica.

  2. Intercambia el JWT autofirmado por un token de ID firmado por Google, que debería tener la reclamación aud configurada en la URL anterior.

  3. Incluye el token de ID en la solicitud al servicio mediante un encabezado Authorization: Bearer ID_TOKEN o X-Serverless-Authorization: Bearer ID_TOKEN. Si se proporcionan ambos encabezados, solo se verifica el encabezado X-Serverless-Authorization.

Recibe solicitudes autenticadas

Dentro del servicio privado receptor, puedes analizar el encabezado de autorización para recibir la información que envía el token del portador.

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"

¿Qué sigue?