Cloud Run から GKE に移行する際に考えないといけない認証の話

# はじめに
Cloud Run から GKE への移行を検討するにあたり認証・認可の方法が分からなかった。
どことどこの間の認証・認可かというと、Pub/Sub をはじめとした GCP サービスと GKE の間。

調べていくと Identity-Aware Proxy という GCP の仕組みが利用できることが分かったので、
経緯と Identity-Aware Proxy の仕組みについて勉強を兼ねてまとめてみる。

導入検討中の方の参考になれば幸いです。

# どういうシチュエーションだったか

Cloud Run から GKE にコンテナ基盤を移行したいというミッションがありました。
Cloud Run を利用していれば自ずと GCP サービスと連携したくなります。
ここで GCP サービスと呼んでいるのは、Pub/Sub や Tasks などです。

簡略化すると次のようなシチュエーションを想定しています。

移行前: Pub/Sub → Cloud Run
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/61286d4b-9e8c-d1ee-9727-c052044aa6cb.png)

移行後: Pub/Sub → GKE
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/4a9ae86d-1a3d-70d1-3d53-420e482f4c41.png)

Pub/Sub のアイコンが違うのはご愛嬌。

# なぜ認証を考えないといけないのか
Cloud Run は HTTP エンドポイントを公開することができます。
当然ですが誰でもアクセスできたら困ります。
なので、Pub/Sub からのアクセスのみ許可したいです。

移行前は Cloud Run の機能を使って認証を入れていました。
Terraform のコードはこんな感じです。

```tf
resource "google_cloud_run_service_iam_member" "example_api" {
  location = google_cloud_run_service.example_api.location
  project  = google_cloud_run_service.example_api.project
  service  = google_cloud_run_service.example_api.name # 認証を挟みたい Cloud Run サービス
  role     = "roles/run.invoker" # Cloud Run の呼び出しに必要なロール
  member   = "serviceAccount:${google_service_account.example_sa.email}" # 認証に使うサービスアカウント
}
```

Pub/Sub から送られるリクエストに `example_sa` を紐付けることで自動的に認証が行われます。
詳しくは [ドキュメント](https://cloud.google.com/pubsub/docs/push#authentication_and_authorization) と [こちらの記事](https://medium.com/google-cloud-jp/gcp-からの-http-リクエストをセキュアに認証する-dda4933afcd6) が参考になります。

```tf
resource "google_pubsub_subscription" "example_subscription" {
  ...
  push_config {
    push_endpoint = "https://cloudrun-api.com/push" # メッセージの送信先エンドポイント (Cloud Run)
    oidc_token {
      service_account_email = google_service_account.example_sa.email # 認証に使うサービスアカウント
    }
  }
  ...
}
```

Cloud Run ならこれで完成です。

さて、GKE でも同じようにできるかというと、答えは No です。
認証・認可は GKE の機能として備わっていないので、代替手段を検討する必要がありました。

# Identity-Aware Proxy が使える
調査の結果、[Identity-Aware Proxy](https://cloud.google.com/blog/ja/products/gcp/control-access-your-web-sites-identity-aware-proxy) (以下、IAP) が代替になると分かりました。

IAP は GCE や GCLB のようなサービスの前段で認証・認可のフローを提供できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/92cd76fe-81a3-f859-944c-a3d7859e97f6.png)
画像: [Charlie Engelke, Identity-Aware Proxy を使ったウェブサイトへのアクセス制御](https://cloud.google.com/blog/ja/products/gcp/control-access-your-web-sites-identity-aware-proxy)

これにより、認証が具備されていないサービスにも GCP のプリンシパルを用いた認証を実装できます。
なお、上図では人 (ユーザアカウント) が描かれていますが、認証対象にはサービスアカウントも含まれています。

今回のシチュエーションでは、GKE のエンドポイントは GKE Ingress Controller で作成された GCLB で公開するつもりでした。
そのため、上図と同様な構成が適用可能でした。

あとは仕組みを理解して、置き換えれば OK です。

# IAP の仕組み
[ドキュメント](https://cloud.google.com/iap/docs/concepts-overview) によると、認証・認可は下図のようなフローで行われます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/08f0c3a6-e23a-5ce2-90e6-909c95c6b516.png)
画像: [Identity-Aware Proxy の概要](https://cloud.google.com/iap/docs/concepts-overview)

### IAP が有効かチェック
まず、GCLB (App Engine や GCE も想定可) がクライアントからリクエストを受け取ります。GCLB は[バックエンドサービス](https://cloud.google.com/load-balancing/docs/backend-service)に対して IAP が有効になっているか確認します。バックエンドサービスは、GKE の文脈では Service オブジェクトと同義と捉えられます。

### 認証ステップ
IAP が有効になっている場合は、リクエストヘッダや Cookie 内の認証情報が IAP 認証サーバに送られます。認証サーバは、認証情報の有無をチェックし、有効な場合、次の認可ステップへ進みます。認証情報が無効な場合は、OAuth のログインフローにリダイレクトされ、認証が完了すれば トークンが Cookie に保存されます。

### 認可ステップ
認可ステップでは、認証情報から取得したユーザ ID から、そのユーザに必要な IAM Role が付与されているかチェックします。ここを PASS すればようやくアプリケーションに到達することができます。

なお、上記はユーザアカウントを利用する場合の認証フローになります。
サービスアカウントを利用する場合は ID トークンを事前に取得しておく必要があります。
詳細は、[サービスアカウントからの認証](https://cloud.google.com/iap/docs/authentication-howto?hl=ja#authenticating_from_a_service_account) をご確認ください 。

長々書いてしまいましたが、ユーザアカウント認証、サービスアカウント認証のどちらの場合も、
トークンを持っていないと IAP で弾かれるんだなと捉えてもらえば大丈夫です。

# GKE で IAP を有効化
GKE で提供するエンドポイントを IAP で保護する手順です。
これは[ドキュメント](https://cloud.google.com/iap/docs/enabling-kubernetes-howto)通りに実装して問題なく動作しました。

[OAuth 同意画面の構成](https://cloud.google.com/iap/docs/enabling-kubernetes-howto#oauth-configure) と [OAuth 認証情報の作成](https://cloud.google.com/iap/docs/enabling-kubernetes-howto#oauth-credentials) は下記理由から Terraform ではなく手動で設定しています。

* OAuth 同意画面は [Terraform 対応](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iap_brand) しているが権限問題でサポートメールアドレスを手動で設定する必要があった
* OAuth 認証情報も [Terraform 対応](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iap_client) していそうだが対応フィールドが少なくどちらにしろ redirect URI の手動設定が必要だった

# audience を変更
ここまでで IAP の概要を把握し、GKE 上で実装することができました。
最後に Pub/Sub サブスクリプションで明示的に audience を設定して完了です。

Terrafrom のコードはこのようになります。

```tf
resource "google_pubsub_subscription" "example_subscription" {
  ...
  push_config {
    push_endpoint = "https://gke-api.com/push" # メッセージの送信先エンドポイント (GKE)
    oidc_token {
      service_account_email = google_service_account.example_sa.email # 認証に使うサービスアカウント
      audience              = "example.apps.googleusercontent.com" # 追加
    }
  }
  ...
}
```

`audience` というフィールドに前節で作成した OAuth クライアントの Client ID を記載します。

`audience` 未指定の場合は、 `push_endpoint` に記載した URL が使われます。
詳しくは、[GCP ドキュメント](https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.subscriptions#oidctoken) または [Terraform ドキュメント](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/pubsub_subscription#oidc_token) をご参照ください。

### なぜ audience を変更するのか
Cloud Run では `audience` 未指定だった (= エンドポイント URL を指定) のに、
なぜ IAP では Client ID を記載しなければいけないのかという疑問が残ると思います。

その理由は両者の仕様の違いです。
Cloud Run は、トークン作成時に audience に [target URL を要求](https://cloud.google.com/run/docs/authenticating/service-to-service#acquire-token) します。

```
(抜粋)
// functionURL := "https://TARGET_URL"
client, err := idtoken.NewClient(ctx, targetURL)
```

対して、IAP は、audience に [Client ID を要求](https://cloud.google.com/iap/docs/authentication-howto#obtaining_an_oidc_token_for_the_default_service_account) します。

```
(抜粋)
// audience := "IAP_CLIENT_ID.apps.googleusercontent.com"
client, err := idtoken.NewClient(ctx, audience)
```

他にも書きたいことがありましたが、記事が膨張してしまうので別の機会にします。

* audience は何のために利用されるのか
* [IAP が期待する audience claim](https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload) とは
* 実際に Pub/Sub から送られる ID トークン (JWT) の中身

# 参考

* https://cloud.google.com/blog/ja/products/gcp/control-access-your-web-sites-identity-aware-proxy
* https://cloud.google.com/iap/docs/concepts-overview
* https://medium.com/google-cloud-jp/gcp-からの-http-リクエストをセキュアに認証する-dda4933afcd6