🛡️

GKE のワークロードから GCP サービスへ 安全 にアクセスする 〜 Workload Identity 入門 〜

2023/07/04に公開

初めに

GKE(Google Kubernetes Engine) を利用したアプリケーション開発において、GKE クラスタ内で実行されているワークロードから GCP サービスにアクセスすることは頻繁にあります。
アプリケーションから GCP サービスを扱うためには Google Cloud API に対して、HTTP / gRPC リクエストを送信します。基本的に クライアント ライブラリ を使い実装することが多いかと思います。

本記事ではアプリケーションから GCP サービスにアクセスする際の "認証" について触れます。GKE 上のワークロードが安全且つ管理しやすい方法で GCP サービスにアクセスするために重要です。
認証方法はいくつかあるのですが、特に推奨されている Workload Identity を用いたアプリケーション認証方法にフォーカスを当て紹介しようと思います!

クライアント ライブラリと Application Default Credentials

GCP クライアント ライブラリは GKE のワークロードのみならず、あらゆる環境(Compute Engine、Cloud Functions、ローカル環境など)のアプリケーションで用いることができます。クライアント ライブラリを使用することで、簡単に Google Cloud APIs にアクセスし GCP サービスを制御することをできるため、アプリケーションのコード量を大幅に削減することが期待できます。

また、クライアント ライブラリは アプリケーションデフォルト認証(Application Default Credentials)(以下、ADC) をサポートしており、開発者は認証処理をクライアント ライブラリにオフロードすることができます。ADC は認証情報を自動的に見つけるためのクライアント ライブラリ(で使われている認証ライブラリ)のストラテジで、以下に従い認証情報を自動で探索します。

特定のパスに存在する認証情報(JSON ファイル)を使用する

GOOGLE_APPLICATION_CREDENTIALS 環境変数で認証情報の JSON ファイルの場所を指定します。IAM サービスアカウント(以下、IAM SA)キーは指定できるファイルの一つです。
またローカル環境で GCP サービスを扱うときにお馴染みの gcloud auth application-default login を実行することで、ユーザー認証情報を実行環境に配置して ADC から使うことができます。


IAM SA キーを用いて GCP サービスにアクセスする例

ADC では、まず GOOGLE_APPLICATION_CREDENTIALS 環境変数 で指定されたファイル、次に gcloud auth application-default login で配置されるファイルを順繰り探していきます。これらが検出されない場合は、次の方法で認証情報の検出を試みます。

Compute Engine に関連付いたサービスアカウントを使用する

指定の場所に認証情報がない場合、ADC は実行環境である VM インスタンス(Compute Engine、App Engine、Cloud Functions など)に関連付いた IAM SA の認証情報を使うことを試みます。
IAM SA の認証情報は メタデータサーバー に問い合わせることで取得することができます。


メタデータサーバーから関連付いた IAM SA の認証情報を取得して GCP サービスにアクセスする例

メタデータサーバーには VM インスタンスにまつわるメタデータが key:value ペアとして保存されています。また、VM からメタデータサーバーへのアクセスは http://metadata.google.internal に対して行い、これは自動的に許可されます。

メタデータ値 はいろいろな種類があり、実際に問い合わせてメタデータを取得してみます。

  • プロジェクトメタデータ(プロジェクトID):http://metadata.google.internal/computeMetadata/v1/project/project-id
    $ curl -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/project/project-id
    study-work-387200/ # VM の project-id を取得
    
  • VM インスタンスのメタデータ(VM に関連付けられた default`` IAM SA の認証情報):http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default```
    $ curl -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
    {"access_token":"***","expires_in":3398,"token_type":"Bearer"} # VM に関連付けられた IAM SA の認証情報
    

このように、メタデータサーバーから IAM サービスアカウントの認証情報を取得し、GCP サービスにアクセスします。これらの認証はクライアント ライブラリ内部で行われています。便利ですね。

GKE 上のワークロードの認証

さて、前置きが長くなりましたが、ここからが本題の GKE 上で実行されているワークロードの認証についてです。上述した方法とクライアント ライブラリを用いた GKE 上における認証方法について記します。加えて、要件次第で生じる不都合についても言及し、上述した方法ではなく "GKE Workload Identity のススメ" に入っていきます。

実行環境に認証情報を配置するパターン

このパターンでは、あらかじめエクスポートした IAM SA を K8s Secret リソースとして GKE 環境にデプロイし、Pod にマウントします。実施方法についてのドキュメントは こちら です。


GKE 上で IAM SA キーを Secret として Pod にマウント/使用し GCP サービスにアクセスする例

こちらの方法は以下のセキュリティ的な観点で非推奨とされています。

  • IAM SA キーの有効期限が長く、手動でログローテーションが必要な点
  • エクスポートした IAM SA の流出リスク

GKE ノード(GCE)に関連付いた IAM SA の認証情報を利用するパターン

では GKE ノードに関連付いた IAM SA の認証情報の利用するパターンではどうでしょう?
GKE ノードには GCE デフォルトの IAM SA または ユーザーが作成した IAM SA を関連付けることができます。


GKE 上でメタデータサーバーから、関連付いた IAM SA の認証情報を取得し GCP サービスにアクセスする例

一見良さそうですが、欠点があります。この方式だと、同じ GKE ノードにデプロイされるワークロード全てで同一の IAM SA が使用されることとなるため、Pod A、B いずれにも権限を付与することができます。例えば、Pod B からは Datastore のアクセス権限が不要な場合、これは、最小権限の原則 に反することになります。

また GKE ノード作成時に IAM SA を指定しない場合、GCE デフォルトの IAM SA が使用されますが、これは広範なアクセス権があり、Kubernetes Engine クラスタの実行には権限が過剰になります。
参考:最小限の権限の Google サービス アカウントを使用する

理想的には、Pod A と Pod B それぞれに、適切なスコープのアクセス権限を付与すること望まれます。そこで Workload Identity が必要となります。

Workload Identity のススメ

Workload Identity とは

Workload Identity 連携は外部ワークロード(AWS, Azure AD, Kubernetes クラスタ) に GCP サービスへのアクセス権を IAM SA を介して付与する仕組みです。

この仕組みを GKE で用いることで、GKE 上で "IAM SA と K8s サービスアカウント(以下、K8s SA) を連携し、GKE 上のワークロードを個別の IAM SA として実行可能" になります。参考:Workload Identity(GKE)

Workload Identity では大きく 2 つのポイントがあります。1つ目は、IAM SA と K8s SA を関連付けること。2つ目は、GKE メタデータサーバーを利用することです。

Workload Identity を使うためには、GKE クラスタで Workload Identity の有効化を行う必要があります。執筆段階においては、Autopilot ではデフォルトで有効化されていますが、Standard は作成時または、作成後に有効化する必要があります。参考:Workload Identityを有効にする

IAM SA と K8s SA を関連付ける

GKE の Workload Identity では K8s SA の annotation として以下を付与することで、K8s SA と IAM SA を関連付けることができます。
※ 加えて、IAM Policy Binding を作成 する必要がありますが Workload Identity 実践編で後述します。

iam.gke.io/gcp-service-accoun=<IAM SA名>@<GCPのPJID>.iam.gserviceaccount.com

これは実際に K8s SA のマニフェストを見ると分かりやすいです。以下が Workload Identity で用いる K8s SA のマニフェストファイルのサンプルです。

apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    iam.gke.io/gcp-service-account: iam-sa-k8snovice@<GCPのPJID>.iam.gserviceaccount.com
  name: sa-k8snovice
  namespace: k8snovice

Workload Identity により、GKE クラスタにおける namespace k8snovice の K8s SA sa-k8snovice を IAM SA iam-sa-k8snovice と関連付けることを意味します。


Workload Identity(GKE) で K8s SA と IAM SA を関連付ける

GKE メタデータサーバーを利用する

GKE メタデータサーバーは大変便利なコンポーネントです。GKE の Workload Identity では GKE メタデータサーバーを利用することで、K8s SA から関連付いた IAM SA の認証情報の取得を行うことができます。

Workload Identity を有効化すると、GKE メタデータサーバーが DaemonSet として各 GKE ノードにデプロイされ、また、前述したクライアント ライブラリからの問い合わせ先が、GKE メタデータサーバーになります。
GKE メタデータサーバーは、クライアント ライブラリからメタデータサーバーへのエンドポイントのリクエスト http://metadata.google.internal/ をインターセプトします。従って、Workload Identity の制約事項 にもあるように、Workload Identity を GKE で有効化した場合、既存の Compute Engine の IAM SA を使った認証はできなくなるので注意です。

K8s SA sa-k8snovice を使用して作成された Pod は、IAM SA iam-sa-k8snovice の認証情報を GKE メタデータサーバーから取得することができます。(下図。GKE メタデータサーバーの理解のため、一部コンポーネントを省略して図示しています。)


GKE メタデータサーバーを使った K8s SA と IAM SA の関連付け

Workload Identity の実践

それでは、実際に GKE クラスタ上で Workload Identity を試してみましょう。

Workload Identity の準備

  • 既存の GKE クラスタで Workload Identity を有効化
    $ gcloud container clusters update <cluster name> \
        --region=asia-northeast1-a \
        --workload-pool=<project id>.svc.id.goog
    
  • 既存のノードプールで Workload Identity を有効化 ※ メタデータエンドポイントが GKE メタデータサーバーになることに注意
    $ gcloud container node-pools update <nodepool name> \
        --cluster=<cluster name> \
        --region=asia-northeast1-a \
        --workload-metadata=GKE_METADATA	
    
  • K8s SA と IAM SA の作成
    # namespace 作成
    $ kubectl create namespace k8snovice
    
    # service account 作成    
    $ kubectl create serviceaccount sa-k8snovice --namespace k8snovice
    
    # IAM サービスアカウントの作成
    $ gcloud iam service-accounts create iam-sa-k8snovice --project=<project id>
    
    # IAM サービスアカウントにロールの付与
    $ gcloud projects add-iam-policy-binding <project id> \
        --member "serviceAccount:iam-sa-k8snovice@<project id>.iam.gserviceaccount.com" \
        --role "roles/datastore.user" # iam-sa-k8snovice に datastore ユーザ権限を付与
    
  • K8s SA と IAM SA の IAM Policy Binding の作成
    # K8s SA と IAM SA のバインド
    $ gcloud iam service-accounts add-iam-policy-binding iam-sa-k8snovice@<project id>.iam.gserviceaccount.com \
        --role roles/iam.workloadIdentityUser \
        --member "serviceAccount:<project id>.svc.id.goog[k8snovice/sa-k8snovice]"
    
    # SA に IAM SA にアノテーションする
    $ kubectl annotate serviceaccount sa-k8snovice \
        --namespace k8snovice \
        iam.gke.io/gcp-service-account=iam-sa-k8snovice@<project id>.iam.gserviceaccount.com    
    

これで、GKE における Workload Identity の準備は完了です。次節で実際に Pod に K8s SA を付与して datastore にアクセスをしてみましょう。

K8s SA: sa-k8snovice 有無の Pod から datastore へアクセス

以下のように K8s SA sa-k8snovice を付与した Pod と、デフォルト K8s SA default を付与した Pod を用意します。sa-k8snovice は Workload Identity により、"roles/datastore.user" 権限を持つ IAM SA iam-sa-k8snovice と関連付いています。

datastore にはあらかじめデータ " Hi! Kubernetes Novice 😇 " を登録しておきました!

kubectl get po -nk8snovice
NAME                       READY   STATUS    RESTARTS   AGE
k8snovice-pod-with-sa      1/1     Running   0          11h
k8snovice-pod-without-sa   1/1     Running   0          11h

各 Pod の中から、GKE メタデータサーバーに認証情報を問い合わせ、その認証情報を使って、datastore に HTTP リクエストを送信してみます。

k8snovice-pod-with-sa の場合、

# sh 実行
$ kubectl exec -it k8snovice-pod-with-sa -nk8snovice -- sh

# GKE メタデータサーバーから認証情報の取得
$ ACCESS_TOKEN=`curl \
   "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" \
   -H "Metadata-Flavor: Google" | jq -r '.access_token'`

# datastore へアクセス
$ curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"partitionId": {"projectId": "study-work-387200"},"readOptions": {"readConsistency": "EVENTUAL"},"query": {"kind": [{"name": "entity_k8snovice"}],"limit": 1}}' "https://datastore.googleapis.com/v1/projects/study-work-387200:runQuery" | jq -r '.batch.entityResults[].entity'
{
  "key": {
    "partitionId": {
      "projectId": "study-work-387200"
    },
    "path": [
      {
        "kind": "entity_k8snovice",
        "name": "00003f5a-9666-452a-800d-024b9e148cba"
      }
    ]
  },
  "properties": {
    "CreatedAt": {
      "timestampValue": "2023-05-27T14:07:00.904486Z"
    },
    "Value": {
      "stringValue": "Hi! Kubernetes Novice 😇"
    }
  }
}

無事、GKE メタデータから取得した認証情報を使って、datastore からデータを取得することができました。

k8snovice-pod-without-saの場合、

# sh 実行
$ kubectl exec -it k8snovice-pod-without-sa -nk8snovice -- sh

# 認証情報の取得
$ ACCESS_TOKEN=`curl \
   "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" \
   -H "Metadata-Flavor: Google" | jq -r '.access_token'`

# datastore へアクセス
$ curl -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"partitionId": {"projectId": "study-work-387200"},"readOptions": {"readConsistency": "EVENTUAL"},"query": {"kind": [{"name": "entity_k8snovice"}],"limit": 1}}' "https://datastore.googleapis.com/v1/projects/study-work-387200:runQuery"
{
  "error": {
    "code": 403,
    "message": "Missing or insufficient permissions.",
    "status": "PERMISSION_DENIED"
  }
}

PERMISSION_DENIED となり、意図通り K8s SA sa-k8snovice を付与していない Pod からの datastore のアクセスを拒否することができました!
これにより同じ GKE ノードのワークロードでも個別に GCP サービスへのアクセス権限をきめ細かく付与できることがわかりました 🎉

Workload Identity の発展

先ほどの K8s SA と IAM SA の関連付けはイメージのため幾つかの認証フローを簡略化しています。最後にもう少しだけ解像度を上げて、GKE の Workload Identityの全体像について説明します。

K8s SA: sa-k8snovice の Pod が、datastore にアクセスするとき、GKE メタデータサーバーを介して、次のような認証フローで認証情報を取得します。

  • ①: GKE メタデータサーバーは Control Plane の OIDC Provider から sa-k8snovice の JWT を取得
    • OIDC Provider は、Workload Identity を有効化するときに作成される、Workload Identity Pool に Provider として登録される
  • ②: GKE メタデータサーバーは、JWT を使って、IAM に IAM SA iam-sa-k8snovice の認証情報をリクエスト
  • ③: IAM は JWT を OIDC Provider に検証した上で、K8s SA: sa-k8snovice と IAM SA iam-sa-k8snovice の紐付けを、Workload Identity Pool の IAM Policy Binding で確認する
    • IAM SA には roles/iam.workloadIdentityUser というロールを付与して、Workload Identity による借用を許可しておきます
  • ④-⑤: IAM SA: iam-sa-k8snovice の認証情報を GKE メタデータサーバーに返却
  • ⑥: アプリケーション (K8s SA: sa-k8snovice) は IAM SA: iam-sa-k8snovice になりすますことで、GKE サービス (datastore) にアクセス

内部を理解しようとするとややこしいですが、実際ユーザーからはPod (アプリケーション) が GKE メタデータサーバーに問い合わせることで、K8s SA に関連した IAM SA の認証情報が取得できるように見えます。簡単に言うと GKE メタデータサーバーがよしなにやってくれています。

※ GKE メタデータサーバーの実装は公開されておらず、幾つかの情報(Cloud Next'19 や記事など)を集めて描いたフローなので正確でない記載を含む可能性があります。誤りに気づかれた際は、お知らせいただけると幸いです!

まとめ

GKE で実行されるワークロードから GCP サービスに安全にアクセスするためには "Workload Identity" を使うと簡単に行うことができます。
GCP が提供する GKE メタデータサーバーなどの仕組みを使うことで内部の動きを気にせずに、K8s SA と IAM SA を関連づけることができることを紹介しました。

イベントの紹介

本記事は、Kubernetes Novice Tokyo #26 のライトニングトークの内容をまとめたものです。
そちらも併せてご確認いただけますと幸いです!
https://k8s-novice-jp.connpass.com/event/286692/

発表スライドはこちらです!
https://speakerdeck.com/k6s4i53rx/getting-started-gke-workload-identity

また、類似して AWS サービスへアクセスする際に用いる「IRSA」について書かれた(by @minorun365)本記事も大変参考になるので、宣伝させていただきます!上記イベンにも登壇されています!
https://qiita.com/minorun365/items/171bb7858fe5524e45d7

参考資料

Discussion