Cloud Run への安全なデプロイ

1. 概要

Cloud Run にサービスをデプロイするためのデフォルトの手順を変更してセキュリティを強化し、デプロイされたアプリに安全にアクセスする方法を確認します。このアプリは、Cymbal Eats と提携して食品の注文を処理する企業が使用する、Cymbal Eats アプリケーションの「パートナー登録サービス」です。

学習内容

Cloud Run にアプリをデプロイするための最小限のデフォルトの手順に少し変更を加えるだけで、セキュリティを大幅に強化できます。既存のアプリとデプロイ手順を使用して、デプロイされたアプリのセキュリティを強化するようにデプロイ手順を変更します。

次に、アプリへのアクセスを承認し、承認されたリクエストを行う方法について説明します。

これはアプリケーションのデプロイのセキュリティを網羅的に説明するものではなく、今後のすべてのアプリのデプロイに適用することで、ほとんど手間をかけずにセキュリティを強化できる変更について説明するものです。

2. 設定と要件

セルフペース型の環境設定

  1. Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列です。この設定はいつでも変更できます。
  • プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud コンソールでは一意の文字列が自動生成されます。通常は、この内容を意識する必要はありません。ほとんどの Codelab では、プロジェクト ID(通常は PROJECT_ID と識別されます)を参照する必要があります。生成された ID が好みではない場合は、ランダムに別の ID を生成できます。または、ご自身で試して、利用可能かどうかを確認することもできます。このステップ以降は変更できず、プロジェクトを通して同じ ID になります。
  • なお、3 つ目の値として、一部の API が使用するプロジェクト番号があります。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
  1. 次に、Cloud のリソースや API を使用するために、Cloud コンソールで課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルの終了後に請求が発生しないようにリソースをシャットダウンするには、作成したリソースを削除するか、プロジェクト全体を削除します。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。

Cloud Shell をアクティブにする

  1. Cloud Console で、[Cloud Shell をアクティブにする] 853e55310c205094.png をクリックします。

55efc1aaa7a4d3ad.png

Cloud Shell を初めて起動した場合は、その内容を説明する画面が(スクロールしなければ見えない位置に)表示されます。その場合は、[続行] をクリックしてください(以後表示されなくなります)。この中間画面は次のようになります。

9c92662c6a846a5c.png

すぐにプロビジョニングが実行され、Cloud Shell に接続されます。

9f0e51b578fecce5.png

この仮想マシンには、必要な開発ツールがすべて用意されています。仮想マシンは Google Cloud で稼働し、永続的なホーム ディレクトリが 5 GB 用意されているため、ネットワークのパフォーマンスと認証が大幅に向上しています。このコードラボでの作業のほとんどは、ブラウザまたは Chromebook から実行できます。

Cloud Shell に接続すると、すでに認証は完了しており、プロジェクトに各自のプロジェクト ID が設定されていることがわかります。

  1. Cloud Shell で次のコマンドを実行して、認証されたことを確認します。
gcloud auth list

コマンド出力

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Cloud Shell で次のコマンドを実行して、gcloud コマンドがプロジェクトを認識していることを確認します。
gcloud config list project

コマンド出力

[core]
project = <PROJECT_ID>

上記のようになっていない場合は、次のコマンドで設定できます。

gcloud config set project <PROJECT_ID>

コマンド出力

Updated property [core/project].

環境設定

このラボでは、Cloud Shell のコマンドラインでコマンドを実行します。通常はコマンドをコピーしてそのまま貼り付けることができますが、プレースホルダの値を正しい値に変更する必要がある場合もあります。

  1. 後のコマンドで使用するために、プロジェクト ID の環境変数を設定します。
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
export SERVICE_NAME=partner-registration-service
  1. アプリを実行する Cloud Run サービス API、NoSQL データ ストレージを提供する Firestore API、デプロイ コマンドで使用される Cloud Build API、ビルド時にアプリケーション コンテナを保持するために使用される Artifact Registry を有効にします。
gcloud services enable \
  run.googleapis.com \
  firestore.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com
  1. ネイティブ モードで Firestore データベースを初期化します。このコマンドは App Engine API を使用するため、まず有効にする必要があります。

このコマンドでは、App Engine のリージョン(使用しないが、過去の理由で作成する必要がある)とデータベースのリージョンを指定する必要があります。App Engine には us-central を使用し、データベースには nam5 を使用します。nam5 は米国のマルチリージョン ロケーションです。マルチリージョン ロケーションは、データベースの可用性と耐久性を最大化します。

gcloud services enable appengine.googleapis.com

gcloud app create --region=us-central
gcloud firestore databases create --region=nam5
  1. サンプルアプリ リポジトリのクローンを作成してディレクトリに移動する
git clone https://github.com/GoogleCloudPlatform/cymbal-eats.git

cd cymbal-eats/partner-registration-service

3. README を確認する

エディタを開き、アプリを構成するファイルを確認します。このアプリのデプロイに必要な手順が記載されている README.md を確認します。これらの手順の一部には、考慮すべき暗黙的または明示的なセキュリティ上の決定が含まれている場合があります。デプロイされたアプリのセキュリティを強化するために、これらの選択肢の一部を変更します。詳細については、こちらをご覧ください。

ステップ 3 - npm install を実行する

アプリで使用されるサードパーティ ソフトウェアの出所と完全性を把握することが重要です。ソフトウェア サプライ チェーンのセキュリティ管理は、Cloud Run にデプロイされるアプリだけでなく、あらゆるソフトウェアの構築に関連します。このラボではデプロイに重点を置いているため、この領域については説明しませんが、このトピックについて別途調査することをおすすめします。

ステップ 4 と 5 - deploy.sh を編集して実行する

これらの手順では、ほとんどのオプションをデフォルトのままにして、アプリを Cloud Run にデプロイします。このステップを変更して、次の 2 つの重要な方法でデプロイのセキュリティを強化します。

  1. 未認証アクセスを許可しないでください。探索中に試してみるには便利ですが、これは商用パートナーが使用するウェブ サービスであり、常にユーザーを認証する必要があります。
  2. アプリケーションが、必要以上の API とリソースへのアクセス権を持つ可能性のあるデフォルトのサービス アカウントではなく、必要な権限のみで調整された専用のサービス アカウントを使用するように指定します。これは最小権限の原則と呼ばれ、アプリケーション セキュリティの基本的なコンセプトです。

ステップ 6 ~ 11 - サンプル ウェブ リクエストを作成して、正しい動作を確認する

アプリケーションのデプロイに認証が必要になったため、これらのリクエストにはリクエスト送信者の ID の証明を含める必要があります。これらのファイルを変更する代わりに、コマンドラインから直接リクエストを行います。

4. サービスを安全にデプロイする

deploy.sh スクリプトで必要な変更として、認証されていないアクセスを許可しないことと、最小限の権限を持つ専用のサービス アカウントを使用することの 2 つが特定されました。

まず、新しいサービス アカウントを作成します。次に、そのサービス アカウントを参照し、認証されていないアクセスを禁止するように deploy.sh スクリプトを編集します。次に、変更した deploy.sh スクリプトを実行する前に、変更したスクリプトを実行してサービスをデプロイします。

サービス アカウントを作成し、Firestore/Datastore への必要なアクセス権を付与する

gcloud iam service-accounts create partner-sa

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:partner-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role=roles/datastore.user

deploy.sh の編集

deploy.sh ファイルを変更して、未認証アクセスを禁止(–no-allow-unauthenticated)し、デプロイされたアプリの新しいサービス アカウント(–service-account)を指定します。GOOGLE_PROJECT_ID を独自のプロジェクトの ID に修正します。

最初の 2 行を削除し、他の 3 行を次のように変更します。

gcloud run deploy $SERVICE_NAME \
  --source . \
  --platform managed \
  --region ${REGION} \
  --no-allow-unauthenticated \
  --project=$PROJECT_ID \
  --service-account=partner-sa@${PROJECT_ID}.iam.gserviceaccount.com

サービスをデプロイする

コマンドラインから deploy.sh スクリプトを実行します。

./deploy.sh

デプロイが完了すると、コマンド出力の最後の行に新しいアプリのサービス URL が表示されます。URL を環境変数に保存します。

export SERVICE_URL=<URL from last line of command output>

curl ツールを使用して、アプリから注文を取得してみましょう。

curl -i -X GET $SERVICE_URL/partners

curl コマンドの -i フラグは、レスポンス ヘッダーを出力に含めるように指示します。出力の最初の行は次のようになります。

HTTP/2 403

アプリは、認証されていないリクエストを許可しないオプションを使用してデプロイされました。この curl コマンドには認証情報が含まれていないため、Cloud Run によって拒否されます。実際にデプロイされたアプリケーションは、このリクエストを実行したり、このリクエストからデータを受信したりすることはありません。

5. 認証済みリクエストを行う

デプロイされたアプリはウェブ リクエストを行うことで呼び出されます。Cloud Run で許可するには、このリクエストを認証する必要があります。ウェブ リクエストは、次の形式の Authorization ヘッダーを含めることで認証されます。

Authorization: Bearer identity-token

ID トークンは、信頼できる認証プロバイダによって発行される、短期間有効な暗号署名付きのエンコードされた文字列です。この場合、有効期限内の有効な Google 発行の ID トークンが必要です。

ユーザー アカウントとしてリクエストを行う

Google Cloud CLI ツールは、デフォルトの認証済みユーザーのトークンを提供できます。次のコマンドを実行して、自分のアカウントの ID トークンを取得し、ID_TOKEN 環境変数に保存します。

export ID_TOKEN=$(gcloud auth print-identity-token)

デフォルトでは、Google が発行した ID トークンの有効期間は 1 時間です。次の curl コマンドを実行して、承認されていないために以前に拒否されたリクエストを行います。このコマンドには、必要なヘッダーが含まれます。

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $ID_TOKEN"

コマンドの出力は HTTP/2 200 で始まり、リクエストが受け入れられ、処理されていることを示します。(1 時間待ってからこのリクエストを再度試行すると、トークンの有効期限が切れているため失敗します)。レスポンスの本文は、出力の末尾の空行の後にあります。

{"status":"success","data":[]}

パートナーはまだいません。

ディレクトリ内のサンプル JSON データを使用して、次の 2 つの curl コマンドでパートナーを登録します。

curl -X POST \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@example-partner.json" \
  $SERVICE_URL/partner

curl -X POST \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@example-partner2.json" \
  $SERVICE_URL/partner

前の GET リクエストを繰り返して、登録済みのすべてのパートナーを表示します。

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $ID_TOKEN"

登録済みの 2 つのパートナーに関する情報を含む、より多くのコンテンツを含む JSON データが表示されます。

承認されていないアカウントとしてリクエストを行う

最後のステップで作成した認証済みリクエストが成功したのは、認証されただけでなく、認証済みユーザー(あなたのアカウント)が承認されたためです。つまり、アカウントにはアプリを呼び出す権限がありました。認証済みアカウントのすべてにその権限が付与されているわけではありません。

前のリクエストで使用されたデフォルトのアカウントは、アプリを含むプロジェクトを作成したアカウントであるため、承認されました。デフォルトでは、アカウント内の任意の Cloud Run アプリケーションを呼び出す権限が付与されます。この権限は必要に応じて取り消すことができます。これは、本番環境のアプリケーションでは望ましいことです。ここでは、権限やロールが割り当てられていない新しいサービス アカウントを作成し、それを使用してデプロイされたアプリにアクセスしてみます。

  1. tester というサービス アカウントを作成します。
gcloud iam service-accounts create tester
  1. この新しいアカウントの ID トークンは、以前にデフォルト アカウントの ID トークンを取得したときとほぼ同じ方法で取得できます。ただし、これには、デフォルトのアカウントにサービス アカウントの権限を借用する権限が必要です。アカウントにこの権限を付与します。
export USER_EMAIL=$(gcloud config list account --format "value(core.account)")

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="user:$USER_EMAIL" \
  --role=roles/iam.serviceAccountTokenCreator
  1. 次のコマンドを実行して、この新しいアカウントの ID トークンを TEST_IDENTITY 環境変数に保存します。コマンドでエラー メッセージが表示された場合は、1 ~ 2 分待ってからもう一度お試しください。
export TEST_TOKEN=$( \
  gcloud auth print-identity-token \
    --impersonate-service-account \
    "tester@$PROJECT_ID.iam.gserviceaccount.com" \
)
  1. 以前と同様に、この ID トークンを使用して認証済みウェブ リクエストを行います。
curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

リクエストは認証されていますが、認可されていないため、コマンド出力は再び HTTP/2 403 で始まります。新しいサービス アカウントには、このアプリを呼び出す権限がありません。

アカウントの承認

Cloud Run サービスにリクエストを行うには、ユーザーまたはサービス アカウントに Cloud Run サービスの Cloud Run 起動元ロールが必要です。次のコマンドを使用して、テスター サービス アカウントにそのロールを付与します。

export REGION=us-central1
gcloud run services add-iam-policy-binding ${SERVICE_NAME} \
  --member="serviceAccount:tester@$PROJECT_ID.iam.gserviceaccount.com" \
  --role=roles/run.invoker \
  --region=${REGION}

新しいロールが更新されるまで 1 ~ 2 分待ってから、認証済みリクエストを繰り返します。TEST_TOKEN が最初に保存されてから 1 時間以上経過している場合は、新しい TEST_TOKEN を保存します。

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

コマンド出力は HTTP/1.1 200 OK で始まり、最後の行に JSON レスポンスが含まれています。このリクエストは Cloud Run によって受け入れられ、アプリによって処理されました。

6. プログラムの認証とユーザーの認証

これまでに行った認証済みリクエストでは、curl コマンドライン ツールが使用されていました。代わりに利用できるツールやプログラミング言語は他にもあります。ただし、認証された Cloud Run リクエストは、プレーンなウェブページを含むウェブブラウザを使用して行うことはできません。ユーザーがウェブページのリンクをクリックするか、ボタンをクリックしてフォームを送信すると、ブラウザは認証済みリクエストで Cloud Run が必要とする Authorization ヘッダーを追加しません。

Cloud Run の組み込み認証メカニズムは、エンドユーザーではなくプログラムで使用することを目的としています。

注:

Cloud Run はユーザー向けのウェブ アプリケーションをホストできますが、このようなアプリケーションでは、ユーザーのウェブブラウザからの認証なしのリクエストを許可するように Cloud Run を設定する必要があります。アプリケーションでユーザー認証が必要な場合、Cloud Run に認証を求めるのではなく、アプリケーションで認証を処理する必要があります。アプリケーションは、Cloud Run の外部のウェブ アプリケーションと同じ方法でこれを行うことができます。その方法については、この Codelab の範囲外です。

これまでのリクエスト例に対するレスポンスは、ウェブページではなく JSON オブジェクトでした。これは、このパートナー登録サービスがプログラムで使用することを目的としており、JSON がプログラムにとって便利な形式であるためです。次に、このデータを使用するプログラムを作成して実行します。

Python プログラムからの認証済みリクエスト

プログラムは、標準の HTTP ウェブ リクエストを介して、保護された Cloud Run アプリケーションの認証済みリクエストを行うことができますが、Authorization ヘッダーを含める必要があります。これらのプログラムの新しい課題は、そのヘッダーに配置する有効な期限切れでない ID トークンを取得することだけです。このトークンは、Google Cloud Identity and Access Management(IAM)を使用して Cloud Run によって検証されるため、IAM で認識される権限によって発行され、署名されている必要があります。多くの言語で利用できるクライアント ライブラリがあり、プログラムでそのようなトークンの発行をリクエストできます。この例で使用するクライアント ライブラリは、Python google.auth です。一般的にウェブ リクエストを行うための Python ライブラリはいくつかありますが、この例では一般的な requests モジュールを使用します。

まず、次の 2 つのクライアント ライブラリをインストールします。

pip install google-auth
pip install requests

デフォルト ユーザーの ID トークンをリクエストする Python コードは次のとおりです。

credentials, _ = google.auth.default()
credentials.refresh(google.auth.transport.requests.Request())
identity_token = credentials.id_token

Cloud Shell やパソコンの標準ターミナル シェルなどのコマンドシェルを使用している場合、デフォルトのユーザーは、そのシェル内で認証されたユーザーになります。Cloud Shell では、通常は Google にログインしているユーザーです。それ以外の場合は、gcloud auth login または他の gcloud コマンドで認証されたユーザーになります。ユーザーがまだログインしていない場合、デフォルトのユーザーが存在しないため、このコードは失敗します。

別のプログラムにリクエストを行うプログラムの場合、通常は個人の ID ではなく、リクエストを行うプログラムの ID を使用します。そのような場合のためにサービス アカウントがあります。Cloud Run サービスを、Cloud Firestore などの API リクエストを行う際に使用する ID を提供する専用のサービス アカウントでデプロイしました。プログラムが Google Cloud プラットフォームで実行されると、クライアント ライブラリは、割り当てられたサービス アカウントをデフォルトの ID として自動的に使用するため、同じプログラム コードが両方の状況で機能します。

Authorization ヘッダーを追加してリクエストを行う Python コードは次のとおりです。

auth_header = {"Authorization": "Bearer " + identity_token}
response = requests.get(url, headers=auth_header)

次の完全な Python プログラムは、Cloud Run サービスに認証済みリクエストを送信して、登録済みのすべてのパートナーを取得し、名前と割り当てられた ID を出力します。次のコマンドをコピーして実行し、このコードをファイル print_partners.py に保存します。

cat > ./print_partners.py << EOF
def print_partners():
    import google.auth
    import google.auth.transport.requests
    import requests

    credentials, _ = google.auth.default()
    credentials.refresh(google.auth.transport.requests.Request())
    identity_token = credentials.id_token

    auth_header = {"Authorization": "Bearer " + identity_token}
    response = requests.get("${SERVICE_URL}/partners", headers=auth_header)

    parsed_response = response.json()
    partners = parsed_response["data"]

    for partner in partners:
        print(f"{partner['partnerId']}: {partner['name']}")


print_partners()
EOF

このプログラムはシェル コマンドで実行します。プログラムがこれらの認証情報を使用できるように、まずデフォルト ユーザーとして認証する必要があります。次の gcloud auth コマンドを実行します。

gcloud auth application-default login

手順に沿ってログインを完了します。次に、コマンドラインからプログラムを実行します。

python print_partners.py

出力は次のようになります。

10102: Zippy food delivery
67292: Foodful

プログラムのリクエストは、ユーザーの ID で認証されたため、Cloud Run サービスに到達しました。ユーザーはこのプロジェクトのオーナーであるため、デフォルトで実行する権限が付与されています。このプログラムは、サービス アカウントの ID で実行されることが一般的です。Cloud Run や App Engine などのほとんどの Google Cloud プロダクトで実行される場合、デフォルトの ID はサービス アカウントになり、個人用アカウントの代わりに使用されます。

7. 完了

お疲れさまでした。これでこの Codelab は終了です。

次のステップ:

Cymbal Eats の他の Codelab をご覧ください。

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud アカウントに課金されないようにするには、リソースを含むプロジェクトを削除するか、プロジェクトを維持して個々のリソースを削除します。

プロジェクトの削除

課金をなくす最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。