Closed15

GCP 内部から外部へ公開しない HTTP Cloud Functions へ request を送信できるようにしたい

nbstshnbstsh

GCP の project 内部から、public には公開しない private な HTTP Cloud Functions (2nd gen) に対して secure に request を送信したい。

VPC Connector を利用する方法もあるみたいだが、今回は IAM の設定のみで実現する方法を試してみる。

nbstshnbstsh

To configure the receiving function to accept requests from a specific calling function, you need to grant the appropriate invoker role to the calling function's service account on the receiving function. For 1st gen functions, the invoker role is Cloud Functions Invoker (roles/cloudfunctions.invoker). For 2nd gen functions, the invoker role is Cloud Run Invoker (roles/run.invoker) and must be granted on the underlying service.

invoker role を付与した service account による認証を行う。

nbstshnbstsh

2nd gen の場合、gcloud functions add-invoker-policy-binding で設定すると、roles/run.invoker が付与される。

gcloud functions add-invoker-policy-binding RECEIVING_FUNCTION \
  --member='serviceAccount:CALLING_FUNCTION_IDENTITY'
nbstshnbstsh
  1. Create a Google-signed ID token with the audience field (aud) set to the URL of the receiving function.

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

ID Token を作成し、Authorization: Bearer ID_TOKEN でリクエストする。

nbstshnbstsh

Generating tokens programmatically

By far the easiest and most reliable way to manage this process is to use the authentication libraries, as shown below, to generate and employ this token.

google-auth-library を利用するのがおすすめらしい。

sample code↓

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */

// Cloud Functions uses your function's url as the `targetAudience` value
// const targetAudience = 'https://project-region-projectid.cloudfunctions.net/myFunction';
// For Cloud Functions, endpoint (`url`) and `targetAudience` should be equal
// const url = targetAudience;


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

sample code 通りにやったがうまくいかない....

404 返ってくるぞ...

nbstshnbstsh

After the following code generates an ID token, it calls your Cloud Function with that token on your behalf. This code works in any environment where the libraries can obtain authentication credentials, including environments that support local Application Default Credentials.

わかったぞ

上記のsample code は

  • ID token の生成
  • 生成した ID Token を Header に付与した状態で request の送信

の二つを一気にやってくれてるんだな。
今回テストした functions は REST API で、存在しない route なので 404 になってたみたい。

nbstshnbstsh

しかし、request 用の client は自前のものを利用したいから、今回は ID Token だけ取りたい...

nbstshnbstsh

これでいけた↓

const auth = new GoogleAuth();

const targetAudience = 'https://project-region-projectid.cloudfunctions.net/myFunction';
const client = await auth.getIdTokenClient(targetAudience);
const token = await client.idTokenProvider.fetchIdToken(targetAudience);

IdTokenClientIdTokenProvider.fetchIdToken が生えてたので利用。

google-auth-library/src/auth/idtokenclient.ts
export interface IdTokenProvider {
    fetchIdToken: (targetAudience: string) => Promise<string>;
}
export declare class IdTokenClient extends OAuth2Client {
    targetAudience: string;
    idTokenProvider: IdTokenProvider;
    /**
     * Google ID Token client
     *
     * Retrieve access token from the metadata server.
     * See: https://developers.google.com/compute/docs/authentication
     */
    constructor(options: IdTokenOptions);
    protected getRequestMetadataAsync(url?: string | null): Promise<RequestMetadataResponse>;
    private getIdTokenExpiryDate;
}
nbstshnbstsh

client.credentials.id_token も生えているが、この値は一度 request を送信しない限り undefined になるので、client.idTokenProvider.fetchIdToken の方が良さげ


const targetAudience =
  'https://project-region-projectid.cloudfunctions.net/myFunction';

const client = await auth.getIdTokenClient(targetAudience);
console.log(client.credentials.id_token);
// => undefined

await client.request({
  url: targetAudience,
});
console.log(client.credentials.id_token)
// => idToken あり
nbstshnbstsh

あとは、この ID Token を Header Authorization: Bearer ID_TOKEN につけて request を送信すればOK

nbstshnbstsh

getClient を利用するパターン

ID token の取得に関するドキュメントあった↓

https://cloud.google.com/docs/authentication/get-id-token#methods

const googleAuth = new GoogleAuth();

const targetAudience =
  'https://project-region-projectid.cloudfunctions.net/myFunction';

const client = (await googleAuth.getClient()) as Compute;

const idToken = await client.fetchIdToken(targetAudience);

console.log(idToken);

純粋に ID token を取得するだけならこれだけでいける。
ただし、googleAuth.getClient() の戻り値が Compute | JSONClient で、fetchIdTokenCompute にのみ存在するメソッドなので、型的には Compute にキャストする必要がある。

getClient(): Promise<Compute | JSONClient | T>;
nbstshnbstsh

metadata server を直接叩く

google-auth-library を利用しないで、metadata server を直接叩く方法もある。

https://cloud.google.com/functions/docs/securing/authenticating#using_the_metadata_server

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

AUDIENCE: function の URL など

nbstshnbstsh

firebase functions で動作確認する例

import { onRequest } from 'firebase-functions/v2/https';

export const printIdToken = onRequest(async (req, res) => {
  const audience =
    'https://project-region-projectid.cloudfunctions.net/myFunction';

  const response = await fetch(
    `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=${audience}`,
    {
      headers: {
        'Metadata-Flavor': 'Google',
      },
    },
  );

  if (!response.ok) {
    throw new Error(`Failed to fetch id token: ${response.statusText}`);
  }

  const idToken = await response.text();

  console.log('idToken:', idToken);

  res.send('ok');
});
このスクラップは2023/06/29にクローズされました