Authenticating service-to-service

If your architecture is using multiple services, these services likely need to communicate with each other, using either asynchronous or synchronous means. Many of these services may be private and therefore require credentials for access.

For asynchronous communication, you can use the following Google Cloud services:

  • Cloud Tasks for one to one asynchronous communication
  • Pub/Sub for one to many, one to one, and many to one asynchronous communication
  • Cloud Scheduler for regularly scheduled asynchronous communication
  • Eventarc for event-based communication

In all of these cases, the service used manages the interaction with the receiving service, based on the configuration you set up.

But for synchronous communication, your service calls another service directly, over HTTP, using its endpoint URL. For this use case, you should make sure that each service is only able to make requests to specific services. For example, if you have a login service, it should be able to access the user-profiles service, but not the search service.

In this situation, Google recommends that you use IAM and a service identity based on a per-service user-managed service account that has been granted the minimum set of permissions required to do its work.

In addition, the request must present proof of the calling service's identity. To do this, configure your calling service to add a Google-signed OpenID Connect ID token as part of the request.

Set up the service account

To set up a service account, you configure the receiving service to accept requests from the calling service by making the calling service's service account a principal on the receiving service. Then you grant that service account the Cloud Run Invoker (roles/run.invoker) role. To do both of these tasks, follow the instructions in the appropriate tab:

Console UI

  1. Go to the Google Cloud console:

    Go to Google Cloud console

  2. Select the receiving service.

  3. Click Show Info Panel in the top right corner to show the Permissions tab.

  4. Click Add principal.

    1. Enter the identity of the calling service. This is usually an email address, by default PROJECT_NUMBER-compute@developer.gserviceaccount.com.

    2. Select the Cloud Run Invoker role from the Select a role drop-down menu.

    3. Click Save.

gcloud

Use the gcloud run services add-iam-policy-binding command:

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

where RECEIVING_SERVICE is the name of the receiving service, and CALLING_SERVICE_IDENTITY is the email address of the service account, by default PROJECT_NUMBER-compute@developer.gserviceaccount.com.

Terraform

To learn how to apply or remove a Terraform configuration, see Basic Terraform commands.

The following Terraform code creates an initial Cloud Run service intended to be public.

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

Replace us-docker.pkg.dev/cloudrun/container/hello with a reference to your container image.

The following Terraform code makes the initial service 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
}

The following Terraform code creates a second Cloud Run service intended to be private.

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

Replace us-docker.pkg.dev/cloudrun/container/hello with a reference to your container image.

The following Terraform code makes the second service private.

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
}

The following Terraform code creates a service account.

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

The following Terraform code allows services attached to the service account invoke the initial private Cloud Run service.

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
}

Acquire and configure the ID token

After you grant the proper role to the calling service account, follow these steps:

  1. Fetch a Google-signed ID token by using one of the methods described in the following section. Set the audience claim (aud) to the URL of the receiving service or a configured custom audience. If you are not using a custom audience, the aud value must remain as the URL of the service, even when making requests to a specific traffic tag.

  2. Add the ID token you fetched from the previous step into one of the following headers in the request to the receiving service:

    • An Authorization: Bearer ID_TOKEN header.
    • An X-Serverless-Authorization: Bearer ID_TOKEN header. You can use this header if your application already uses the Authorization header for custom authorization. This removes the signature before passing the token to the user container.

For other ways to get an ID token that are not described on this page, see Methods for getting an ID token.

Use the authentication libraries

The easiest and most reliable way to manage this process is to use the authentication libraries, as shown below, to generate and use this token. This code works in any environment, even outside of Google Cloud, where the libraries can obtain authentication credentials for a service account, including environments that support local Application Default Credentials. However, the code does not work for obtaining authentication credentials for a user account.

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

Use the metadata server

If for some reason you cannot use the authentication libraries, you can fetch an ID token from the Compute metadata server while your container is running on Cloud Run. Note that this method does not work outside of Google Cloud, including from your local machine.

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

Where AUDIENCE is the URL of the service you are invoking or a configured custom audience.

The following table summarizes the main parts of a metadata query request:

Components Description
Root URL

All metadata values are defined as sub-paths below the following root URL:

http://metadata.google.internal/computeMetadata/v1
Request header

The following header must be in each request:

Metadata-Flavor: Google

This header indicates that the request was sent with the intention of retrieving metadata values, rather than unintentionally from an insecure source, and lets the metadata server return the data you requested. If you don't provide this header, the metadata server denies your request.

For an end-to-end walkthrough of an application using this service-to-service authentication technique, follow the securing Cloud Run services tutorial.

Use workload identity federation from outside Google Cloud

If your environment uses an identity provider supported by workload identity federation, you can use the following method to securely authenticate to your Cloud Run service from outside Google Cloud:

  1. Set up your service account as described in Set up the service account on this page.

  2. Configure workload identity federation for your identity provider as described in Configuring workload identity federation.

  3. Follow the instructions in Granting external identities permission to impersonate a service account.

  4. Use the REST API to acquire a short-lived token, but instead of calling generateAccessToken to obtain an access token, call generateIdToken instead to get an ID token.

    For example, using 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
    

    Where SERVICE_ACCOUNT is the email address of the service account the workload identity pool is configured to access, and SERVICE_URL is the URL of the Cloud Run service you are invoking. This value should remain as the URL of the service, even when making requests to a specific traffic tag. $STS_TOKEN is the Security Token Service token you received in the previous step in the workload identity federation instructions.

You can include the ID token from the previous step in the request to the service by using an Authorization: Bearer ID_TOKEN header or an X-Serverless-Authorization: Bearer ID_TOKEN header. If both headers are provided, only the X-Serverless-Authorization header is checked.

Use a downloaded service account key from outside Google Cloud

If workload identity federation is not appropriate for your environment, you can use a downloaded service account key to authenticate from outside Google Cloud. Update your client code to use the authentication libraries as described previously with Application Default Credentials.

You can acquire a Google-signed ID token by using a self-signed JWT, but this is quite complicated and potentially error-prone. The basic steps are as follows:

  1. Self-sign a service account JWT with the target_audience claim set to the URL of the receiving service or a configured custom audience. If not using custom domains, the target_audience value should remain as the URL of the service, even when making requests to a specific traffic tag.

  2. Exchange the self-signed JWT for a Google-signed ID token, which should have the aud claim set to the preceding URL.

  3. Include the ID token in the request to the service by using an Authorization: Bearer ID_TOKEN header or an X-Serverless-Authorization: Bearer ID_TOKEN header. If both headers are provided, only the X-Serverless-Authorization header is checked.

You can examine this Cloud Functions example for a sample of the preceding steps.

Receive authenticated requests

Within the receiving private service, you can parse the authorization header to receive the information being sent by the Bearer token.

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"