サービス間認証

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

非同期通信では、次の 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 Console に移動します。

    Google Cloud Console に移動

  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)です。

ID トークンの取得と構成

呼び出し側のサービス アカウントに適切なロールを付与したら、次の操作を行う必要があります。

  1. 受信側のサービスの URL に設定されたオーディエンス クレーム(aud)を使用して、Google 署名付き ID トークンを取得します。aud 値は、特定のトラフィック タグにリクエストを送信する場合でも、サービスの URL として残す必要があります。

  2. 受信側サービスに対するリクエストの Authorization: Bearer ID_TOKEN ヘッダーに 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);
  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();
  }
}

メタデータ サーバーの使用

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

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

AUDIENCE は、起動しているサービスの URL です。

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

認証済みリクエストの受信

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

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"

Google Cloud の外部からの呼び出し

Workload Identity 連携を使用するか、ダウンロードしたサービス アカウント キーを使用して、Google Cloud の外部からプライベート サービスを呼び出すこともできます。以降のセクションでは、この両方の方法について説明します。

Workload Identity 連携を使用する

Workload Identity 連携でサポートされている ID プロバイダを環境で使用している場合は、次の方法で 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 連携の前の手順で受信したセキュリティ トークン サービス トークンです。

ID トークンを取得したら、受信側のサービスに対するリクエストの Authorization: Bearer ID_TOKEN ヘッダーにそのトークンを含めます。

ダウンロードしたサービス アカウント キーを使用する

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

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

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

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

  3. サービスに対するリクエストの Authorization: Bearer ID_TOKEN ヘッダーに ID トークンを含めます。

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