Cloud IAP を使って、GKEのアプリケーションにGoogle認証をかます

Cloud IAPとは、Cloud Identity-Aware Proxyの略で、GCPのサービス(?)の1つ。
Cloud IAP を使用すると、HTTPS によってアクセスされるアプリケーションの一元的な承認レイヤを確立できるため、ネットワークレベルのファイアウォールに頼らずに、アプリケーションレベルのアクセス制御モデルを使用できるらしい。
ここにかいてあった。→ [Cloud Identity-Aware Proxy の概要](https://cloud.google.com/iap/docs/concepts-overview?hl=ja)

イメージとしては、Google認証によるアクセス制御を受け持ってもらえる感じかなと。
Googleアカウントだけでなく、Googleグループの単位でアクセス制限がかけられるのが便利そう。

![iap-load-balancer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/5eefb209-7f66-ee3a-b764-3c066402fc52.png)


# 実際に動かす

実際に簡単なアプリケーションをGKEにデプロイして、IAPによるアクセス制限をかけてみます。
https://github.com/grandcolline/iap-test


- GCPプロジェクトがあること
- ローカルPCでgcloudでアクセスできること
- ローカルPCにkubectlがインストールされていること
- Cloud DNSでドメイン管理をおこなっていること

が前提条件です＞＜

## 準備1) ソースコード取得

コードをここに準備しました。これをクローンしちゃってください。
https://github.com/grandcolline/iap-test

```bash
$ git clone git@github.com:grandcolline/iap-test.git
$ cd iap-test
```

## 準備2) GKEクラスタの作成とappのデプロイ

GCPの管理画面から、GKEのクラスタを作成します。
自分は、`iap-test-cluster`というクラスターを作成しました。
クラスター名・ノード数などは適当で大丈夫です。
https://console.cloud.google.com/kubernetes/list
<img width="1157" alt="スクリーンショット 2019-12-10 21.02.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/bafb79f3-056a-6b88-cf1b-78a29017b7b3.png">


コマンドを叩いて、credentialの取得とappのデプロイを行います。
GKEクラスターにappという簡単なAPIサーバのDeploymentとServiceを反映します。

```bash
# クラスタ一覧取得
$ gcloud container clusters list
NAME              LOCATION           MASTER_VERSION  MASTER_IP       MACHINE_TYPE  NODE_VERSION    NUM_NODES  STATUS
iap-test-cluster  asia-northeast1-a  1.13.11-gke.14  35.187.203.144  g1-small      1.13.11-gke.14  3          RUNNING

# クラスタのcredentialを取得
$ gcloud container clusters get-credentials \
    iap-test-cluster \
    --region=asia-northeast1-a

# credential確認
$ kubectl config get-contexts
CURRENT   NAME                                                      CLUSTER                                                   AUTHINFO                                                  NAMESPACE
          docker-desktop                                            docker-desktop                                            docker-desktop
          docker-for-desktop                                        docker-desktop                                            docker-desktop
*         gke_grandcolline-dev_asia-northeast1-a_iap-test-cluster   gke_grandcolline-dev_asia-northeast1-a_iap-test-cluster   gke_grandcolline-dev_asia-northeast1-a_iap-test-cluster

# 反映
$ kubectl apply -f k8s/app.yml
```

Workloadタブで確認できるはずです。
https://console.cloud.google.com/kubernetes/workload
<img width="1153" alt="スクリーンショット 2019-12-10 20.59.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/93ffc497-05f6-bd22-7247-c0ec9218a4c5.png">


## 準備3) Ingressの作成

ここでは、CloudDNSに使えるドメインがあることが前提です。
まずは、VPC Network > External IP addressから、静的IPを取得します。
typeをglobalで作らないとIngressで使えないので注意です。
https://console.cloud.google.com/networking/addresses/list
自分は`iap-test-ingress`という名前で取得しました。
<img width="1485" alt="スクリーンショット 2019-12-10 22.26.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/18f8ea28-f011-aac8-83e5-af9ffea09df5.png">



NetWork Service > CloudDNSで、先ほど取得した静的IPにドメインを割り振ります。
自分は、`iap-test.dev.grandcolline.com` というドメインを割り振りました。
https://console.cloud.google.com/net-services/dns/zones
<img width="1396" alt="スクリーンショット 2019-12-10 22.32.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/f347a4c0-b63f-dea3-c397-91489c99ab97.png">


コマンドからIngressを反映します。

```bash
# 設定ファイルの書き換え
#  1. ingress.global-static-ip-nameを自分の取得した静的IP名に書き換える
#  2. domainを自分の取得したドメインに書き換える
$ vim k8s/ingress.yml

# 反映
$ kubectl apply -f k8s/ingress.yml
```

少し待つと、Load balancerが作成され、`https://<ドメイン>/hello`でアクセスできるようになります。
<img width="1491" alt="スクリーンショット 2019-12-10 22.47.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/c2f17af6-6ef2-f923-b526-91da0920e2ae.png">


以上で、準備は終わりです。
ここから、[Cloud IAP for GKE の有効化](https://cloud.google.com/iap/docs/enabling-kubernetes-howto?hl=ja)を参考にCloudIAPの設定をしていきます。

## OAuth同意画面の構成

[OAuth同意画面](https://console.cloud.google.com/apis/credentials/consent)にアクセスして、

- アプリケーション名
- サポートメール

を入力します。

<img width="990" alt="スクリーンショット 2019-12-12 4.33.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/596b2a7b-0f20-8911-d456-6d54c00fe800.png">

## IAPの設定

セキュリティ＞Identity-Aware Proxy にアクセスして、
https://console.cloud.google.com/security/iap
`default/app`のIAPをオンにします。
<img width="1185" alt="スクリーンショット 2019-12-12 4.43.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/bdb4156e-aa76-2fef-33b8-7ea94e449a2c.png">

Googleの公式の手順では、OAuth認証情報の作成なども必要そうですが、
上記の対応をすることで、勝手にOAuth認証情報も作成されます（自分はされました）
https://console.cloud.google.com/apis/credentials


この状態で、先ほどの`https://<ドメイン>/hello`にアクセスすると、Googleログインが求められ、Access Deniedのページへと飛ばされます！

<img width="805" alt="スクリーンショット 2019-12-12 4.51.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/3fa44657-54fb-f000-3c4f-66e40dd611fe.png">

うまく、IAPが挟まったようです！

アクセス権限を与えるためには、
セキュリティ＞Identity-Aware Proxy から、
https://console.cloud.google.com/security/iap
`default/app`を選択して、ADD MEMBERから、`IAP-secured Web App User`の権限でアプリにアクセス可能なアカウントを追加します。
ここには、

- Google アカウント: user@gmail.com
- Google グループ: admins@googlegroups.com
- サービス アカウント: server@example.gserviceaccount.com
- G Suite ドメイン: example.com

のアカウントが使えるそうです。
<img width="1231" alt="スクリーンショット 2019-12-12 4.56.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/f4f6cc18-41d3-6235-ecb7-e50312a77f55.png">

改めて、`https://<ドメイン>/hello`にいくと、アクセスができます！

## ユーザ情報を取得する

[ユーザのIDの取得](https://cloud.google.com/iap/docs/identity-howto?hl=ja)を参考に、アプリ側でユーザ情報を取得します。
Cloud IAPはユーザーのIDを以下のHTTPヘッダーでバックエンドサービスに渡します。

| ヘッダー名 | 説明 | 値の例 |
|:--|:--|:--|
| X-Goog-Authenticated-User-Email | ユーザーのメールアドレス | accounts.google.com:example@gmail.com |
| X-Goog-Authenticated-User-ID | ユーザーの永続的で一意な ID | accounts.google.com:userIDvalue |

ですので、アプリ側でヘッダーを取得するようなコードを書くことで、ユーザ情報を取得できます。

```golang
func userHandler(w http.ResponseWriter, r *http.Request) {
	mail := r.Header.Get("X-Goog-Authenticated-User-Email")
	id := r.Header.Get("X-Goog-Authenticated-User-ID")
	fmt.Fprint(w, "ID: "+id+"\nmail: "+mail)
}
```

こちらのハンドラーを`https://<ドメイン>/user`にルーティングしてあるので、こちらのURLにアクセスすると、IDとメールアドレスが取得できるかと思います！

ただ、こちらの方法ではCloud IAPによって承認されていることを確認はできていないので、注意は必要です。

## IAPの承認を検証する

[署名済みヘッダーによるアプリの保護](https://cloud.google.com/iap/docs/signed-headers-howto?hl=ja)を参考に、承認を検証します。

Cloud IAPを通る際に、`X-Goog-*`ヘッダーを削除するそうですが、もしIAPをバイパスする場合には、攻撃者は`X-Goog-Authenticated-User-{Email,ID}`を偽造することができます。
そこで、`X-Goog-IAP-JWT-Assertion`ヘッダーに入っている、JWTを使って、IAPによって承認されていることを検証します。

まず、Identity-Aware Proxyから `Signed Header JWT Audience` を取得します。
<img width="1186" alt="スクリーンショット 2019-12-12 5.34.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77782/04cc0efa-35be-3ce8-449e-9faca95d2e5c.png">

環境変数に追加して、アプリケーションを再デプロイをします。

```bash
# 環境変数のAUDIENCEを、取得したSignedHeaderJWTAudienceに書き換え
$ vi k8s/app.yml

# 変更を反映
$ kubectl apply -f k8s/app.yml
```
デプロイが完了したら、`https://<ドメイン>/auth`にアクセスをして、ユーザ情報が取得できていれば、検証がうまくできています。

アプリケーションで行っている処理は単純です。

　1. ヘッダーの`X-Goog-IAP-JWT-Assertion`から、JWTを取り出す。
　2. Googleが公開している公開鍵から、鍵を取得する。
　3. 鍵を使って、検証。ここで期限などもチェック。
　4. Audienceが正しいかを確認。

を行います。

詳細は、[署名済みヘッダーによるアプリの保護](https://cloud.google.com/iap/docs/signed-headers-howto?hl=ja)を参考にしてください。
コードはこんな感じです。何かおかしな場所がありましたら、指摘いただければ幸いです。

```golang
func authHandler(w http.ResponseWriter, r *http.Request) {
	// get token
	tokenStr := r.Header.Get("X-Goog-IAP-JWT-Assertion")
	audience := os.Getenv("AUDIENCE")
	claim := &iapClaim{}

	_, err := jwt.ParseWithClaims(tokenStr, claim, func(token *jwt.Token) (interface{}, error) {
		// check signing method
		if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok {
			return nil, errors.New("Unexpected signing method")
		}
		// get public key
		if _, ok := token.Header["kid"].(string); !ok {
			return nil, errors.New("not found kid")
		}
		rawKey, err := fetchPublicKey(token.Header["kid"].(string))
		if err != nil {
			return nil, err
		}
		key, err := jwt.ParseECPublicKeyFromPEM(rawKey)
		if err != nil {
			return nil, err
		}
		return key, nil
	})
	if err != nil {
		fmt.Fprint(w, "Validation: NG"+"\nerror: "+err.Error())
		return
	}

	if claim.Audience != audience {
		fmt.Fprint(w, "Validation: NG"+"\nerror: invalid audience")
		return
	}

	fmt.Fprint(w, "Validation: OK"+"\nID: "+claim.Subject+"\nmail: "+claim.Email)
}
```
https://github.com/grandcolline/iap-test/blob/master/main.go#L41

# まとめ的ななにがし

自分は、今までCloud IAPを使ったことがなかったのですが、簡単にGoogle認証をかませてすごいと思いました。
一旦、動くところまではできましたが、サービスアカウントでの使い方や、結局backend-configの設定をしなくていいかなど、わかっていない部分もたくさんあります😨

ただ、Googleグループなどでの制御もできるので、ステージング環境などのアクセス制限などでも使い勝手が良さそうだなぁという印象です。もっと勉強して、今後も積極的に使っていきたいです！！