サービス間認証

アーキテクチャで複数のサービスを使用している場合、これらのサービスは同期または非同期で相互に通信する必要があります。これらのサービスの多くは非公開で、アクセスには認証情報が必要です。

非同期通信では、次の Google Cloud サービスを使用できます。

  • Cloud Tasks(1 対 1 の非同期通信を行う場合)。
  • Pub/Sub(1 対多、1 対 1、多対 1 の非同期通信を行う場合)
  • Cloud Scheduler(定期的にスケジュール設定された非同期通信を行う場合)
  • Eventarc(イベントベースの通信を行う場合)

いずれの場合も、使用中のサービスが、設定した構成に基づいて受信側のサービスとの通信を管理します。

ただし、同期通信の場合は、サービスがエンドポイント URL を使用して HTTP 経由で別のサービスを直接呼び出します。このユースケースでは、各サービスが特定のサービスに対してのみリクエストを送信できるようにします。たとえば、login サービスの場合、user-profiles サービスにはアクセスできますが、search サービスにはアクセスできません。

この場合は、IAM と、その作業に必要な権限の最小セットが付与されたサービスごとのユーザー管理サービス アカウントに基づいてサービス ID を使用することをおすすめします。

さらに、リクエストで呼び出し側サービス ID の証明を提示する必要があります。これを行うには、Google 署名付き OpenID Connect ID トークンをリクエストの一部として追加するように呼び出し側サービスを構成します。

サービス アカウントを設定する

サービス アカウントを設定するには、呼び出し元のサービス アカウントを受信側サービスのプリンシパルにすることで、呼び出し側サービスからのリクエストを受け入れるように受信側サービスを構成します。そのサービス アカウントに Cloud Run 起動元(roles/run.invoker)のロールを付与します。該当するタブの手順に沿って、これらの作業を行います。

コンソール UI

  1. Google Cloud コンソールに移動します。

    Google Cloud コンソールに移動

  2. 受信側のサービスを選択します。

  3. 右上隅にある [情報パネルを表示] をクリックして、[権限] タブを表示します。

  4. [プリンシパルを追加] をクリックします。

    1. 呼び出し側のサービスの ID を入力します。これは通常、デフォルトでメールアドレス PROJECT_NUMBER-compute@developer.gserviceaccount.com です。

    2. [ロールの選択] プルダウン メニューから、Cloud Run Invoker ロールを選択します。

    3. [保存] をクリックします。

gcloud

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'

ここでは、RECEIVING_SERVICE は受信側サービスの名前で、CALLING_SERVICE_IDENTITY はサービス アカウントのメールアドレス(デフォルトでは PROJECT_NUMBER-compute@developer.gserviceaccount.com)です。

Terraform

Terraform 構成を適用または削除する方法については、基本的な Terraform コマンドをご覧ください。

次の Terraform コードは、一般公開となる最初の Cloud Run サービスを作成します。

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

us-docker.pkg.dev/cloudrun/container/hello は、コンテナ イメージへの参照に置き換えます。

次の Terraform コードを使用すると、最初のサービスが公開されます。

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
}

次の Terraform コードは、非公開となる 2 つ目の Cloud Run サービスを作成します。

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

us-docker.pkg.dev/cloudrun/container/hello は、コンテナ イメージへの参照に置き換えます。

次の Terraform コードを使用すると、2 番目のサービスが非公開になります。

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
}

次の Terraform コードは、サービス アカウントを作成します。

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

次の Terraform コードを使用すると、サービス アカウントに関連付けられたサービスが、最初の非公開 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
}

ID トークンの取得と構成

呼び出し元のサービス アカウントに適切なロールを付与したら、次の操作を行います。

  1. 次のセクションで説明するいずれかの方法で、Google によって署名された ID トークンを取得します。オーディエンス クレーム(aud)を、受信側のサービスまたは構成されたカスタム オーディエンスの URL に設定します。カスタム オーディエンスを使用しない場合は、特定のトラフィック タグにリクエストを送信する場合でも、aud 値をサービスの URL として残す必要があります。

  2. 前のステップで取得した ID トークンを、受信側のサービスに対するリクエストの次のいずれかのヘッダーに追加します。

    • Authorization: Bearer ID_TOKEN ヘッダー。
    • X-Serverless-Authorization: Bearer ID_TOKEN ヘッダー。アプリケーションがすでにカスタム承認に Authorization ヘッダーを使用している場合は、このヘッダーを使用できます。これにより、トークンがユーザー コンテナに渡される前に署名が削除されます。

このページで説明されていない ID トークンを取得するその他の方法については、ID トークンの取得方法をご覧ください。

認証ライブラリを使用する

このプロセスを最も簡単かつ信頼性の高い方法で管理するには、以下に示すように認証ライブラリを使用してこのトークンを生成し、適用します。このコードは、ライブラリによってサービス アカウントの認証情報が取得される Google Cloud 外のあらゆる環境で機能します(ローカル アプリケーションのデフォルト認証情報をサポートする環境を含む)。ただし、このコードはユーザー アカウントの認証情報を取得できません。

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

メタデータ サーバーを使用する

なんらかの理由で認証ライブラリを使用できない場合は、コンテナが Cloud Run で実行されている間、コンピューティング メタデータ サーバーから ID トークンを取得できます。この方法は、ローカルマシンからなど、Google Cloud の外部では機能しないことに注意してください。

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

「オーディエンス」は、起動しているサービスの URL または構成済みのカスタム オーディエンスです。

次の表に、メタデータ クエリ リクエストの主要部分を示します。

コンポーネント 説明
ルート URL

すべてのメタデータ値は、次のルート URL のサブパスとして定義されます。


http://metadata.google.internal/computeMetadata/v1
リクエスト ヘッダー

次のヘッダーは各リクエストに含める必要があります。


Metadata-Flavor: Google

このヘッダーにより、そのリクエストがメタデータ値を取得する目的で送信されたものであり、安全でないソースから意図せず送信されたものではないことが示されるため、リクエストしたデータがメタデータ サーバーから返されるようになります。このヘッダーが含まれていないリクエストはメタデータ サーバーで拒否されます。

このサービス間の認証方式を使用するアプリケーションのエンドツーエンドのチュートリアルは、安全な Cloud Run サービスのチュートリアルをご覧ください。

Google Cloud の外部から Workload Identity 連携を使用する

Workload Identity 連携でサポートされている ID プロバイダを環境で使用している場合は、次の方法で Google Cloud の外部から Cloud Run サービスへの認証を安全に行うことができます。

  1. このページのサービス アカウントの設定の説明に従ってサービス アカウントを設定します。

  2. Workload Identity 連携の構成の説明に従って ID プロバイダの Workload Identity 連携を構成します。

  3. 外部 ID にサービス アカウントの権限借用を許可するの手順を行います。

  4. REST API を使用して有効期間の短いトークンを取得します。ただし、generateAccessToken を呼び出してアクセス トークンを取得する代わりに、generateIdToken を呼び出して ID トークンを取得します。

    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
    

    ここで、SERVICE_ACCOUNT は Workload Identity プールがアクセスするように構成されているサービス アカウントのメールアドレスです。SERVICE_URL は、呼び出す Cloud Run サービスの URL です。この値は、特定のトラフィック タグにリクエストを送信する場合でも、サービスの URL として残す必要があります。$STS_TOKEN は、Workload Identity 連携の前の手順で受信したセキュリティ トークン サービス トークンです。

Authorization: Bearer ID_TOKEN ヘッダーまたは X-Serverless-Authorization: Bearer ID_TOKEN ヘッダーを使用して、サービスへのリクエストの前の手順で取得した ID トークンを含めることができます。両方のヘッダーを指定すると、X-Serverless-Authorization ヘッダーのみがチェックされます。

Google Cloud の外部からダウンロードしたサービス アカウント キーを使用する

環境で Workload Identity 連携の使用が適切でない場合は、ダウンロードしたサービス アカウント キーを使用して、Google Cloud の外部から認証できます。上記のように、アプリケーションのデフォルト認証情報で認証ライブラリを使用するようにクライアント コードを更新します。

自己署名 JWT を使用して Google 署名付き ID トークンを取得できますが、作業はかなり複雑になり、エラーが発生しやすくなります。基本的な手順は次のとおりです。

  1. target_audience クレームを受信側サービスまたは構成済みカスタム オーディエンスの URL に設定して、サービス アカウント JWT に自己署名します。カスタム ドメインを使用しない場合、target_audience 値は、特定のトラフィック タグにリクエストを送信する場合でも、サービスの URL として残す必要があります。

  2. Google によって署名された ID トークンと自己署名された JWT を交換します。このトークンでは、aud クレームに前述の URL が設定されています。

  3. Authorization: Bearer ID_TOKEN ヘッダーまたは X-Serverless-Authorization: Bearer ID_TOKEN ヘッダーを使用して、サービスへのリクエストに ID トークンを含めます。両方のヘッダーを指定すると、X-Serverless-Authorization ヘッダーのみがチェックされます。

上記の手順のサンプルについては、この Cloud Functions の関数の例で確認できます。

認証済みリクエストを受信する

受信側のプライベート サービス内で、Authorization ヘッダーを解析して署名なしトークンによって送信される情報を受信できます。

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"