Cloud IAPとは、Cloud Identity-Aware Proxyの略で、GCPのサービス(?)の1つ。
Cloud IAP を使用すると、HTTPS によってアクセスされるアプリケーションの一元的な承認レイヤを確立できるため、ネットワークレベルのファイアウォールに頼らずに、アプリケーションレベルのアクセス制御モデルを使用できるらしい。
ここにかいてあった。→ Cloud Identity-Aware Proxy の概要
イメージとしては、Google認証によるアクセス制御を受け持ってもらえる感じかなと。
Googleアカウントだけでなく、Googleグループの単位でアクセス制限がかけられるのが便利そう。
実際に動かす
実際に簡単なアプリケーションをGKEにデプロイして、IAPによるアクセス制限をかけてみます。
https://github.com/grandcolline/iap-test
- GCPプロジェクトがあること
- ローカルPCでgcloudでアクセスできること
- ローカルPCにkubectlがインストールされていること
- Cloud DNSでドメイン管理をおこなっていること
が前提条件です><
準備1) ソースコード取得
コードをここに準備しました。これをクローンしちゃってください。
https://github.com/grandcolline/iap-test
$ 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
コマンドを叩いて、credentialの取得とappのデプロイを行います。
GKEクラスターにappという簡単なAPIサーバのDeploymentとServiceを反映します。
# クラスタ一覧取得
$ 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
準備3) Ingressの作成
ここでは、CloudDNSに使えるドメインがあることが前提です。
まずは、VPC Network > External IP addressから、静的IPを取得します。
typeをglobalで作らないとIngressで使えないので注意です。
https://console.cloud.google.com/networking/addresses/list
自分はiap-test-ingress
という名前で取得しました。
NetWork Service > CloudDNSで、先ほど取得した静的IPにドメインを割り振ります。
自分は、iap-test.dev.grandcolline.com
というドメインを割り振りました。
https://console.cloud.google.com/net-services/dns/zones
コマンドからIngressを反映します。
# 設定ファイルの書き換え
# 1. ingress.global-static-ip-nameを自分の取得した静的IP名に書き換える
# 2. domainを自分の取得したドメインに書き換える
$ vim k8s/ingress.yml
# 反映
$ kubectl apply -f k8s/ingress.yml
少し待つと、Load balancerが作成され、https://<ドメイン>/hello
でアクセスできるようになります。
以上で、準備は終わりです。
ここから、Cloud IAP for GKE の有効化を参考にCloudIAPの設定をしていきます。
OAuth同意画面の構成
OAuth同意画面にアクセスして、
- アプリケーション名
- サポートメール
を入力します。
IAPの設定
セキュリティ>Identity-Aware Proxy にアクセスして、
https://console.cloud.google.com/security/iap
default/app
のIAPをオンにします。
Googleの公式の手順では、OAuth認証情報の作成なども必要そうですが、
上記の対応をすることで、勝手にOAuth認証情報も作成されます(自分はされました)
https://console.cloud.google.com/apis/credentials
この状態で、先ほどのhttps://<ドメイン>/hello
にアクセスすると、Googleログインが求められ、Access Deniedのページへと飛ばされます!
うまく、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
改めて、https://<ドメイン>/hello
にいくと、アクセスができます!
ユーザ情報を取得する
ユーザのIDの取得を参考に、アプリ側でユーザ情報を取得します。
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 |
ですので、アプリ側でヘッダーを取得するようなコードを書くことで、ユーザ情報を取得できます。
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の承認を検証する
署名済みヘッダーによるアプリの保護を参考に、承認を検証します。
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
を取得します。
環境変数に追加して、アプリケーションを再デプロイをします。
# 環境変数の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が正しいかを確認。
を行います。
詳細は、署名済みヘッダーによるアプリの保護を参考にしてください。
コードはこんな感じです。何かおかしな場所がありましたら、指摘いただければ幸いです。
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)
}
まとめ的ななにがし
自分は、今までCloud IAPを使ったことがなかったのですが、簡単にGoogle認証をかませてすごいと思いました。
一旦、動くところまではできましたが、サービスアカウントでの使い方や、結局backend-configの設定をしなくていいかなど、わかっていない部分もたくさんあります😨
ただ、Googleグループなどでの制御もできるので、ステージング環境などのアクセス制限などでも使い勝手が良さそうだなぁという印象です。もっと勉強して、今後も積極的に使っていきたいです!!