Autenticação de serviço para serviço

Se a sua arquitetura estiver a usar vários serviços, é provável que estes serviços tenham de comunicar entre si através de meios assíncronos ou síncronos. Muitos destes serviços podem ser privados e, por isso, requerem credenciais para acesso.

Para a comunicação assíncrona, pode usar os seguintes Google Cloud serviços:

  • Cloud Tasks para comunicação assíncrona individual
  • Pub/Sub para comunicação assíncrona de um para muitos, um para um e muitos para um
  • Cloud Scheduler para comunicação assíncrona agendada regularmente
  • Eventarc para comunicação baseada em eventos

Em todos estes casos, o serviço usado gere a interação com o serviço de receção, com base na configuração que configurou.

No entanto, para a comunicação síncrona, o seu serviço chama outro serviço diretamente, através de HTTP, usando o URL do respetivo ponto final. Para este exemplo de utilização, deve certificar-se de que cada serviço só pode fazer pedidos a serviços específicos. Por exemplo, se tiver um serviço login, este deve poder aceder ao serviço user-profiles, mas não ao serviço search.

Nesta situação, a Google recomenda que use o IAM e uma identidade de serviço baseada numa conta de serviço gerida pelo utilizador por serviço à qual foi concedido o conjunto mínimo de autorizações necessárias para fazer o seu trabalho.

Além disso, o pedido tem de apresentar um comprovativo da identidade do serviço de chamadas. Para tal, configure o seu serviço de chamadas para adicionar uma chave de acesso OpenID Connect assinada pela Google como parte do pedido.

Configure a conta de serviço

Para configurar uma conta de serviço, configura o serviço de receção para aceitar pedidos do serviço de chamada, tornando a conta de serviço do serviço de chamada num principal no serviço de receção. Em seguida, concede à conta de serviço a função de invocador do Cloud Run (roles/run.invoker). Para realizar ambas as tarefas, siga as instruções no separador adequado:

IU da Play Console

  1. Aceda à Google Cloud consola:

    Aceda à Google Cloud consola

  2. Selecione o serviço de receção.

  3. Clique em Mostrar painel de informações no canto superior direito para mostrar o separador Autorizações.

  4. Clique em Adicionar principal.

    1. Introduza a identidade do serviço de chamadas. Normalmente, trata-se de um endereço de email, por predefinição PROJECT_NUMBER-compute@developer.gserviceaccount.com.

    2. Selecione a função Cloud Run Invoker no menu pendente Selecionar uma função.

    3. Clique em Guardar.

gcloud

Use o 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'

em que RECEIVING_SERVICE é o nome do serviço de receção e CALLING_SERVICE_IDENTITY é o endereço de email da conta de serviço, por predefinição PROJECT_NUMBER-compute@developer.gserviceaccount.com.

Terraform

Para saber como aplicar ou remover uma configuração do Terraform, consulte os comandos básicos do Terraform.

Adicione o seguinte a um recurso google_cloud_run_v2_service na sua configuração do 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
  }
}

Substitua us-docker.pkg.dev/cloudrun/container/hello por uma referência à imagem do contentor.

O código do Terraform seguinte torna o serviço inicial 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
}

O código Terraform seguinte cria um segundo serviço do Cloud Run destinado a 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"
    }
  }
}

Substitua us-docker.pkg.dev/cloudrun/container/hello por uma referência à imagem do contentor.

O código Terraform seguinte torna o segundo serviço 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
}

O seguinte código Terraform cria uma conta de serviço.

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

O seguinte código do Terraform permite que os serviços anexados à conta de serviço invoquem o serviço do 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
}

Adquira e configure o token de ID

Depois de conceder a função adequada à conta de serviço de chamadas, siga estes passos:

  1. Obtenha um token de ID assinado pela Google através de um dos métodos descritos na secção seguinte. Defina a reivindicação de público-alvo (aud) para o URL do serviço de receção ou um público-alvo personalizado configurado. Se não estiver a usar um público-alvo personalizado, o valor aud tem de permanecer como o URL do serviço, mesmo quando faz pedidos a uma etiqueta de tráfego específica.

  2. Adicione o token de ID obtido no passo anterior a um dos seguintes cabeçalhos no pedido ao serviço de receção:

    • Um cabeçalho Authorization: Bearer ID_TOKEN.
    • Um cabeçalho X-Serverless-Authorization: Bearer ID_TOKEN. Pode usar este cabeçalho se a sua aplicação já usar o cabeçalho Authorization para autorização personalizada. Isto remove a assinatura antes de transmitir o token ao contentor do utilizador.

Para outras formas de obter um token de ID que não estão descritas nesta página, consulte os Métodos para obter um token de ID.

Use as bibliotecas de autenticação

Uma forma de adquirir e configurar o processo de token de ID é usar as bibliotecas de autenticação. Este código funciona em qualquer ambiente, mesmo fora do Google Cloud, onde as bibliotecas podem obter credenciais de autenticação para uma conta de serviço. Para usar este método, transfira um ficheiro de chave da conta de serviço e defina a variável de ambiente GOOGLE_APPLICATION_CREDENTIALS para o caminho do ficheiro de chave da conta de serviço. Para mais informações, consulte o artigo sobre a chave da conta de serviço.

Este código não aceita credenciais de autenticação para uma conta de utilizador.

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

Ir


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

Use o servidor de metadados

Se, por algum motivo, não conseguir usar as bibliotecas de autenticação, pode obter um token de ID do servidor de metadados do Compute enquanto o contentor estiver em execução no Cloud Run. Tenha em atenção que este método não funciona fora do Google Cloud, incluindo a partir do seu computador local.

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

Onde AUDIENCE é o URL do serviço que está a invocar ou um público-alvo personalizado configurado.

A tabela seguinte resume as partes principais de um pedido de consulta de metadados:

Componentes Descrição
URL raiz

Todos os valores de metadados são definidos como subcaminhos abaixo do seguinte URL raiz:

http://metadata.google.internal/computeMetadata/v1
Cabeçalho do pedido

O seguinte cabeçalho tem de estar em cada pedido:

Metadata-Flavor: Google

Este cabeçalho indica que o pedido foi enviado com a intenção de obter valores de metadados, em vez de o fazer involuntariamente a partir de uma fonte insegura, e permite que o servidor de metadados devolva os dados que solicitou. Se não fornecer este cabeçalho, o servidor de metadados nega o seu pedido.

Para ver um exemplo completo de uma aplicação que usa esta técnica de autenticação de serviço para serviço, siga o tutorial sobre como proteger serviços do Cloud Run.

Use a federação de identidades de cargas de trabalho a partir do exterior Google Cloud

Se o seu ambiente usar um fornecedor de identidade suportado pela federação de identidades de cargas de trabalho, pode usar o seguinte método para se autenticar de forma segura no seu serviço do Cloud Run a partir de fora do Google Cloud:

  1. Configure a sua conta de serviço conforme descrito em Configure a conta de serviço nesta página.

  2. Configure a federação de identidade da carga de trabalho para o seu fornecedor de identidade, conforme descrito no artigo Configurar a federação de identidade da carga de trabalho.

  3. Siga as instruções em Conceder autorização a identidades externas para usar a identidade de uma conta de serviço.

  4. Use a API REST para adquirir um token de curta duração, mas, em vez de chamar generateAccessToken para obter um token de acesso, chame generateIdToken para receber um token de ID.

    Por exemplo, usando o 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

    Onde SERVICE_ACCOUNT é o endereço de email da conta de serviço que o Workload Identity Pool está configurado para aceder, e SERVICE_URL é o URL do serviço do Cloud Run que está a invocar. Este valor deve permanecer como o URL do serviço, mesmo quando fizer pedidos a uma etiqueta de tráfego específica. $STS_TOKEN é o token do serviço de tokens de segurança que recebeu no passo anterior nas instruções da federação de identidades da carga de trabalho.

Pode incluir o token de ID do passo anterior no pedido ao serviço através de um cabeçalho Authorization: Bearer ID_TOKEN ou um cabeçalho X-Serverless-Authorization: Bearer ID_TOKEN. Se forem fornecidos ambos os cabeçalhos, apenas o cabeçalho X-Serverless-Authorization é verificado.

Use uma chave de conta de serviço transferida de fora do Google Cloud

Se a Workload Identity Federation não for adequada para o seu ambiente, pode usar uma chave de conta de serviço transferida para fazer a autenticação a partir de fora doGoogle Cloud. Atualize o código do cliente para usar as bibliotecas de autenticação, conforme descrito anteriormente. Para mais informações, consulte o artigo sobre a chave da conta de serviço.

Pode adquirir um token de ID assinado pela Google através de um JWT autoassinado, mas este processo é bastante complicado e potencialmente propenso a erros. Os passos básicos são os seguintes:

  1. Assine automaticamente um JWT de conta de serviço com a reivindicação target_audience definida para o URL do serviço de receção ou um público-alvo personalizado configurado. Se não usar domínios personalizados, o valor target_audience deve permanecer como o URL do serviço, mesmo quando fizer pedidos a uma etiqueta de tráfego específica.

  2. Troque o JWT autoassinado por um token de ID assinado pela Google, que deve ter a reivindicação aud definida para o URL anterior.

  3. Inclua o token de ID no pedido ao serviço através de um cabeçalho Authorization: Bearer ID_TOKEN ou um cabeçalho X-Serverless-Authorization: Bearer ID_TOKEN. Se ambos os cabeçalhos forem fornecidos, apenas o cabeçalho X-Serverless-Authorization é verificado.

Receba pedidos autenticados

No serviço privado de receção, pode analisar o cabeçalho de autorização para receber as informações enviadas pelo token Bearer.

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"

O que se segue?