📖

GCP の Compute Metadata Credentials について

20 min read

Google Cloud Platform(以降 GCP) の認証認可については GCP と OAuth2 などの記事があるが、 Compute Metadata credential そのものの解説はあまり十分にされているとは言えないので、今回一つの記事で説明する。

Compute Metadata Credentials と聞いてもピンと来ない人向けに書くと、「Cloud Run や Cloud Functions などの GCP のプロダクトから GCP の API を呼ぶのにサービスアカウントキーが必要ないこと」の裏側がどのように動いているかの話について説明する記事である。

なお、 Compute Metadata Credentials の名称は一般的に確立しているとは言えないが、他に十分に統一された呼び名があるとは言えないため、この記事では Compute Metadata Credentials に統一している。

Compute Metadata Credentials とは

Compute Metadata Credentials は Google Cloud Platform(以降 GCP) の標準の credential である Application Default Credentials(AIP-4110 として定義, 以後 ADC) の一部の Default Credentials From Google Virtual Machines(AIP-4115) として定義されている。

Compute Metadata Credentials を使うことで、 Compute Metadata server 互換の実行環境から、実行環境にアタッチされたアカウント(通常は Service Account)の OAuth 2 アクセストークン及び OpenID Connect(OIDC) ID トークンを直接取得することができる。
これにより GCP 内から Google の API を呼んだり、その他 OIDC ID token を要求するエンドポイントにアクセスする必要がある際には、漏洩すると危険な Service Account Key の管理及び、 OAuth 2 の flow(この場合 JWT Bearer flow) が不要となる。

AWS ユーザ向けに説明すると、 IAM roles for Amazon EC2IAM Roles for Tasks
等をプログラムから使うために Default Credential Provider Chain の一部として使われている Instance profile credentials と Amazon ECS container credentials に対応する概念だと考えると良い。

Compute Engine の instance metadata の一部として導入され、 computeMetadata をパスの一部に持つため Compute Metadata (server) と呼ばれるが、環境によって Compute Metadata の実体は異なる。プログラムから見れば同じインターフェースとして提供されているので通常は実体を意識する必要はない。

curl で Compute Metadata Credentials を直接使う

Compute Metadata は単純なインターフェースなので、 Compute Engine や Cloud Shell など、任意のコマンドが実行できる環境であれば curl コマンドでも利用できる。プログラムコードを例示するよりも簡潔なため、まず curl を使って具体例を説明する。

token endpoint によるアクセストークンの取得

下記のように service-accounts/default/token endpoint (以降 token endpoint) にアクセスすると、結果の JSON の access_token フィールドから、実行環境にアタッチされたアカウントに紐付いた OAuth2 アクセストークンが得られる。

$ curl -s -H 'Metadata-Flavor: Google' http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
{"access_token":"ya29.*redacted*","expires_in":3508,"token_type":"Bearer"}

Google の Internal DNS では metadata.google.internal の FQDN が設定されているため、 IP アドレス 169.254.169.254 ではなく metadata, metadata.google.internal でもアクセス可能である。

$ dig +noall +answer metadata.google.internal 
metadata.google.internal. 3600	IN	A	169.254.169.254

更に下記のように /etc/hosts に書かれており DNS の lookup も必要ないため、通信の上では IP アドレスを指定してもホスト名を指定しても違いはない。

$ grep metadata /etc/hosts
169.254.169.254 metadata.google.internal  # Added by Google

なお、 Metadata-Flavor ヘッダは SSRF 対策のため v1 から必須となっており、指定しない場合は 403 エラーとなる。

$ curl -s http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
<!DOCTYPE html>
<html lang=en>
  <meta charset=utf-8>
  <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
  <title>Error 403 (Forbidden)!!1</title>
  <style>
    *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
  </style>
  <a href=//www.google.com/><span id=logo aria-label=Google></span></a>
  <p><b>403.</b> <ins>That’s an error.</ins>
  <p>Your client does not have permission to get URL <code>/computeMetadata/v1/instance/service-accounts/default/token</code> from this server. Missing Metadata-Flavor:Google header. <ins>That’s all we know.</ins>

また、 scope クエリパラメータで OAuth 2.0 scope を指定することができる実行環境も存在するが、 Compute Engine ではインスタンス作成時に指定したスコープから変更できないため単に無視される。

ちなみに Google の OAuth2 アクセストークンは tokeninfo endpoint を使うことで素性を確認できる。(RFC7662 の OAuth 2.0 Token Introspection とは互換性がないがほぼ同じ機能を持っている。)

$ ACCESS_TOKEN=$(curl -s -H 'Metadata-Flavor: Google' http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token | jq -r .access_token)
$ curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://oauth2.googleapis.com/tokeninfo

# アクセストークンに userinfo.email スコープがない場合
{
  "azp": "113576253568340515084",
  "aud": "113576253568340515084",
  "scope": "https://www.googleapis.com/auth/cloud-platform",
  "exp": "1624977176",
  "expires_in": "3550",
  "access_type": "online"
}
# アクセストークンに userinfo.email スコープがある場合
{
  "azp": "113576253568340515084",
  "aud": "113576253568340515084",
  "scope": "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/cloud-platform",
  "exp": "1624977285",
  "expires_in": "3569",
  "email": "463253289144-compute@developer.gserviceaccount.com",
  "email_verified": "true",
  "access_type": "online"
}

このケースでは https://www.googleapis.com/auth/userinfo.email スコープが設定されていないため、サービスアカウントの email は確認できないが、設定されている場合は確認できる。

なお、 OAuth 2 ベースなので、他の GCP の API を呼ぶ時も同様に Authorization ヘッダに Bearer トークンとしてアクセストークンを渡して使用する。

identity endpoint による OIDC ID トークンの取得

サポートしていない実行環境も存在するが、通常は Compute Metadata の service-accounts/default/identity endpoint(以降 identity endpoint) を使って実行環境にアタッチされたアカウントに紐付いた OIDC ID トークンを取得することができる。audience は audience クエリパラメータで指定することができる。

$ curl -s -H 'Metadata-Flavor: Google' http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://example.com
eyJ*redacted*

結果は Google によって署名された OIDC ID token となる。 Google は OpenID Certified であり、 Discovery Document と合わせることで任意の OIDC compatible なサードパーティシステムとの連携が可能であると考えられる。

GCP 内だと Cloud IAP や Cloud Run/Cloud Functions invoker でのサービス間認証に使うことができる。

OIDC の規格上は aud claim は配列にできるが、 identity endpoint は単一の audience のみをサポートする。

なお、 JWT Debugger などの任意の方法で ID token の内容を確認すると、実行環境が Compute Engine の場合は上記のような最低限のクエリパラメータでは payload 部には下記のような claims のみが含まれていることが確認できる。

{
  "aud": "https://example.com",
  "azp": "113576253568340515084",
  "exp": 1624965161,
  "iat": 1624961561,
  "iss": "https://accounts.google.com",
  "sub": "113576253568340515084"
}

これは デフォルトでは format=standard が指定されているものとして扱われているためである。

https://cloud.google.com/compute/docs/instances/verifying-instance-identity?hl=en#request_signature

FORMAT: the optional parameter that specifies whether or not the project and instance details are included in the payload. Specify full to include this information in the payload or standard to omit the information from the payload. The default value is standard.

format=full を指定することでドキュメントに書かれた全ての情報が ID token に付与される。

$ curl -H 'Metadata-Flavor: Google' 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://example.com&format=full'
{
  "aud": "https://example.com",
  "azp": "113576253568340515084",
  "email": "463253289144-compute@developer.gserviceaccount.com",
  "email_verified": true,
  "exp": 1624965429,
  "google": {
    "compute_engine": {
      "instance_creation_timestamp": 1624960269,
      "instance_id": "8000458455602648036",
      "instance_name": "test-instance",
      "project_id": "apstndb-sandbox",
      "project_number": 463253289144,
      "zone": "asia-northeast1-a"
    }
  },
  "iat": 1624961829,
  "iss": "https://accounts.google.com",
  "sub": "113576253568340515084"
}

https://cloud.google.com/iap/docs/authentication-howto?hl=en#obtaining_an_oidc_token_for_the_default_service_account

Note: Ensure that the generated ID token has an 'email' claim. This is a requirement for IAP.

と書かれているように Cloud IAP は email claim を要求するため、 Compute Engine と Cloud IAP の組み合わせで使われる可能性がある場合は format=full を指定する必要がある。

なお、各サーバーレスプロダクトなどの Compute Engine ベースではない環境では単に format クエリパラメータは無視され、 email は Service Account のメールアドレスとなる。

ライブラリから Compute Metadata Credentials を使う

Google が公式にサポートする言語には Application Default Credentials を実装する Google Unified Auth Clients(GUAC) が存在する。

https://google.aip.dev/auth/4110

Auth libraries following the standards in these AIPs are known as "Google Unified Auth Clients", or GUAC for short. The resulting libraries are colloquially called GUACs.

どれが GUAC であるのかは特に言及されていないが、下記のライブラリであると考えられる。

言語 GUAC
Go golang.org/x/oauth2/google
Java googleapis/google-auth-library-java
.NET Google.Apis.Auth.OAuth2
Node.js googleapis/google-auth-library-nodejs
Python googleapis/google-auth-library-python
Ruby googleapis/google-auth-library-ruby
PHP googleapis/google-auth-library-php
C++ googleapis/google-cloud-cpp
Swift googleapis/google-auth-library-swift
Dart googleapis_auth

GCP の公式のクライアントライブラリはこれらの GUAC を内部的に使うため、クライアントライブラリを使って素直に実装すれば何も意識せずとも Compute Metadata Credentials が提供される任意の実行環境で GCP の API にアクセスすることができる。

OIDC ID token の生成については、 Google の API 呼び出しとは異なるので暗黙的には行われない。Cloud IAP のドキュメントの Obtaining an OIDC token for the default service account に一通りの言語での例があるのでこれを参考にすると良いだろう。
なお、 Go の golang.org/x/oauth2/google は GUAC 相当の位置付けではあるが、 Compute Metadata を使った OIDC ID token の取得はまだ含まれていないので、 google.golang.org/api/idtoken パッケージを使用する必要がある。
これは OIDC ID token が ADC の定義と最近統合されたことも関係している可能性がある。

Compute Metadata に対応した GCP のプロダクト

Compute Metadata に対応した GCP のプロダクトを独自に調査して下記表にまとめた。

Product ID token Default Service Account 任意の Service Account 使用 備考
Compute Engine PROJECT_NUMBER-compute@developer.gserviceaccount.com
Cloud Run PROJECT_NUMBER-compute@developer.gserviceaccount.com(Compute Engine と共通)
Cloud Functions YOUR_PROJECT_ID@appspot.gserviceaccount.com(App Engine と共通)
App Engine Standard Environment YOUR_PROJECT_ID@appspot.gserviceaccount.com PHP 5 と Python 2.7 の2つの 1st gen runtime は compute metadata に対応していないため、 App Engine Legacy API である App Idenitty service を通してアクセストークンを取得する。
App Engine Flexible Envorinment YOUR_PROJECT_ID@appspot.gserviceaccount.com 未対応(option あり) custom runtime 含め全て同様に動作する。
Kubernetes Engine Workload Identity なし GKE Metadata Server は kube-system/gke-metadata-server DaemonSet が実体。トークン発行時に roles/iam.workloadIdentityUser が必要。Keyless Entry: Securely Access GCP Services From Kubernetes (Cloud Next '19) の 24:20〜 に裏で行われていることの説明あり。
Cloud Build × [PROJECT_NUMBER]@cloudbuild.gserviceaccount.com 実体は cloudbuild network 経由の metadata コンテナで、ローカルエミュレータの cloud-build-local同様の構造になっていると考えられる。※ manual build 以外の build trigger は未対応
Cloud Shell △ ※ Cloud Console を使っているユーザアカウント × 最初に Authorize Cloud Shell ダイアログが出る。※ identity endpoint を叩くことはできるが、 audience パラメータが無視されるため Cloud IAP などの一般的な用途に利用できない。

各列の意味は下記の通り。

  • Product: プロダクト名
  • idtoken: identity endpoint から OIDC ID トークンが取得可能かどうか
  • Default Service Account: デフォルトで使われる Service Account の email
  • 任意の Service Account 使用: Default Service Account 以外の Service Account をアタッチする手段があるかどうか
  • 備考: その他備考

(TODO: ビッグデータ系などその他のプロダクトの追加)

Service Account Key ではできるが Compute Metadata Credentials ではできないこと

Compute Metadata は特定の Service Account Key ではなく直接 Service Account に紐付いた OAuth2 アクセストークンと OIDC ID トークンを取得できるため、殆どのユースケースでは Service Account Key を意識する必要はなくなる。
数少ない反例に Service Account Key でのみ秘密鍵を使って署名をすることができるというものがある。ユースケースには下記がある。

  • Cloud Storage の Signed URL の作成
  • 一般的な Self-Signed JWT の作成

これらに対応するために Compute Metadata ではなく User-managed Service Account Key の JSON ファイルを作成することもできるが、 Compute Metadata の優位性を大きく損なうこととなる。
Compute Metadata Credentials が使える環境で署名をしたい場合、 Service Account Credentials APIsignBlob/signJwt メソッドを使って、その Service Account 自身の System-managed Service Account Key で署名することで、漏洩の危険がある User-managed Service Account Key を作らずに実装することができる。

特定の Service Account に対して Service Account Credentials API を呼ぶには Service Account Token Creator ロール(roles/iam.serviceAccountTokenCreator) が必要となるため、署名が必要な Service Account は自分自身への Token Creator ロールを持つこととなる。Token Creator ロールは有効な User-managed Service Account Key の秘密鍵を持っているのと同じ力があると言える。

Service Account Credentials API を使って署名する場合に注意すべき点として、下記のように最短で署名を行ってから12時間で System-managed Service Account Key がローテーションされ、署名が無効になる可能性があることが上げられる。署名が12時間以上有効であることが要件の場合は User-managed Service Account Key を使うことも検討する必要がある。

https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob?hl=en#response-body

The key used for signing will remain valid for at least 12 hours after the blob is signed.

Signed URL の実装

Signed URL は Credentials API の signBlob を使って実装できる(が、最短で12時間で使えなくなる可能性がある)ことはドキュメント上にも記載があるが、具体的な実装方法は Signed URL を実装したライブラリによって異なる。

https://cloud.google.com/storage/docs/access-control/signed-urls?hl=en#signing-iam

When generating a signed URL using a program, one option for signing the string is to use the IAM signBlob method provided by Google Cloud. The Signature that is output from this method is used when assembling the signed URL.
The signBlob service regularly rotates the private key that it uses. Signed URLs generated are usable for at least 12 hours, but may stop working prior to your set expiration time if the expiration time is greater 12 hours. Given this, signed URLs generated from signBlob are best used for short-lived access to resources.

全ての言語については把握していないが、 Go では下記の issue などを参考にして実装すると良いだろう。

https://github.com/GoogleCloudPlatform/golang-samples/issues/2079

手前味噌ではあるが、 apstndb/adcplussigner.SmartSigner として credential の差異を吸収してこれらの署名ができる実装を公開しているのでこれも参考にしても良いだろう。(SignJwt 周りはあまり相性が良い JWT ライブラリがなかったため自前で実装しており、プロダクションレディではないと考えられる。)