Authenticating service-to-service

Stay organized with collections Save and categorize content based on your preferences.

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

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

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

  template {
    spec {
      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_service.private.status[0].url
        }
      }

      # Give the "public" Cloud Run service
      # a service account's identity
      service_account_name = 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_service.public.location
  project  = google_cloud_run_service.public.project
  service  = google_cloud_run_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_service" "private" {
  name     = "private-service"
  location = "us-central1"

  template {
    spec {
      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_service.private.location
  project  = google_cloud_run_service.private.project
  service  = google_cloud_run_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_service.private.location
  project  = google_cloud_run_service.private.project
  service  = google_cloud_run_service.private.name

  policy_data = data.google_iam_policy.private.policy_data
}

Acquire and configure the ID token

Once the calling service account has been granted the proper role, you need to:

  1. Fetch a Google-signed ID token with the audience claim (aud) set to the URL of the receiving service. The aud value should remain as the URL of the service, even when making requests to a specific traffic tag.

  2. Include the ID token in an Authorization: Bearer ID_TOKEN header in the request to the receiving service.

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, including environments that support local Application Default Credentials.

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);
  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: %v", err)
	}

	resp, err := client.Get(targetURL)
	if err != nil {
		return fmt.Errorf("client.Get: %v", err)
	}
	defer resp.Body.Close()
	if _, err := io.Copy(w, resp.Body); err != nil {
		return fmt.Errorf("io.Copy: %v", 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.

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

Receiving 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 import jwt


def receive_authorized_get_request(request):
    """
    receive_authorized_get_request takes the "Authorization" header from a
    request, decodes it using the google-auth client library, and returns
    back the email from the header to the caller.
    """
    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 = jwt.decode(creds, verify=False)
            return f"Hello, {claims['email']}!\n"

        else:
            return f"Unhandled header format ({auth_type}).\n"
    return "Hello, anonymous user.\n"

Call from outside Google Cloud

You can call a private service from outside Google Cloud using workload identity federation or by using a downloaded service account key. Both methods are described in the following sections.

Use workload identity federation

If your environment uses an identity provider supported by workload identity federation, you can use this method to securely authenticate to your Cloud Run service:

  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.

Once you have the ID token, you can include it in an Authorization: Bearer ID_TOKEN header in the request to the receiving service.

Use a downloaded service account key

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

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

  1. Self-sign a service account JWT with the target_audience claim set to the URL of the receiving service. 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 above URL.

  3. Include the ID token in an Authorization: Bearer ID_TOKEN header in the request to the service.

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