Autenticación entre servicios

Si tu arquitectura usa varios servicios, es probable que estos servicios tengan que comunicarse entre sí mediante métodos asíncronos o síncronos. Muchos de estos servicios pueden ser privados y, por lo tanto, requieren credenciales para acceder.

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

  • Cloud Tasks para la comunicación asíncrona individual
  • Pub/Sub para la comunicación asíncrona de uno a muchos, de uno a uno y de muchos a uno
  • Cloud Scheduler para la comunicación asíncrona programada periódicamente
  • Eventarc para la comunicación basada en eventos

En todos estos casos, el servicio utilizado gestiona la interacción con el servicio receptor en función de la configuración que hayas definido.

Sin embargo, en la comunicación síncrona, tu servicio llama a otro servicio directamente a través de HTTP mediante su URL de endpoint. En este caso práctico, debes asegurarte de que cada servicio solo pueda enviar 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 gestionada por el usuario por servicio a la que se le haya concedido el conjunto mínimo de permisos necesarios para hacer su trabajo.

Además, la solicitud debe presentar una prueba de la identidad del servicio que llama. Para ello, configura tu servicio de llamadas para que añada un token de ID de OpenID Connect firmado por Google como parte de la solicitud.

Configurar la cuenta de servicio

.

Para configurar una cuenta de servicio, debes configurar el servicio receptor para que acepte solicitudes del servicio llamante. Para ello, convierte la cuenta de servicio del servicio llamante en un principal en el servicio receptor. A continuación, asigna a esa cuenta de servicio el rol Invocador de Cloud Run (roles/run.invoker). Para llevar a cabo ambas tareas, sigue las instrucciones de la pestaña correspondiente:

Interfaz de usuario de la consola

  1. Ve a la Google Cloud consola:

    Ir a la Google Cloud consola

  2. Selecciona el servicio receptor.

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

  4. Haz clic en Añadir principal.

    1. Introduce la identidad del servicio que llama. Normalmente, se trata de una dirección de correo electrónico. De forma predeterminada, es PROJECT_NUMBER-compute@developer.gserviceaccount.com.

    2. Selecciona el rol Cloud Run Invoker en el menú desplegable Selecciona un rol.

    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 receptor y CALLING_SERVICE_IDENTITY es la dirección de correo de la cuenta de servicio, que es PROJECT_NUMBER-compute@developer.gserviceaccount.com de forma predeterminada.

Terraform

Para saber cómo aplicar o quitar una configuración de Terraform, consulta Comandos básicos de Terraform.

Añade lo siguiente a un recurso google_cloud_run_v2_service en tu configuración de Terraform:

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

Sustituye 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 que está 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"
    }
  }
}

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

El siguiente código de Terraform 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 vinculados a la cuenta de servicio invoquen el servicio de Cloud Run privado inicial.

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
}

Obtener y configurar el token de ID

Después de asignar el rol adecuado a la cuenta de servicio que hace la llamada, sigue estos pasos:

  1. Obtén un token de ID firmado por Google mediante uno de los métodos descritos en la siguiente sección. Define la reclamación de audiencia (aud) en la URL del servicio receptor o en una audiencia personalizada configurada. Si no utiliza una audiencia personalizada, el valor de aud debe seguir siendo la URL del servicio, incluso cuando se hagan solicitudes a una etiqueta de tráfico específica.

  2. Añade el token de ID que has obtenido en el paso anterior a uno de los siguientes encabezados de la solicitud al servicio receptor:

    • 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. De esta forma, se elimina la firma antes de pasar el token al contenedor del usuario.

Para ver 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.

Usar las bibliotecas de autenticación

Una forma de obtener y configurar el proceso del token de ID es usar las bibliotecas de autenticación. Este código funciona en cualquier entorno, incluso fuera de Google Cloud, donde las bibliotecas pueden obtener credenciales de autenticación para una cuenta de servicio. Para usar este método, descarga un archivo de clave de cuenta de servicio y define la variable de entorno GOOGLE_APPLICATION_CREDENTIALS en la ruta del archivo de clave de cuenta de servicio. Para obtener más información, consulta el artículo sobre las claves de cuentas de servicio.

Este código no acepta 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.fetch(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();
  }
}

Usar el servidor de metadatos

Si por algún motivo no puedes usar las bibliotecas de autenticación, puedes obtener 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, ni siquiera desde tu 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 una audiencia personalizada configurada.

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 solicitud

El siguiente encabezado debe estar en cada solicitud:

Metadata-Flavor: Google

Este encabezado indica que la solicitud se ha enviado para recuperar valores de metadatos y que no proviene de una fuente no segura desde la que se ha enviado involuntariamente. Además, permite que el servidor de metadatos devuelva los datos que has solicitado. Si no proporcionas este encabezado, el servidor de metadatos denegará tu solicitud.

Para ver un tutorial completo sobre una aplicación que usa esta técnica de autenticación entre servicios, consulta el tutorial sobre cómo proteger servicios de Cloud Run.

Usar la federación de identidades de cargas de trabajo desde fuera Google Cloud

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

  1. Configura tu cuenta de servicio tal como se describe en la sección Configurar la cuenta de servicio de esta página.

  2. Configura la federación de identidades de cargas de trabajo para tu proveedor de identidades tal como se describe en el artículo Configurar la federación de identidades de cargas de trabajo.

  3. Sigue las instrucciones que se indican en el artículo Conceder permiso a identidades externas para suplantar la identidad de una cuenta de servicio.

  4. Usa la API REST para obtener un token de corta duración, pero, en lugar de llamar a generateAccessToken para 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 @- <<EOF | jq -r .token
      {
          "audience": "SERVICE_URL"
      }
    EOF
    )
    echo $ID_TOKEN

    Donde SERVICE_ACCOUNT es la dirección de correo de la cuenta de servicio a la que se ha configurado el grupo de identidades de carga de trabajo para acceder y SERVICE_URL es la URL del servicio de Cloud Run que estás invocando. Este valor debe seguir siendo la URL del servicio, incluso cuando se hagan solicitudes a una etiqueta de tráfico específica. $STS_TOKEN es el token del servicio de tokens de seguridad que has recibido en el paso anterior de las instrucciones de federación de identidades de carga de trabajo.

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

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

Si la federación de identidades de carga de trabajo no es adecuada para tu entorno, puedes usar una clave de cuenta de servicio descargada para autenticarte desde fuera deGoogle Cloud. Actualiza el código de tu cliente para usar las bibliotecas de autenticación tal como se ha descrito anteriormente. Para obtener más información, consulta el artículo sobre las claves de cuentas de servicio.

Puedes obtener un token de ID firmado por Google mediante un JWT autofirmado, pero este proceso es bastante complicado y puede dar lugar a errores. Los pasos básicos son los siguientes:

  1. Autofirma un JWT de cuenta de servicio con la reclamación target_audience definida en la URL del servicio receptor o en una audiencia personalizada configurada. Si no se usan dominios personalizados, el valor de target_audience debe seguir siendo la URL del servicio, incluso cuando se hagan solicitudes a una etiqueta de tráfico específica.

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

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

Recibir solicitudes autenticadas

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

Python

from flask import Request

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


def receive_request_and_parse_auth_header(request: Request) -> str:
    """Parse the authorization header, validate the Bearer token
    and decode the token to get its information.

    Args:
        request: Flask request object.

    Returns:
        One of the following:
        a) The email from the request's Authorization header.
        b) A welcome message for anonymous users.
        c) An error description.
    """
    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":
            # Find more information about `verify_token` function here:
            # https://google-auth.readthedocs.io/en/master/reference/google.oauth2.id_token.html#google.oauth2.id_token.verify_token
            try:
                decoded_token = id_token.verify_token(creds, requests.Request())
                return f"Hello, {decoded_token['email']}!\n"
            except GoogleAuthError as e:
                return f"Invalid token: {e}\n"
        else:
            return f"Unhandled header format ({auth_type}).\n"

    return "Hello, anonymous user.\n"

Siguientes pasos