GKEをバックエンドに持つGCPのLBでmTLS機能を使う

はじめに

SREグループ・ヒロチカです。GO株式会社では、サービスのクラウドインフラの設計から構築・運用までを担当しています。 以前の記事で、Google Cloudのグローバル外部アプリケーションロードバランサでmTLS機能を利用した通信を行う構成をご紹介しました。 今回はその続きとしてロードバランサのバックエンドサービス側がGKEとなるような構成で同様のmTLS機能を利用した通信を行うための設定を、いくつかのハマりポイントなども含めてこちらに記載できればと思います。

経緯

当初の構成

前回のブログ記事

GCPのロードバランサーでmTLS機能を使う

前回の続きとなるため少しだけ内容を振り返ると、あるアプリケーションについて手動で行っていたサーバ証明書の更新管理を手離れよくさせるため、Googleマネージドなサーバ証明書が利用できるGCPのレイヤ7ロードバランサーのネットワーク構成へと移行を検討しました。 その中で、アプリケーションの要件の一つであったクライアント証明書の検証についてはmTLS機能を用いた構成にすることで要件を満たしたい、というのが大筋の話でした。

前回はmTLS通信を行う一般的な構成に則った形で構築を行いましたが、今回はアプリケーションを動かしている環境がGKEでのケースとなります。

弊社のアプリケーションは基本的にKubernetesベースの社内共通基盤(kenosと名称)を実行環境としており、今回のアプリケーションもkenos上、つまりGKEが実行環境となります。 このkenosのGKE環境ではアプリケーションロードバランサとしてIngressリソースを利用しており、GKEのIngress Controllerを用いてKubernetes管理下にて構築・制御しています。

今回、このGKE Ingressの部分で前回行ったようなレイヤ7ロードバランサーのmTLS通信機能を実装できれば良かったのですが、残念ながらGKE Ingress ControllerにはmTLS機能の制御行うための設定やパラメータがありませんでした。 GCPのコンソール上からはIngressリソースとして起動したロードバランサは確認できるため、terraformで設定をimportして別途mTLS機能を設定する方法も検討しましたが見送りました。 理由としては1つのロードバランサに対して複数の管理系統から制御を行う状態となるため、お互いの設定変更がお互いに反映されない状況が生まれ事故が発生しやすい構造となるからです。

そこでロードバランサの構成要素を分解し、ロードバランサのメインとなる通信制御やSSL終端、mTLS制御などを行うフロントエンド部分とurl mappingの設定までをterraformで管理し、通信を流す先として設定するバックエンド部分はKubernetes側で設定・管理するような構成を考えました。 つまりロードバランサのbackend serviceまでをKubernetesから設定、それ以外(mTLS含む)の設定はterraformから行うといった形です。 これにより、terraformとKubernetesがお互い干渉しない形での構成を実現できます。

当初の構成案

当初の構成における問題とその解決策

しかしながら上記で構築中、1点問題が発生しました。 Kubernetes側からbackend configを関連づけたserviceの構築だけでは、GCPのロードバランサのコンソールやgcloudコマンドから確認できるbackend serviceが見当たりませんでした。 自分の認識としてGCPのロードバランサ側から設定するbackend serviceは、Kubernetes側から作成するbackend configを関連づけたserviceを作成することでGKE Ingress Controllerを通して作成されると思っていましたが、実際にはそれだけでは作成されない設定のようでした。 調べるとKubernetes側からbackend configを関連づけたserviceはあくまでKubernetesのserviceとしてまでで、ロードバランサの設定で必要となるbackend serviceはこのbackend configを関連づけたserviceなどを基に作成されるGCP側での定義設定であったため、Kubernetes側からでは実態が確認できないものでした。 また当たり前かもしれませんが、このロードバランサ側が利用するbackend service部分のみを指定して構築することはGKE Ingress Controllerからでは難しそうでした。

そのための解決策として、一度GKE Ingress ControllerからIngressリソースを丸っと構築することで、Kubernetes側でbackend configやservice等の設定変更管理が可能でかつGCPのロードバランサが必要とするbackend serviceを作成するという対応をとりました。

ちなみに運用の中でKubernetes側で行いたいbackend configやserviceの設定変更の追従は、backend serviceのみではなくこのIngressリソース自体がGCP側に認識され続けている状態でないと行われません。 例えばbackend serviceが作成された後はもう利用しないからとbackend configやそれに付随するservice以外のIngress設定を消してしまうと、以降Kubernetes側からの変更が反映されなくなるため、通信ができなくなる等の新たな事故の発生要因となってしまいます。 つまり、この形でbackend serviceを運用し続ける限り、Ingressリソースは残しておく必要があります。 GKE Ingress Controllerから起動したIngressリソースのうち、backend service以外もダミーとしてKubernetesの管理下で運用します。

問題点を考慮した構成案

GKE Ingress ControllerがIngressの作成をきっかけに設定するGCPのロードバランサ側のbackend serviceを利用するイメージです。 Kubernetes側のbackend configやserviceの設定を抱き込む形で作成され、Ingressを残し続けKubernetes側からのbackend configやserviceの変更が追従される状態を作ります。

構築

経緯項での内容を考慮した最終的な構成で構築していきますが、GKE Ingress Controllerを用いたbackend service並びにGKE Ingressについては、GKEの各環境によっても異なるため下記の公式ドキュメント等を参照しつつ利用したいbackend serviceを考慮しながら構築していただければと思います。

アプリケーション ロードバランサ用の GKE Ingress

Ingress構築後、GCPコンソール上のロードバランシングのページの中で対象リソースを確認できればGCP側にリソースとして認識されている状態です。 また、バックエンドのタブで構築したIngressに紐づくbackend serviceも確認できます。

続けて、ロードバランサの設定をterraformで行っていきます。

mTLS設定部分と、カスタムヘッダー設定は前回記事を踏襲するためここでは割愛します。そちらを参照ください。

全体構成としては少しイレギュラーですが、ロードバランサ単体の設定としての基本設計は一般的な内容と大きく変わりません。 backend serviceをterraformのデータソースで持ってきている箇所や、今回もともと設定したかったServer TLS Policyの部分などがポイントです。 必要に応じて、ロードバランサをStaticIPで固定しDNS登録しなおすなどしていただければと思います。

# 固定IP
resource "google_compute_global_address" "this" {
  name = "sample-google-compute-global-address"
}

# googleマネージドサーバ証明書
resource "google_compute_managed_ssl_certificate" "this" {
  name = "sample-google-compute-managed-ssl-certs"

  managed {
    domains = "sample.example.com"
  }
}

# SSLPolicy
resource "google_compute_ssl_policy" "this" {
  name        = "sample-google-compute-ssl-policy"

  profile         = "MODERN"
  min_tls_version = "TLS_1_2"
}

# Target HTTP(S) proxy
resource "google_compute_target_https_proxy" "this" {
  name             = "sample-google-compute-target-https-proxy"
  url_map          = google_compute_url_map.this.self_link
  ssl_policy       = google_compute_ssl_policy.this.name
  ssl_certificates = [google_compute_managed_ssl_certificate.this.certificate_id]

  server_tls_policy = google_network_security_server_tls_policy.this.id
}

# forwarding rule
resource "google_compute_global_forwarding_rule" "this" {
  name                  = "sample-google-compute-forwarding-rule"
  target                = google_compute_target_https_proxy.this.self_link
  ip_address            = google_compute_global_address.this.address
  port_range            = "443"
  ip_protocol           = "TCP"
  load_balancing_scheme = "EXTERNAL"
}

# KubernetesからIngressを作成して作られたバックエンドサービスを持ってくる
data "google_compute_backend_service" "existing_backend_service" {
  name = "sample-google-compute-backend-service"
}

# url map
resource "google_compute_url_map" "this" {
  name            = "sample-loadbalancer"
  default_service = data.google_compute_backend_service.existing_backend_service.self_link
}

アクセス確認

前回と同じように、今回の構成で通信確認をしたログが下記になります。

$ wget -t 1 --private-key ./sample_client_cert.key --certificate ./sample_client_cert.pem https://sample.example.com/test_path
--20xx-xx-xx 00:00:00--  https://sample.example.com/test_path
sample.example.com (sample.example.com) をDNSに問いあわせています... XXX.XXX.XXX.XXX
sample.example.com (sample.example.com)|XXX.XXX.XXX.XXX|:443 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 15 [application/json]
`test_path' に保存中

test_path   100%[=====================>]      1  --.-KB/s 時間 0s

20xx-xx-xx 00:00:00 (X.XX MB/s) - `test_path' へ保存完了 [1/1]

backend serviceの設定を行うbackend configの部分でログを出すようにすればGCPのログエクスプローラでも認証状況も確認できます。

正しいクライアント証明書をもとに正常にアクセスできている場合には以下のようなログがでました。

こちらはリクエスト側でクライアント証明書を指定せずにアクセスした時のログになります。statusDetailsがclient_cert_not_providedになっておりアクセスが拒否されているのがわかります。

その他注意点など

ロードバランサ側のbackend serviceの作成とKubernetesからの設定変更維持のために、ダミーとしてKubernetes側からIngressを起動させ続けています。 見かけ上通信は発生しないため、運用中に誤って消してしまうなどにはご注意いただければと思います。 加えて、ダミーのIngressはダミーではありつつも外部との接点箇所になりうるため、外部からのIPやポートを全て閉じておくなどセキュリティ面については十分注意してください。

また、今回のmTLS通信ではクライアント証明書の情報をロードバランサにそのまま登録して許可するAllowListedCertificates設定で簡易的な証明書検証を行っています。 ロードバランサ上でクライアント証明書の検証を行う一般的なmTLS通信を実現するには、GCPによるクライアント証明書の制約がありますので最初にご確認することをおすすめします。 ちなみに、弊社ではこのクライアント証明書の制約に一部引っかかってしまい、現段階ではまだ運用のフェーズには至っておりません。

証明書の要件

おわりに

GKEをバックエンドに持つ構成でGCPのロードバランサでのmTLS機能設定を行いました。 イレギュラー構成のためある程度の運用リスクはあるものの、行いたい通信・構成の実現はできました。 GKE環境からとGCP側ロードバランサからとの分離点をbackend serviceではなく別の箇所で持つような構成についても考えましたが、弊社として統合環境のメリットがあるkenos環境の構成からさらに逸脱した構成・運用となることを鑑みると良い代案はすぐには出てこなさそうでした。状況によっては他の方法も考えられるかもしれません。 ただやはり、GKE Ingress ControllerからmTLSを制御できるようになればシンプルかつスマートに機能実現できるため、将来的にはこの部分が連携してくれたらいいなと期待しています。 なお、今回はかなり独自ハック的な構成となっているため、同じような設定をされる際には、あくまで参考までとして自己責任にてお願いします。