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

はじめに

SREグループ・ヒロチカです。GO株式会社では、サービスのクラウドインフラの設計から構築・運用までを担当しています。 今回、GCPで利用しているロードバランサーでクライアント認証の機能をmTLSで実現したいという要件があり、いくつか構築しながら調査したのでこちらで紹介できればと思います。

経緯

GKEにあるレイヤ4のロードバランサーを経由しTLSの終端まで独自実装で行ってるサービスが有り、今まで個別にサーバ証明書を購入し更新していたのですが、このサービス仕様が少し特殊なこともあり構成情報を把握しているエンジニアも少なく証明書管理が適切に行われずトラブルに発展した事案がありました。 そこでこのサービスについてレイヤ7のロードバランサーを利用するように構成変更し、TLSの終端をロードバランサー側で行うようにすることで今まで個別に証明書を購入し更新していた箇所をGoogleのマネージドな証明書に置き換え、証明書管理の省力化と属人化の解消を行いたいと検討を始めました。 このGoogleマネージド証明書はサポートされるロードバランサーが限られているため、今回の構成ではロードバランサーをレイヤ7のアプリケーションロードバランサーに変更する必要があるのですが、今回のサービスはクライアント証明書を持ったリクエストを受け付けるサービスのため、クライアント証明書の情報をサービスの方まで流してあげる必要がありました。 この要件の中で、サーバ証明書をレイヤ7のロードバランサーに置き換える際、このリクエストのクライアント証明書の情報をそのままパススルーして渡せれば良かったのですがGCPのレイヤ7のロードバランサーにはパススルー機能がなさそうで、その代替案としてmTLS認証の機能を使った方法が取れそうだと今回の調査を行う運びとなりました。 なお、今回マネージドにしたいサーバ証明書とリクエストに含まれるクライアント証明書は関連性のない別ものの証明書です。

構成

既存の構成と、今回新しく考えた構成の比較図となります。

既存構成

  • サーバ証明書TLSの終端はアプリケーション
  • レイヤ4ロードバランサでは、リクエストに含まれるクライアント証明書をそのままサーバへ流している
  • リクエストに含まれるクライアント証明書の検証もアプリケーション側実装

新構成

  • サーバ証明書TLSの終端をロードバランサーで行い、証明書はGoogleマネージド証明書を利用
  • mTLS機能を使うためクライアント証明書の認証で使う情報をロードバランサーに登録しておき、ロードバランサーでクライアント証明書の認証を行う
  • アプリケーション側でクライアント証明書の情報を確認できるようmTLSで認証が通った場合はヘッダーにクライアント証明書の情報を入れてアプリケーションへと渡す
  • ロードバランサーからアプリケーションまでは、GKEの内部のネットワークを通る

mTLS(相互TLS)について

mTLSは、サーバとクライアントが相互に認証しあう仕組みのことです。 一般的なHTTPS通信でのTLS認証では、クライアントが「サーバ証明書」を検証する形でのみ行われますが、mTLS認証では、サーバ証明書を検証する形に加えて、クライアントからサーバに渡す「クライアント証明書」も使い検証を行います。 クライアントとサーバの両端において、お互いに正しい秘密鍵を持っていることを確認しあうことでクライアントとサーバの当事者同士がお互いに正しいことを確認できます。 今回のケースでは特にサーバ側から見てアクセス元のクライアントが正しいかを確認するために、このクライアント証明書を持ったアクセスを行っていました。

構築

参考ドキュメント: - 相互TLS認証 - ユーザー指定の証明書を使用して相互 TLS を設定する

基本、上記の公式ドキュメントにしたがって実装していきます。

ロードバランサーに登録したいクライアント証明書の一式は既存のものを使うため、すでに準備済みという状態からスタートです。

クライアント証明書の情報を登録

ロードバランサーに登録するためのクライアント証明書の情報を登録していきます。

Trust Configの作成・登録

証明書マネージャにある信頼構成(Trust Config)に、クライアント証明書情報を登録します。

  • terraformでの例
resource "google_certificate_manager_trust_config" "default" {
  name        = "sample-trust-config"
  description = "sample description for the trust config"
  location    = "asia-northeast1"

  trust_stores {
    trust_anchors { 
      pem_certificate = file("./root_cert.pem")
    }
    intermediate_cas { 
      pem_certificate = file("./ca_cert.pem")
    }
  }
}

これで、Trust Configが登録されます。

なお、今回のクライアント証明書の認証については、上記のterraformの例のようにCA情報を登録する形ではなくクライアント証明書の情報をそのまま登録して許可する形(allowListedCertificates)で行いたかったため、trust-config.yamlファイルを直接作成して登録しました。 ちなみに、terraformの方ではまだallowListedCertificatesの方式は対応していないようです。 なので、このTrust Config情報を直接yamlで作成してimportする方法で行いました。 ドキュメントにあるようにCA情報を直接yamlで登録する際は、証明書情報を下記のように改行などをフォーマットする必要があります。

$ export ALLOWLISTED_CERT=$(cat sample_client_cert.pem | sed 's/^[ ]*//g' | tr '\n' $ | sed 's/\$/\\n/g')

$ cat << EOF > sample_trust_config.yaml
name: sample-trust-config
allowlistedCertificates:
- pemCertificate: "${ALLOWLISTED_CERT?}"
EOF

gcloudコマンドでimportする

gcloud certificate-manager trust-configs import sample-trust-config --source=sample_trust_config.yaml

登録情報はコンソールの証明書マネージャの信頼構成の項目で確認できます。

ss1.png

Server TLS Policyの作成

Server TLS Policyでは、Trust Configに設定したクライアント証明書検証後のロードバランサーの動きを定義します。 以下、terraformで作成する例になります。

resource "google_network_security_server_tls_policy" "server_tls_policy" {
  provider = google-beta
  name     = "sample-server-tls-policy"

  description = "sample server tls policy"
  project     = GCP_PROJECT
  location    = "global"
  allow_open  = "false"

  mtls_policy {
    client_validation_mode = "REJECT_INVALID"
    # client_validation_mode         = "ALLOW_INVALID_OR_MISSING_CLIENT_CERT"
    client_validation_trust_config = "projects/GCP_PROJECT/locations/global/trustConfigs/sample-trust-config"
  }
}

先ほど作成したTrust Configの設定や、プロジェクト名、location等は適宜調整してください。 上記の設定のうちclient_validation_modeで、認証成功可否に合わせたクライアントの接続処理を定義します。 ALLOW_INVALID_OR_MISSING_CLIENT_CERTREJECT_INVALID2つのモードがあり、大まかな違いとしてクライアント証明書自体がない場合・検証で失敗した場合に、全てのリクエストを拒否するのがREJECT_INVALIDで、全てのリクエストを許可するのがALLOW_INVALID_OR_MISSING_CLIENT_CERTになります。 例えば、検証が失敗している状態をヘッダーを通して後ろのアプリケーションに渡したい場合はALLOW_INVALID_OR_MISSING_CLIENT_CERTを設定することになります。

詳細は、こちらのページにあります。 MTLSクライアント検証モード

設定後はコンソールやgcloudコマンドでも確認できます。

gcloud beta network-security server-tls-policies list --location=global

ss2.png

ロードバランサーの構築

今回は、mTLS認証の設定確認を行うサンプルとして、適当な従来の外部アプリケーションロードバランサーをコンソールから作っています。 この構築部分で、今回の大元の目的でもあるGoogleマネージド証明書を使うように設定します。 こちら特別な構築方法をおこなっているわけではありませんので、通信要件にあわせたロードバランサーのタイプ・バックエンド・フロントエンドのケースで構築いただければと思います。

なおmTLSに対応してるGCPロードバランサーの種類は、こちらのドキュメントに記載されています。

ロードバランサーへ認証情報を登録

作成したServer TLS Policyをロードバランサーへと登録していきます。

対象のロードバランサーのtarget-proxyを探し、現在の設定をexportします。

$ gcloud compute target-https-proxies list
NAME                   SSL_CERTIFICATES URL_MAP
sample-lb-target-proxy sample-lb        sample-lb

$ gcloud beta compute target-https-proxies export sample-lb-target-proxy --destination=mtls_target_proxy.yaml --global

Server TLS Policyを1行追加します。

$ echo "serverTlsPolicy: //networksecurity.googleapis.com/projects/GCP_PROJECT/locations/global/serverTlsPolicies/sample-server-tls-policy" >> mtls_target_proxy.yaml

編集したyamlを、先ほどのexportしたtarget-proxyに対して上書きimportします。

$ gcloud beta compute target-https-proxies import sample-lb-target-proxy --source=mtls_target_proxy.yaml --global
Target Https Proxy [sample-lb-target-proxy] will be overwritten.

Do you want to continue (Y/n)?  Y

Updating TargetHttpsProxy...done.

コンソールからもクライアント認証という列が現れて、設定されている状態が確認できます。

設定前

設定後

カスタムヘッダーの設定

今回の構成ではmTLS認証が成功した後、クライアント証明書の認証に関する情報をアプリケーションに流すリクエストのヘッダーに入れて渡してあげる必要があります。

バックエンドに渡すことができるカスタムヘッダー値とその内容は、こちらのドキュメントに記載されています。 設定したClient Validation Modeモードによって渡せる内容も変わってきます。

こちらもgcloudコマンドや、コンソールのロードバランサの設定内にあるバックエンドの構成の項目などから設定することができます。

$ gcloud beta compute backend-services update sample-backend-service \
  --global \
  --custom-request-header='X-Client-Cert-Leaf:{client_cert_leaf}'

アクセス確認

ここまでの設定を行った環境で通信確認を行ったものが下記になります。

$ wget -t 1 --private-key ./sample_client_cert.key --certificate ./sample_client_cert.pem https://mtls-sample.sample-host.example.com/test_path
--20xx-xx-xx 00:00:00--  https://mtls-sample.sample-host.example.com/test_path
mtls-sample.sample-host.example.com (mtls-sample.sample-host.example.com) をDNSに問いあわせています... XXX.XXX.XXX.XXX
mtls-sample.sample-host.example.com (mtls-sample.sample-host.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]

REJECT_INVALIDを設定しているため、 鍵が渡されないパターンや鍵が異なるパターンでは、サーバ証明書だけ通った後のクライアント認証部分でしっかり失敗してくれます。

$ wget -t 1 https://mtls-sample.sample-host.example.com/color
--20xx-xx-xx 00:00:00--  https://mtls-sample.sample-host.example.com/color
mtls-sample.sample-host.example.com (mtls-sample.sample-host.example.com) をDNSに問いあわせています... XXX.XXX.XXX.XXX
mtls-sample.sample-host.example.com (mtls-sample.sample-host.example.com)|XXX.XXX.XXX.XXX|:443 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... データが受信されませんでした
中止しました。

期待しない失敗の場合などは、ロードバランサーでcloud loggingにログを出すなどして確認してください。 今回の検証で確認したアプリケーションについてはたまたまヘッダーのログを出していたため、リクエストが通った場合に設定したヘッダーが期待通りアプリケーションに送られていることも確認できました。

・・・・
20xx/xx/xx 00:00:00 x-client-cert-leaf --> [:XXXXXXXX・・・・XXXXXX:]
・・・

注意事項

証明書を更新したい、Client Validation Modeの変更をしたい、といった場合にそれぞれのconfigファイルを作り直したいケースがあるかと思いますがそのまま更新できませんでした。 新しいconfigファイルを作成した上で付け替える、依存するそれぞれの設定を一度削除して作りなおすといった手順が必要となってきます。

また、Googleマネージド証明書にしたサーバ証明書についてはよいのですが、クライアント証明書の方はマネージドではないため有効期限のチェックや更新などの証明書管理を忘れずに行う必要があります。

通信経路についても、レイヤ7のロードバランサーTLS終端を行うのでアプリケーションまでの経路を保護したい場合には注意してください。 あわせてアプリケーションへ渡すよう設定したヘッダー情報についても注意が必要です。 今回、例に出したような形で証明書情報を意図せずにそのままログに出している場合などがあるかと思うので、ヘッダー情報をログに出さない、アプリケーション側でマスクする、などの考慮が必要です。

おわりに

今回、GCPでのmTLS認証について調査・構築し、取り急ぎやりたいことが実現できる状態はできました。 所感としてかなり泥臭く設定していく必要があったり、terraformについてもまだ未対応の設定パラメータがあるようで、これからアップデートされるであろう設定箇所などもまだまだありそうです。 最新情報を確認の上、設定をしていただければと思います。

ちなみに、このアプリケーションの新構成は弊社が統合管理しているGKE環境で運用したいと考えており、この記事で行ったmTLS認証まわりのロードバランサー設定をGKEのコントローラーから構築し管理できるのかを調査して検証していく必要がありました。 それはまた別のお話で...