אימות בין שירותים

אם הארכיטקטורה שלכם משתמשת בכמה שירותים, סביר להניח שהשירותים האלה צריכים לתקשר ביניהם, באמצעות אמצעים אסינכרוניים או סינכרוניים. יכול להיות שהרבה מהשירותים האלה הם פרטיים, ולכן נדרשים פרטי כניסה כדי לגשת אליהם.

לתקשורת אסינכרונית, אפשר להשתמש בשירותים הבאים Google Cloud :

  • Cloud Tasks לתקשורת אסינכרונית אחד על אחד
  • Pub/Sub לתקשורת אסינכרונית מסוג אחד לרבים, אחד לאחד ורבים לאחד
  • Cloud Scheduler לתקשורת אסינכרונית מתוזמנת באופן קבוע
  • Eventarc לתקשורת מבוססת-אירועים

בכל המקרים האלה, השירות שבו נעשה שימוש מנהל את האינטראקציה עם השירות המקבל, על סמך ההגדרה שהגדרתם.

אבל בתקשורת סינכרונית, השירות שלכם קורא לשירות אחר ישירות, דרך HTTP, באמצעות כתובת ה-URL של נקודת הקצה שלו. בתרחיש השימוש הזה, חשוב לוודא שכל שירות יכול לשלוח בקשות רק לשירותים ספציפיים. לדוגמה, אם יש לכם שירות login, הוא צריך להיות מסוגל לגשת לשירות user-profiles, אבל לא לשירות search.

במצב כזה, Google ממליצה להשתמש ב-IAM ובזהות שירות שמבוססת על חשבון שירות שמנוהל על ידי המשתמש לכל שירות, שקיבל את ההרשאות המינימליות שנדרשות לביצוע העבודה.

בנוסף, בבקשה צריך להיות אימות של זהות השירות ששולח את הבקשה. כדי לעשות את זה, צריך להגדיר את השירות שקורא ל-Google כדי להוסיף אסימון מזהה של OpenID Connect בחתימת Google כחלק מהבקשה.

הגדרת חשבון השירות

כדי להגדיר חשבון שירות, צריך להגדיר את השירות המקבל כך שיקבל בקשות מהשירות המתקשר. לשם כך, צריך להגדיר את חשבון השירות של השירות המתקשר כחשבון משתמש בשירות המקבל. לאחר מכן מקצים לחשבון השירות הזה את התפקיד Cloud Run Invoker‏ (roles/run.invoker). כדי לבצע את שתי המשימות האלה, פועלים לפי ההוראות בכרטיסייה המתאימה:

ממשק המשתמש של המסוף

  1. נכנסים למסוף Google Cloud :

    כניסה למסוף Google Cloud

  2. בוחרים את שירות הקבלה.

  3. לוחצים על Show Info Panel (הצגת חלונית המידע) בפינה השמאלית העליונה כדי להציג את הכרטיסייה Permissions (הרשאות).

  4. לוחצים על Add principal.

    1. מזינים את הזהות של השירות המתקשר. בדרך כלל זה כתובת אימייל, ובאופן ברירת מחדל PROJECT_NUMBER-compute@developer.gserviceaccount.com.

    2. בתפריט הנפתח Select a role (בחירת תפקיד), בוחרים את התפקיד Cloud Run Invoker.

    3. לוחצים על Save.

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.

מוסיפים את השורות הבאות למשאב google_cloud_run_v2_service בתצורת Terraform:

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
}

קבלת אסימון מזהה והגדרתו

אחרי שמקצים את התפקיד המתאים לחשבון השירות שקורא ל-API, פועלים לפי השלבים הבאים:

  1. מאחזרים אסימון מזהה בחתימת Google באמצעות אחת מהשיטות שמתוארות בקטע הבא. מגדירים את טענת הקהל (aud) לכתובת ה-URL של השירות המקבל או לקהל בהתאמה אישית שהוגדר. אם אתם לא משתמשים בקהל בהתאמה אישית, הערך aud חייב להישאר כתובת ה-URL של השירות, גם כששולחים בקשות לתג תנועה ספציפי.

  2. מוסיפים את האסימון המזהה שאוחזר בשלב הקודם לאחת מהכותרות הבאות בבקשה לשירות המקבל:

    • כותרת Authorization: Bearer ID_TOKEN.
    • כותרת X-Serverless-Authorization: Bearer ID_TOKEN. אפשר להשתמש בכותרת הזו אם האפליקציה כבר משתמשת בכותרת Authorization לאימות מותאם אישית. הפעולה הזו מסירה את החתימה לפני העברת האסימון למאגר של המשתמש.

במאמר שיטות לקבלת אסימון מזהה מפורטות דרכים נוספות לקבלת אסימון מזהה שלא מתוארות בדף הזה.

שימוש בספריות לאימות

אחת הדרכים להשיג ולהגדיר את תהליך אסימון המזהה היא באמצעות ספריות האימות. הקוד הזה פועל בכל סביבה, גם מחוץ ל- 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()

המשך


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

שימוש בשרת מטא-נתונים

אם מסיבה כלשהי אין לכם אפשרות להשתמש בספריות האימות, תוכלו לאחזר אסימון מזהה משרת המטא-נתונים של Compute בזמן שהקונטיינר פועל ב-Cloud Run. שימו לב שהשיטה הזו לא פועלת מחוץ ל- Google Cloud, כולל מהמחשב המקומי.

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

כאשר AUDIENCE היא כתובת ה-URL של השירות שאתם מפעילים או קהל בהתאמה אישית שהגדרתם.

בטבלה הבאה מפורטים החלקים העיקריים של בקשת שאילתת מטא-נתונים:

רכיבים תיאור
כתובת URL בסיסית

כל ערכי המטא-נתונים מוגדרים כנתיבי משנה מתחת לכתובת ה-URL הבסיסית הבאה:

http://metadata.google.internal/computeMetadata/v1
כותרת הבקשה

כל בקשה חייבת לכלול את הכותרת הבאה:

Metadata-Flavor: Google

הכותרת הזו מציינת שהבקשה נשלחה בכוונה מפורשת לאחזר ערכי מטא-נתונים, ולא נשלחה בלי כוונה ממקור לא מאובטח, והיא מאפשרת לשרת המטא-נתונים להחזיר את הנתונים שביקשתם. אם לא תספקו את הכותרת הזו, שרת המטא-נתונים ידחה את הבקשה שלכם.

כדי לקבל הסבר מפורט על אפליקציה שמשתמשת בטכניקת האימות הזו משירות לשירות, אפשר לעיין במדריך לאבטחת שירותי Cloud Run.

שימוש באיחוד שירותי אימות הזהות של עומסי עבודה מחוץ ל- Google Cloud

אם בסביבה שלכם נעשה שימוש בספק זהויות שתומך באיחוד שירותי אימות הזהות של עומסי עבודה, אתם יכולים להשתמש בשיטה הבאה כדי לבצע אימות מאובטח לשירות Cloud Run שלכם מחוץ ל- Google Cloud:

  1. מגדירים את חשבון השירות כמו שמתואר בקטע הגדרת חשבון השירות בדף הזה.

  2. מגדירים את האיחוד של Workload Identity עבור ספק הזהויות, כפי שמתואר במאמר הגדרת איחוד של Workload Identity.

  3. פועלים לפי ההוראות בקטע הענקת גישה לזהויות חיצוניות כדי להתחזות לחשבון שירות.

  4. משתמשים ב-API בארכיטקטורת REST כדי לקבל אסימון לטווח קצר, אבל במקום להפעיל את generateAccessToken כדי לקבל טוקן גישה, מפעילים את generateIdToken כדי לקבל אסימון מזהה.

    לדוגמה, באמצעות 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 היא כתובת האימייל של חשבון השירות שהוגדר למאגר הזהויות של עומס העבודה כדי לגשת אליו, ו-SERVICE_URL היא כתובת ה-URL של שירות Cloud Run שמופעל. הערך הזה צריך להיות כתובת ה-URL של השירות, גם כששולחים בקשות לתג תנועה ספציפי. ‫$STS_TOKEN הוא אסימון Security Token Service שקיבלתם בשלב הקודם בהוראות בנושא איחוד זהויות של עומסי עבודה.

אפשר לכלול את האסימון המזהה מהשלב הקודם בבקשה לשירות באמצעות כותרת Authorization: Bearer ID_TOKEN או כותרת X-Serverless-Authorization: Bearer ID_TOKEN. אם שני סוגי הכותרות מסופקים, רק הכותרת X-Serverless-Authorizationנבדקת.

שימוש במפתח של חשבון שירות שהורד מחוץ ל- Google Cloud

אם איחוד שירותי אימות הזהות של עומסי עבודה לא מתאים לסביבה שלכם, אתם יכולים להשתמש במפתח של חשבון שירות שהורדתם כדי לבצע אימות מחוץ ל-Google Cloud. מעדכנים את קוד הלקוח כך שישתמש בספריות האימות כפי שמתואר למעלה. מידע נוסף מופיע במאמר בנושא מפתח של חשבון שירות.

אפשר לקבל אסימון מזהה חתום על ידי Google באמצעות JWT בחתימה עצמית, אבל התהליך הזה מורכב מאוד ועלול להוביל לשגיאות. אלה השלבים הבסיסיים:

  1. חתימה עצמית על JWT של חשבון שירות עם ההצהרה target_audience שמוגדרת לכתובת ה-URL של שירות הקבלה או לקהל בהתאמה אישית שהוגדר. אם לא משתמשים בדומיינים בהתאמה אישית, הערך של target_audience צריך להיות כתובת ה-URL של השירות, גם כששולחים בקשות לתג תנועה ספציפי.

  2. מחליפים את ה-JWT בחתימה עצמית באסימון מזהה עם חתימה של Google, שבו ההצהרה aud מוגדרת לכתובת ה-URL הקודמת.

  3. כוללים את האסימון המזהה בבקשה לשירות באמצעות כותרת Authorization: Bearer ID_TOKEN או כותרת X-Serverless-Authorization: Bearer ID_TOKEN. אם מסופקות שתי הכותרות, רק הכותרת 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"

המאמרים הבאים