ぽくつなです

GCP の Application Default Credentials を使った認証

公式ドキュメントで説明されているけど、同僚に何度か説明する機会があったり、作る必要のないサービスアカウントキーを目にすることも多いのでまとめておく。

認証情報が登場しないアプリケーションコード

例えば以下のコードで Secret Manager に保存したトークンを取得することができる。SecretManagerServiceClient にサービスアカウントキーを渡さずとも動作する。

const {SecretManagerServiceClient} = require('@google-cloud/secret-manager');
const client = new SecretManagerServiceClient();
(async () => {
  const [secret] = await client.accessSecretVersion({
    name: 'projects/pokutuna-playground/secrets/token/versions/latest',
  });
  console.log(secret.payload.data.toString('utf8'));
})();

これは僕の手元であれば動作するし、みなさんの手元ではエラーになるでしょう。 このコードでアクセス制御ができているのは、クライアントライブラリが実行環境から認証情報を解決しているからです。

Cloud SDKをインストールして gcloud auth application-default login し、name をあなたのシークレットに置き換えれば、あなたのシークレットが読めるでしょう。1

Application Default Credentials

Google が提供するクライアントライブラリは以下の優先順位で認証情報を解決している。
Application Default Credentials (ADC) はこの認証情報を解決するフロー、または得られた認証情報のことを指して呼ぶ。

  1. GOOGLE_APPLICATION_CREDENTIALS 環境変数に設定されたサービスアカウントキー(のファイルパス)
  2. ~/.config/gcloud/applicstion_default_credentials.json に配置された OAuth2 Secret
  3. メタデータサーバーに問い合わせて得られた認証情報

これは言語問わず共通して実装されており、Node の場合 google-auth-library-nodejs で実装され、googleapis や、@google-cloud/ 以下のクライアントライブラリで利用されている。

  1. はサービスアカウントキーを使って実行ユーザを上書きするための仕組み
    クライアントライブラリに明にサービスアカウントキー渡す場合 ADC より優先して使われるようになっている

  2. で利用されるファイルは gcloud auth application-default login によって配置される
    コマンドを実行することで、ブラウザが起動し Google アカウントログインが求められ、ログインしたユーザの認証情報が配置される。

  3. GCP 上の実行環境で解決される
    GCP 上の実行環境では、メタデータサーバーが、アクセス元に応じた認証情報を返してくれる。 Compute Engine、App Engine や Cloud Functions などではインスタンス作成時やデプロイ時にアタッチするサービスアカウントを指定できる。何も指定していなければデフォルトのサービスアカウントがアタッチされている。
    GKE では Workload Identity を利用して、Kubernetes Service Account と Google Service Account を紐付けて認証情報を解決できる。

この仕組みを使うことで

  • ローカル開発環境ではサービスアカウントキーを使わない(2を利用する)
  • GCP の実行環境ではサービスアカウントキーをデプロイしない(3に任せる)

となって、サービスアカウントキーが必要な場面はかなり限定される。

どうしてもサービスアカウントキーを指定したい場合も環境変数で与える(1を使う)ことで、アプリケーションコードに登場しないようにできる。

サービスアカウントキーを配るのを最小にしたい

公式ドキュメントにおいても、開発中は ADC に任せることを推奨している。

サービス アカウントの使用と管理のベスト プラクティス  |  Cloud IAM のドキュメント  |  Google Cloud

開発中にサービス アカウントを使用しない

日常業務で、gcloud コマンドライン ツール、gsutil、terraform などのツールを使用する場合があります。これらのツールの実行でサービス アカウントを使用しないでください。代わりに、gcloud auth login(gcloud ツールと gsutil の場合)または gcloud auth application-default login(terraform などのサードパーティ ツールの場合)を実行して、ツールがユーザーの認証情報を使用することを許可します。

キーの権限や流出に気を使う必要があるので、極力 ADC に寄せたい。

セキュリティ面以外では、1つのサービスアカウントにつきサービスアカウントキーは10個という管理上の制約もある。 開発用サービスアカウントキーを開発者のローカルに配るとすると、先着10回までしか生成できず、10人以上で開発できないし、古いものを消そうにも気を使う、という状況に陥る。

実際のところ、一番サービスアカウントキーを置きたくなるのは外部の CI だろう。
Github Actions などでは渋々配置することになる。AWS や Azure などからは、Workload Identity 連携を使うことで、サービスアカウントキーなしに認証できるようになった。外から BigQuery を叩きたい場合も Workload Identity を設定する手間を厭わなければキーが要らない。
別のファイルを置くことにはなるので管理やクライアントライブラリの呼び出し時に渡したりなどの手間は変わらないのと、サービスアカウントキーを生成するのに比べ Workload Identity の設定は数段面倒なので、諦めて置いてしまうことはある。

ADC を使うことによる不安と対策

GCP 上で動作させる際に ADC を利用しない理由はほぼない。
しかしローカル開発において、ADC に任せることで認証情報が暗黙的になり不安になるというのは共感する。

開発者はオーナー(roles/owner)などの強力な権限を持っていることが多く、複数のプロジェクトを操作するので、

  1. ローカルでは上手くいくがデプロイして権限が足りないことに気づく
  2. 意図したものと異なるプロジェクトに対して操作を実行してしまう

ということが起きうる。
この点では確かに明にサービスアカウントキーを渡す方がわかりやすく事故が起きにくい。

1 に関して、サービスアカウントになりすまして確認することができる。
アタッチする予定のサービスアカウントや、開発用サービスアカウントに対して roles/iam.serviceAccountTokenCreator ロールを持っているとサービスアカウントに成り代わって認証情報を取得できる。 このロールは2つのリソース(なりすます側とサービスアカウント)の間の設定であるため、オーナーでもデフォルトで持っていない。以下の手順で設定する。

メンバーに 1 つのサービス アカウントの権限の使用を許可する - サービス アカウントの権限借用の管理  |  Cloud IAM のドキュメント  |  Google Cloud

このロールを持っていると gcloud コマンドでは CLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNT={SA_EMAIL} 環境変数または --impersonate-service-account={SA_EMAIL} でなりすまして操作を実行できる。gcloud コマンドで、このサービスアカウントが PubSub トピックへメッセージをパブリッシュできるか確認できる。

2021-08 現在、まだクライアントライブラリも環境変数などから透過的に解決してくれるわけではなく追加の実装が必要である。

bq コマンドも impersonate に対応していなかったり対応が限定的、gcloud & gsutil 以外で気楽に使える状態ではないので、面倒なのでクライアントライブラリ側で解決するプロトコルを決めて実装してほしいものである。bq は長期的には gcloud bq に機能が移植されていって普段遣いできるようになるんじゃないかなあ〜

有効期間が短いサービス アカウント認証情報の作成  |  Cloud IAM のドキュメント  |  Google Cloud

2 に関して、破壊的な操作で起きると悲惨である。
認証情報が省略できるのと同様に、クライアントライブラリの呼び出し時にプロジェクトIDを省略することができる。 できるのだけど、あまりおすすめしない。ADC と同様にプロジェクトIDが探索されるのだが、GCLOUD_PROJECTGOOGLE_CLOUD_PROJECT 環境変数を設定していない場合、gcloud config list した際の core.project がデフォルトとして使われてしまい、落とし穴として働いてしまう。
(サービスアカウントキーを指定している場合はキーに含まれるプロジェクトIDが使われる、この辺からたどると分かる googleapis/google-auth-library-nodejs@2fcab77 - src/auth/googleauth.ts#L205L210)

対策としては

  • クライアントに渡すプロジェクトIDを省略しない
  • カレントプロジェクトを設定しない
    • gcloud config unset project するとカレントプロジェクトを設定しないままで居られる
    • 普段から gcloud --project=... で都度プロジェクトを指定する

ぐらいしかないので、ややイマイチである。

関連する話題

デフォルトのサービスアカウント

GCP の実行環境にアタッチするサービスアカウントを明に指定しない場合はデフォルトのものが使われる。

  • Compute Engine などでは {PROJECT_NUMBER}-compute@developer.gserviceaccount.com
  • App Engine や Cloud Functions などでは {PROJECT_ID}@appspot.gserviceaccount.com

デフォルトでは編集者(roles/editor)ロールという広い範囲を操作できる権限が付いている。 十分強力な権限が付いていることは、初心者に優しかったり、プロジェクト開始時にいきなり IAM の設定を要求されないという良い面もあるのだけど、慣れているユーザはあまり使いたくないだろう。必要最小限の role を与えたサービスアカウントをアタッチするのが理想的だけど、管理の手間もあるので個人的には以下のようにやっている。

  • 小さなプロジェクトでは気にせずデフォルトのサービスアカウントのまま動かす
  • デフォルトサービスアカウントを何かから参照したくなったら変える
    • 別プロジェクトの IAM に追加して権限を与えたくなったりキーを外部に置きたくなったとき

ちなみに Secret Manager のシークレットを読むには 編集者(roles/editor) では足りず、Secret Manager のシークレット アクセサー(roles/secretmanager.secretAccessor)ロールが必要である。

gcloud auth login と gcloud auth application-default login の違い

gcloud auth login は CloudSDK(gcloudコマンド) で使われる認証情報を設定する。

gcloud auth application-default login は ADC で使われる ~/.config/gcloud/application_default_credentials.json を配置する。

用途が分かれており、ADC を利用したくない場合でも gcloud コマンドの利用に不都合がないように分かれているものと思われる。

両方更新するには gcloud auth login --update-adc

ローカルの application_default_credentials.json を使うのを止めたい場合は gcloud auth application-default revoke で削除できる。

コンテナではどうするのか

google/cloud-sdk - Docker Image | Docker Hub では named volume に gcloud auth login した認証情報を保管して必要な時にマウントする方法が案内されている。application default についても同様にセットできるだろう。

ADC を使いたいだけなら、ホスト側で作った application_default_credential.json をマウントするのが楽である。

$ docker run -v ~/.config/gcloud/application_default_credentials.json:/root/.config/gcloud/application_default_credentials.json ...

Cloud SQL Proxy では実行ユーザが nonroot なのでマウント先を変えなければならない、このあたりは都度調べることになる。

$ docker run -p3306:3306 --rm --hostname=mysql -v ~/.config/gcloud/application_default_credentials.json:/home/nonroot/.config/gcloud/application_default_credentials.json gcr.io/cloudsql-docker/gce-proxy:latest /cloud_sql_proxy -instances=...

もちろん自作か Google 提供のイメージ以外に ~/.config/gcloud をマウントするのはリスクがあるので避けるべきである。

リンク

関連する話題への良いドキュメント

関連記事

blog.pokutuna.com


  1. 正確にはカレントプロジェクトの設定も必要になる