驗證服務對服務

如果架構使用多項服務,這些服務可能需要使用非同步或同步方式彼此通訊。許多這類服務可能屬於私人服務,因此需要憑證才能存取

如要進行非同步通訊,您可以使用下列 Google Cloud 服務:

在所有這些情況下,所使用的服務會根據您設定的設定,管理與接收服務的互動情形。

不過,如果是同步通訊,您的服務會透過 HTTP 直接呼叫另一項服務,並使用該服務的端點網址。針對這種用途,您應確保每項服務只能向特定服務提出要求。舉例來說,如果您擁有 login 服務,則該服務應該能夠存取 user-profiles 服務,但無法存取 search 服務。

在這種情況下,Google 建議您使用 IAM 和服務身分,並以個別服務使用者代管的服務帳戶為基礎,授予執行工作所需的最低權限

此外,要求必須提供呼叫服務身分的證明。如要這樣做,請設定呼叫服務,以便在要求中加入 Google 簽署的 OpenID Connect ID 權杖。

設定服務帳戶

如要設定服務帳戶,您可以將呼叫服務的服務帳戶設為接收服務的實體,讓接收服務接受來自呼叫服務的要求。接著,您可以將 Cloud Run 叫用者 (roles/run.invoker) 角色授予該服務帳戶。如要執行這兩項工作,請按照相關分頁的操作說明進行:

主控台使用者介面

  1. 前往 Google Cloud 控制台:

    前往 Google Cloud 控制台

  2. 選取接收的服務。

  3. 按一下右上角的 [Show Info Panel] (顯示資訊面板),顯示「Permissions」(權限) 分頁。

  4. 按一下「新增主體」

    1. 輸入呼叫服務的身分。這通常是電子郵件地址,預設為 PROJECT_NUMBER-compute@developer.gserviceaccount.com

    2. 從「Select a role」下拉式選單中選取 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 設定中的 google_cloud_run_v2_service 資源:

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

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 程式碼會建立第二個 Cloud Run 服務,並將其設為私人

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

us-docker.pkg.dev/cloudrun/container/hello 替換為容器映像檔的參照。

以下 Terraform 程式碼會將第二個服務設為私人。

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) 設為接收服務的網址或已設定的自訂目標對象。如果您未使用自訂目標對象,即使向特定流量標記提出要求,aud 值仍必須是服務的網址。

  2. 將您在前一個步驟中擷取的 ID 符記,加入向接收服務提出要求的其中一個標頭:

    • Authorization: Bearer ID_TOKEN 標頭。
    • X-Serverless-Authorization: Bearer ID_TOKEN 標頭。如果應用程式已使用 Authorization 標頭進行自訂授權,您可以使用這個標頭。這會在將權杖傳遞至使用者容器之前移除簽名。

如要瞭解本頁未提及的其他 ID 權杖取得方式,請參閱「取得 ID 權杖的方法」。

使用驗證程式庫

取得及設定 ID 權杖程序的其中一種方法,就是使用驗證程式庫。這個程式碼可在任何環境中運作,甚至在 Google Cloud之外也能運作,因為程式庫可取得服務帳戶的驗證憑證。如要使用這個方法,請下載服務帳戶金鑰檔案,然後將環境變數 GOOGLE_APPLICATION_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);

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

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 上執行時,從 Compute 中繼資料伺服器擷取 ID 權杖。請注意,這個方法無法在 Google Cloud以外的地方 (包括本機) 使用。

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

其中 AUDIENCE 是您要叫用的服務網址,或已設定的自訂目標對象

下表總結了中繼資料查詢要求的主要部分:

元件 說明
根網址

所有中繼資料值都會定義為下列根網址下的子路徑:

http://metadata.google.internal/computeMetadata/v1
要求標頭

每個要求都必須包含下列標頭:

Metadata-Flavor: Google

此標頭指示是在擷取中繼資料值的意圖之下傳送要求,而非隨意從不安全的來源傳送,並允許中繼資料伺服器傳回您要求的資料。如果您不提供此標頭,中繼資料伺服器會拒絕您的要求。

如需使用這項服務對服務驗證技術的應用程式,瞭解端對端操作說明,請參閱Cloud Run 服務安全性教學課程

從外部使用 Workload Identity 聯盟 Google Cloud

如果您的環境使用工作負載身分聯盟支援的識別資訊提供者,您可以使用下列方法,從外部 Google Cloud安全地驗證 Cloud Run 服務:

  1. 按照本頁「設定服務帳戶」一節的說明設定服務帳戶。

  2. 如「設定 Workload Identity 聯盟」一文所述,為您的身分提供者設定 Workload Identity 聯盟。

  3. 請按照「授予外部身分模擬服務帳戶的權限」一文中的操作說明進行操作。

  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 @- <&ltEOF | jq -r .token
      {
          "audience": "SERVICE_URL"
      }
    EOF
    )
    echo $ID_TOKEN

    其中 SERVICE_ACCOUNT 是工作負載身分集區設定的服務帳戶電子郵件地址,SERVICE_URL 則是您要叫用的 Cloud Run 服務網址。即使向特定流量標記提出要求,這個值仍應維持為服務的網址。$STS_TOKEN 是您在工作負載身分識別資訊聯合作業指示的上一個步驟中收到的安全性權杖服務權杖。

您可以使用 Authorization: Bearer ID_TOKEN 標頭或 X-Serverless-Authorization: Bearer ID_TOKEN 標頭,在向服務提出要求時,加入上一個步驟中的 ID 符記。如果提供兩個標頭,系統只會檢查 X-Serverless-Authorization 標頭。

使用從外部下載的服務帳戶金鑰 Google Cloud

如果 Workload Identity 聯盟不適合您的環境,您可以使用下載的服務帳戶金鑰,從Google Cloud外部進行驗證。更新用戶端程式碼,以便使用驗證程式庫,如前述所述。詳情請參閱「服務帳戶金鑰」。

您可以使用自行簽署的 JWT 取得 Google 簽署的 ID 權杖,但這項操作相當複雜,且可能會發生錯誤。基本步驟如下:

  1. 自行簽署服務帳戶 JWT,需具備設定為接收服務網址的 target_audience 憑證附加資訊,或已設定的自訂目標對象。如果未使用自訂網域,target_audience 值應維持為服務的網址,即使向特定流量標記提出要求也是如此。

  2. 使用自行簽署的 JWT 交換 Google 簽署的 ID 權杖,這個符記應已將 aud 憑證附加資訊設定為上述網址。

  3. 使用 Authorization: Bearer ID_TOKEN 標頭或 X-Serverless-Authorization: Bearer ID_TOKEN 標頭,在向服務提出要求的請求中加入 ID 符記。如果提供兩個標頭,系統只會檢查 X-Serverless-Authorization 標頭。

接收已驗證的要求

在接收私人服務的情況下,您可以剖析授權標頭,接收 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"

後續步驟