Cloud Functions の呼び出しを許可されたアカウントのみが実行できるよう制御する方法

記事タイトルとURLをコピーする

G-gen の又吉です。当記事では、Cloud Functions の呼び出しを許可されたアカウントのみが実行できるよう制御する方法を紹介します。

概要

背景

Cloud Functions のトリガーには大きく 2 つのカテゴリに分かれています。

No トリガー 名 説明
1 HTTP (S) トリガー Cloud Functions の URL エンドポイントを使用し HTTP リクエストをトリガーに関数を実行する
2 イベントトリガー Pub / Sub トピックのメッセージや Cloud Storage バケットの変更等をトリガーに関数を実行する

さらに HTTP トリガーでは、以下の 2 つの認証方法が存在します。

No 認証方法 説明
1 未認証の呼び出しを許可 allUsers」という特別なプリンシパルに「Cloud Run 起動元」ロールが付与されるため、インターネット上のすべてのユーザーがアクセスできるようになる
2 認証が必要 IAM を使用して許可するユーザーを管理できる

Cloud Functions 新規作成時の HTTPS トリガー設定項目

Web サイト等のインターネットに公開するシステム以外は、基本的に[認証が必要]としてトリガーを設定することが HTTP トリガーの推奨事項とされています。

構成

今回の構成はシンプルに、functions-1 関数を「認証が必要」とした HTTP トリガーでデプロイします。

functions-2 関数には functions-1 関数を実行できる権限を付与し、functions-3 関数には何も権限を付与せず、それぞれが functions-1 関数を HTTP リクエストで呼び出します。

構成図

尚、本記事では Cloud Functions の概要については触れていないため、詳しく知りたい方は以下の記事をご参考に下さい。

blog.g-gen.co.jp

準備

準備と動作確認はすべて gcloud コマンドで実行します。 gcloud コマンドがローカル環境で使えない場合は、Cloud Shell からでも実行可能です。

gcloud コマンドのインストール方法はこちらの公式リファレンスを参照下さい。

必要な API の有効化

対象のプロジェクト内で以下の API を有効化します。

  • artifactregistry.googleapis.com
  • cloudbuild.googleapis.com
  • cloudfunctions.googleapis.com
  • logging.googleapis.com
  • pubsub.googleapis.com
  • run.googleapis.com

以下のコマンドを実行します。

gcloud services enable artifactregistry.googleapis.com \
  cloudbuild.googleapis.com \
  cloudfunctions.googleapis.com \
  logging.googleapis.com \
  pubsub.googleapis.com \
  run.googleapis.com

サービスアカウントの作成

functions-2 関数と、functions-3 関数のサービスアカウントを作成する。

functions-2 関数のサービスアカウント作成

gcloud iam service-accounts create functions-2 \
--display-name="functions-2" \
--project=<project_id>

functions-3 関数のサービスアカウント作成

gcloud iam service-accounts create functions-3 \
--display-name="functions-3" \
--project=<project_id>

フォルダ構成

実行環境で、以下のフォルダを作成する。 尚、functions-2 関数と functions-3 関数は同じソースコードを利用し、相違点はサービスアカウントの権限のみとする。

authenticationg_for_invocation
├── functions_1
│    ├── main.py
│    └── requirements.txt
│
└── functions_2_3
      ├── main.py
      └── requirements.txt

functions_1/main.py

functions-1 関数は、リクエストが来たら Hello HTTP!! と返すだけのシンプルな関数とする。

import functions_framework
  
  
@functions_framework.http
def hello_http(request):
  
   return "Hello HTTP!!"

functions_1/requirements.txt

functions-framework==3.*

functions_2_3/main.py

Python クライアントライブラリのサンプル を参考に、実行関数に付与されたサービスアカウントの認証情報を使用して、functions-1 関数のトークンを取得する処理を実装している。

尚、実行関数に付与されたサービスアカウントに functions-1 関数に対してのroles/run.invoker 権限が含まれていればトークンが取得できる。

import os
  
import functions_framework
import urllib
import google.auth.transport.requests
import google.oauth2.id_token
  
  
# functions-1 関数のエンドポイント URL
ENDPOINT = os.environ["ENDPOINT"]
  
  
@functions_framework.http
def make_authorized_get_request(request):
  
    # Request をインスタンス化
    req = urllib.request.Request(ENDPOINT)
  
    # サービスアカウントの認証情報を使用し functions-1 関数のトークンを取得
    auth_req = google.auth.transport.requests.Request()
    id_token = google.oauth2.id_token.fetch_id_token(auth_req, ENDPOINT)
    print(id_token)
  
    # トークンをヘッダーに付与し functions-1 関数に HTTP リクエストを送信
    req.add_header("Authorization", f"Bearer {id_token}")
    response = urllib.request.urlopen(req)
  
    return response.read()

functions_2_3/requirements.txt

functions-framework==3.*
google-auth==2.16.0
requests==2.27.1
urllib3==1.26.14

Cloud Functions の作成

はじめに、デフォルトリージョンを設定する。

gcloud config set functions/region asia-northeast1 && \
gcloud config set run/region asia-northeast1

functions-1 関数の作成

authenticationg_for_invocation/ がカレントディレクトリになっていることを確認し、以下を実行。

gcloud functions deploy functions-1 \
  --gen2 \
  --trigger-http \
  --runtime=python310 \
  --entry-point=hello_http \
  --source=functions_1/ 

functions-2 関数の作成

functions-2 関数のデプロイに必要な情報は以下の通り。

  1. functions-1 関数の URI エンドポイント
  2. functions-2 関数に付与するサービスアカウントのメールアドレス

以下のコマンドで functions-1 関数の URI エンドポイントを確認する。

gcloud functions describe functions-1 --gen2 | grep uri

以下のように出力されたら、 https://functions-1-xxxxxxxxxx-an.a.run.app をコピーしておく。

matayuuu@cloudshell:~ (project_id)$ gcloud functions describe functions-1 --gen2 | grep uri
  uri: https://functions-1-xxxxxxxxxx-an.a.run.app

次に、以下のコマンドで functions-2 関数のサービスアカウントのメールアドレスを取得する

gcloud iam service-accounts list | grep EMAIL: | grep functions-2

以下のように出力された、functions-2@project_id.iam.gserviceaccount.com をコピーしておく。

matayuuu@cloudshell:~ (project_id)$ gcloud iam service-accounts list | grep EMAIL: | grep functions-2
EMAIL: functions-2@project_id.iam.gserviceaccount.com

以下コマンドの、--set-env-vars-service-account を書き換えて functions-2 関数デプロイを実行。

gcloud functions deploy functions-2 \
  --gen2 \
  --trigger-http \
  --runtime=python310 \
  --entry-point=make_authorized_get_request \
  --source=functions_2_3/ \
  --set-env-vars=ENDPOINT=<先程出力した functions-1 関数のURI> \
  --service-account=<先程出力した functions-2 関数のサービスアカウント>

functions-3 関数の作成

先程と同様の手順で functions-3 関数をデプロイする情報を取得し、以下のデプロイコマンドを実行する。

gcloud functions deploy functions-3 \
  --gen2 \
  --trigger-http \
  --runtime=python310 \
  --entry-point=make_authorized_get_request \
  --source=functions_2_3/ \
  --set-env-vars=ENDPOINT=<先程出力した functions-1 関数のURI> \
  --service-account=<先程作成した functions-3 関数のサービスアカウント>

権限の付与

functions-1 関数の呼び出し元権限 (roles/run.invoker) を、functions-2 関数のサービスアカウントのみに付与する。

gcloud run services add-iam-policy-binding functions-1 \
  --member='serviceAccount:functions-2@project_id.iam.gserviceaccount.com' \
  --role='roles/run.invoker' 

動作確認

実行 1

以下のコマンドから、functions-2 関数を実行する。

gcloud functions call functions-2  --gen2

以下のように、Hello HTTP!! とレスポンスがあれば成功です。

matayuuu@cloudshell:~/authenticationg_for_invocation (project_id)$ gcloud functions call functions-2 --gen2

  Hello HTTP!!

実行 2

以下のコマンドから、functions-3 関数を実行する。

gcloud functions call functions-3  --gen2

以下のように、エラーとなれば予測していた挙動となります。

matayuuu@cloudshell:~/authenticationg_for_invocation (project_id)$ gcloud functions call functions-3  --gen2
ERROR: gcloud crashed (HTTPError): 500 Server Error: Internal Server Error for url: https://functions-3-xxxxxxxxxx-an.a.run.app/

If you would like to report this issue, please run the following command:
  gcloud feedback

To check gcloud for common problems, please run the following command:
  gcloud info --run-diagnostics

Cloud Logging にて functions-3 のログを確認すると、以下のエラーが出力されてました。

Traceback (most recent call last):
〜(省略)〜
  File "/layers/google.python.runtime/python/lib/python3.10/urllib/request.py", line 643, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 403: Forbidden

HTTP リクエスト時に 403 エラーが返ってきてることがわかります。

functions-3 関数のサービスアカウントには functions-1 関数の呼び出し元権限がないため、HTTP リクエストに失敗していることが想定されます。

また、Cloud Logging にて functions-1 のログも確認すると、以下のエラーが出力されてました。

The request was not authenticated. Either allow unauthenticated invocations or set the proper Authorization header. Read more at https://cloud.google.com/run/docs/securing/authenticating Additional troubleshooting documentation can be found at: https://cloud.google.com/run/docs/troubleshooting#unauthorized-client

上記より、functions-3 関数からの呼び出しは認証されていない呼び出しだったことがわかりました。

もう少し深堀ってみる

curl コマンドで実行

functions-1 関数の呼び出しを、Cloud Shell から curl コマンドで呼び出すこともできます。

Cloud Functions のコンソール画面functions-1 関数を選択 > テスト中 タブを選択 > テストコマンド を確認すると以下のコマンドが出力されてました。

curl -m 70 -X POST https://functions-1-xxxxxxxxxx-an.a.run.app \
-H "Authorization: bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{
  "name": "Hello World"
}'

このコマンドを Cloud Shell から実行すると functions-1 関数からHello HTTP!!が返ってきました。

matayuuu@cloudshell:~ (project_id)$ curl -m 70 -X POST https://functions-1-xxxxxxxxxx-an.a.run.app \
> -H "Authorization: bearer $(gcloud auth print-identity-token)" \
> -H "Content-Type: application/json" \
> -d '{  "name": "Hello World" }'
Hello HTTP!!

functions-1 関数が呼び出せたことがわかります。

トークンの確認

先程のテストコマンドの 2 行目にあるリクエストヘッダーに注目いただくと、 Authorization: bearer $(gcloud auth print-identity-token) と記載があります。

確認のため、Cloud Shell にて gcloud auth print-identity-token 以下を実行すると以下の文字列が返ってきました。

matayuuu@cloudshell:~ (project_id)$ gcloud auth print-identity-token
eyJhbGciOiJSUzI1NiIsImtpZCI6ImFmYzRmYmE2NTk5ZmY1ZjYzYjcyZGM1MjI0MjgyNzg2ODJmM2E3ZjEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiNjE4MTA0NzA4MDU0LTlyOXMxYzRhbGczNmVybGl1Y2hvOXQ1Mm4zMm42ZGdxLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNjE4MTA0NzA4MDU0LTlyOXMxYzRhbGczNmVybGl1Y2hvOXQ1Mm4zMm42ZGdxLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTA0ODc3Mjg2MDgxMjE4NDY0NzA4IiwiaGQiOiJnLWdlbi5jby5qcCIsImVtYWlsIjoibWF0YXl1dXVAZy1nZW4uY28uanAiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlVGLWJuSHJSTzZBLW5kOE1zTVpBX0EiLCJpYXQiOjE2NzQzOTA0OTAsImV4cCI6MTY3NDM5NDA5MCwianRpIjoiMjlmY2VhOTQzMzFlNzMxOGIwNTFjNDQ4ZDUzOTBjMDBlNTQ5NDU4MSJ9.l1BfkuBlmYThwRkCpWFd7V3ATWSn-zwUfuLdChw6EKg3N0qyi2NYZioiDVEMFKy-OTbap4IpGzNYi3scpux9vMnGda-UDrDwWkN8iMhBpdD1ZVjiGew8kUYwOytr-2YRbk2dM_AnOTSIfB9dgj9drWsNZHrUuG6mnYK4l4zvwIRzzh4kwdtZpd0jLkmHHIaAORMHVkIdfx88peEK3BYKzdkNrwoveuG9VjZwtef0DHMtT0-Obmd0d7Yaa8v7Onzk0s0SrAwfvFyXrsi_wY8nRJ8C5f6ljNklLlyqxcZr8UFE-3Z64hYixWMjbTqgqpjFKq4v1Pxldm_MQauJ0RAg4A

また、functions_2_3 のコード内でトークンを取得する処理において、取得したトークンを確認するため print() するようにしています 。

    # 〜(抜粋)〜
    # サービスアカウントの認証情報を使用し functions-1 関数のトークンを取得
    auth_req = google.auth.transport.requests.Request()
    id_token = google.oauth2.id_token.fetch_id_token(auth_req, ENDPOINT)
    print(id_token)

再度、functions-2 関数を実行し Cloud Logging からログを確認すると、以下のトークンが print() されてました。

eyJhbGciOiJSUzI1NiIsImtpZCI6ImFmYzRmYmE2NTk5ZmY1ZjYzYjcyZGM1MjI0MjgyNzg2ODJmM2E3ZjEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Z1bmN0aW9ucy0xLXZlNXBzaHhjZHEtYW4uYS5ydW4uYXBwIiwiYXpwIjoiMTAxNzgwOTIxMzYzNDgwNTMyNTQ4IiwiZW1haWwiOiJmdW5jdGlvbnMtMkBwb2MtaW52b2tlLWdjZi1odHRwLTIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNjc0Mzc1Mjc2LCJpYXQiOjE2NzQzNzE2NzYsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInN1YiI6IjEwMTc4MDkyMTM2MzQ4MDUzMjU0OCJ9.l7IvSXFWQu2NfvmCQEzdZbWQ3mPtzO3myqW45Grw_bEedLLAe7DYO0x3iPMBtOrlVXdl4REn8wxGNaDqsL_UZBAePAI6PQJdVmNn-QA46JGPC6K74ZohR3KzWFI7_NEudnL_l9ZBVazyQUc34XfG9T9OXJgtCmZefwWYTMvJC8EYkqg4FnvEkw1xAeuwCr9pE1aqFSsCQTFU-VMwzxPpQ33AF49UMtMfU5qhmFoeBMFnwfpMt9tFwR-K9HE2fAqtysUUXfk_FJDAkxw25rN2uA0EKHIS-FQ738N-ZKKD7wjlhrDcoAalerSjBpU0UgUpArK9o7gFAu5Ztyd4qv0tvQ

gcloud auth print-identity-token で取得したトークンと比べてみると異なる文字列が返ってきました。

このトークンの正体は、 OpenID Connect(OIDC)トークンまたは ID トークンといい、Google によって署名された ID トークンであります。尚、存続期間は最大 1 時間 (デフォルト) となります。

Cloud Functions や Cloud Run の呼び出しにおいて、この ID トークンが必要になります。

トークンを取得する仕組み

トークンを取得するときの認証情報の仕組みについては、アプリケーションのデフォルト認証情報 (ADC)が使われています。

今回 ADC について詳しい説明は省略しますが、functions-2 関数では 接続されたサービスアカウント認証情報が ADC に提供されています。

また、Cloud Shell のような Google Cloud 上のクラウドベース開発環境を使用する場合は、ログイン時に指定した認証情報が ADC に提供されるため、プロジェクトレベルのオーナー権限を持った筆者のユーザーアカウント認証情報が提供されていたことがわかります。

トークンのデコード

No 実行環境 誰のトークン
1 Cloud Shell 筆者のユーザーアカウント認証情報で取得したトークン
2 functions-2 関数 functions-2 関数のサービスアカウント認証情報で取得したトークン

このトークンはエンコードされているため、jwt.io Debugger を使えばデコードができます。

Cloud Shell で出力したトークンをデコード

functions-2 関数で取得したトークンをデコード

又吉 佑樹(記事一覧)

クラウドソリューション部

はいさい、沖縄出身のクラウドエンジニア!

セールスからエンジニアへ転身。Google Cloud 全 11 資格保有。Google Cloud Champion Innovator (AI/ML)。Google Cloud Partner Top Engineer 2024。Google Cloud 公式ユーザー会 Jagu'e'r でエバンジェリストとして活動中。好きな分野は AI/ML。