TT Lab
はじめる
学ぶ 学習パス コース

Istio深化 — なぜそう流れるのか

三つの TLS モードを起動し、誰が証明書を提示するかを確かめる

TT Labで続きを見る

目標

GatewayのTLSモードが、ゲートウェイEnvoyリスナーの何になるかをルールとして書き写し、SIMPLE・MUTUAL・PASSTHROUGHとhttpsRedirectを等価なリスナーとして起動して、クライアントが受け取った証明書で、TLSが終わる場所を証明します。

なぜ重要なのか

ゲートウェイの証明書障害は、たいてい「TLSがどこで終わるのか」を誤解していることから来ます。ゲートウェイの証明書を直したのに、クライアントはバックエンドの証明書を見ていたり、SNIがなくてチェーンを選べなかった接続を、証明書の問題と取り違えたりします。モードごとにEnvoyに何ができるかがわかれば、リスナー設定1枚と統計1行で切り分けられます。

ステップ

  1. /root/ist2-gw/gw.yamlにGatewayを書いてください。名前はshop-gw、ネームスペースはdefault、selectorはistio: ingressgateway、サーバーは2つです。①ポート443・名前https・プロトコルHTTPS、hostsshop.example.com、tls.mode: SIMPLE、tls.credentialName: shop-cert。②ポート80・名前http・プロトコルHTTP、hostsshop.example.com、tls.httpsRedirect: true。istioctl validate -f gw.yamlの出力と終了コードを、/root/ist2-gw/01-validate.txtに入れてください(最後の行はrc=)。その後、credentialNameの行だけを除いたコピーを/root/ist2-gw/gw-nocred.yamlとして作り、同じコマンドで検査して、/root/ist2-gw/01-nocred.txtに入れてください。
  2. /root/ist2-gw/02-modes.txtに5行を書いてください。SIMPLE、MUTUAL、OPTIONAL_MUTUAL、PASSTHROUGH、ISTIO_MUTUALごとに、<모드> terminates=<gateway|upstream> client_cert=<none|required|optional|mesh> filter=<hcm|tcp_proxy> needs_vs=<yes|no>の形(プレースホルダーはモードです)で1行ずつ、空白で区切って書きます。terminatesはTLSを復号する場所、client_certはゲートウェイがクライアント証明書をどう扱うか(meshはistiodが発行したメッシュ証明書を要求します)、filterはチェーンの最後のネットワークフィルター、needs_vsはトラフィックが流れるためにVirtualServiceが必要かどうかです。サーバーのプロトコルは、HTTPS(PASSTHROUGHはTLS)として扱います。
  3. /root/ist2-gw/certsにopensslで証明書を作ってください。CA(ca.crt・ca.key、サブジェクト/O=lab/CN=lab-ca)と、そのCAが署名したゲートウェイ証明書(gateway.crt・gateway.key、サブジェクト/O=gateway/CN=shop.example.com、SANDNS:shop.example.com)です。その後、/root/ist2-gw/gw-simple.yamlにEnvoyの設定を書いてください。管理ポートは9988、リスナー127.0.0.1:10088のチェーンにDownstreamTlsContext(ゲートウェイ証明書)とHCMを置き、ルート設定の名前はIstioがこのHTTPSサーバーに付ける名前https.443.https.shop-gw.default、domainsにshop.example.com、クラスターoutbound|8117||shop.default.svc.cluster.local → 127.0.0.1:8117(平文のアップストリームupstream.py 8117 ok)です。起動した後、shop.example.comでリクエストして、/root/ist2-gw/03-simple.txtに4行を書いてください。code=(HTTPコード)、body=(レスポンス本文)、seen_o=(クライアントが受け取った証明書のサブジェクトのOの値だけ)、seen_sha256=(その証明書のSHA-256フィンガープリント。openssl x509 -fingerprint -sha256の出力の=の後ろ)です。
  4. 同じCAでクライアント証明書/root/ist2-gw/certs/client.crt・client.key(サブジェクト/O=client/CN=client、SANDNS:client)を作ってください。/root/ist2-gw/gw-mutual.yamlは、gw-simple.yamlと同じで、DownstreamTlsContextにrequire_client_certificate: trueとvalidation_context.trusted_ca(/root/ist2-gw/certs/ca.crt)を加えたものです。起動した後、/root/ist2-gw/04-mutual.txtに4行を書いてください。without_cert_code=・without_cert_rc=(クライアント証明書なしでリクエストしたHTTPコードとcurlの終了コード)、with_cert_code=(--cert・--keyでリクエストしたHTTPコード)、fail_verify_no_cert=(統計listener.127.0.0.1_10088.ssl.fail_verify_no_certの値)です。
  5. 同じCAでバックエンド証明書/root/ist2-gw/certs/backend.crt・backend.key(サブジェクト/O=backend/CN=shop.example.com、SANDNS:shop.example.com)を作り、openssl s_server -accept 8111 -cert /root/ist2-gw/certs/backend.crt -key /root/ist2-gw/certs/backend.key -www -quietでTLSアップストリームを起動してください。/root/ist2-gw/gw-pass.yamlにPASSTHROUGHゲートウェイを書いてください。管理ポートは9988、リスナー127.0.0.1:10088にtls_inspectorリスナーフィルター、チェーンはfilter_chain_match.server_names: ["shop.example.com"]とtcp_proxy1つ(transport_socketなし)、クラスターoutbound|8111||shop.default.svc.cluster.local → 127.0.0.1:8111です。起動した後、shop.example.comでリクエストして、/root/ist2-gw/05-passthrough.txtに3行を書いてください。code=、seen_o=、seen_sha256=(ステップ3と同じ方法)です。
  6. gw-pass.yamlで起動したゲートウェイに、2回接続してみてください。①SNIをother.example.comにして(--resolve other.example.com:10088:127.0.0.1)、②SNIなしでIPで(https://127.0.0.1:10088/)です。どちらも--cacert /root/ist2-gw/certs/ca.crtを渡します。/root/ist2-gw/06-sni.txtに5行を書いてください。sni=other.example.com、sni_rc=(①のcurl終了コード)、no_sni_rc=(②の終了コード)、stat=(チェーンを選べなかった接続を数えるリスナー統計の完全な名前)、count=(2回接続した後のその統計の値)です。
  7. /root/ist2-gw/gw-redirect.yamlに、ステップ1のポート80のサーバーに当たる平文リスナーを書いてください。管理ポートは9988、リスナーは127.0.0.1:10098(transport_socketなし)、HCMのルート設定の名前はIstioが平文の80のサーバーに付ける名前http.80、virtual hostのdomainsにshop.example.com、そのvirtual hostにrequire_tls: ALL、ルートはoutbound|8117||shop.default.svc.cluster.local → 127.0.0.1:8117です。起動した後、curl -H 'Host: shop.example.com' localhost:10098/cartの結果を、/root/ist2-gw/07-redirect.txtに2行で書いてください。code=(HTTPコード)、location=(Locationヘッダーの値そのまま)です。
  8. /root/ist2-gw/08-report.mdに5行を書いてください。simple_seen_o=(ステップ3で見たO)、passthrough_seen_o=(ステップ5)、mutual_without_cert_code=(ステップ4)、sni_mismatch_rc=(ステップ6の①の終了コード)、redirect_code=(ステップ7)です。その下に、- で始まる説明を4行以上書いてください。

参考

Gatewayのサーバー2つを書き、istioctlがSIMPLEに要求するものを見る

/root/ist2-gw/gw.yamlにGatewayを書いてください。名前はshop-gw、ネームスペースはdefault、selectorはistio: ingressgateway、サーバーは2つです。①ポート443・名前https・プロトコルHTTPS、hostsshop.example.com、tls.mode: SIMPLE、tls.credentialName: shop-cert。②ポート80・名前http・プロトコルHTTP、hostsshop.example.com、tls.httpsRedirect: true。istioctl validate -f gw.yamlの出力と終了コードを、/root/ist2-gw/01-validate.txtに入れてください(最後の行はrc=)。その後、credentialNameの行だけを除いたコピーを/root/ist2-gw/gw-nocred.yamlとして作り、同じコマンドで検査して、/root/ist2-gw/01-nocred.txtに入れてください。

Gatewayは、ゲートウェイPodのEnvoyにポートと証明書だけを開くリソースです。ルートは、VirtualServiceが付いて初めてできます。SIMPLEは「ゲートウェイがTLSを終端する」という意味なので、サーバー証明書が必ず必要で、IstioはそれをcredentialNameが指すSecretから受け取ります。そのため、その行を除くと、クラスターがなくてもistioctlが拒否します。拒否の理由をそのまま入れてください。終了コードは、コマンドの直後の$?です。sed '/credentialName/d'で1行だけ除けます。

5つのTLSモードを、Envoy側の形に書き写す

/root/ist2-gw/02-modes.txtに5行を書いてください。SIMPLE、MUTUAL、OPTIONAL_MUTUAL、PASSTHROUGH、ISTIO_MUTUALごとに、<모드> terminates=<gateway|upstream> client_cert=<none|required|optional|mesh> filter=<hcm|tcp_proxy> needs_vs=<yes|no>の形(プレースホルダーはモードです)で1行ずつ、空白で区切って書きます。terminatesはTLSを復号する場所、client_certはゲートウェイがクライアント証明書をどう扱うか(meshはistiodが発行したメッシュ証明書を要求します)、filterはチェーンの最後のネットワークフィルター、needs_vsはトラフィックが流れるためにVirtualServiceが必要かどうかです。サーバーのプロトコルは、HTTPS(PASSTHROUGHはTLS)として扱います。

分かれ道は1つです。ゲートウェイEnvoyのチェーンにDownstreamTlsContextができるかどうかです。できれば、ゲートウェイが復号し、復号したのでHTTPを読んでHCMでルーティングできます。できなければ、ゲートウェイには暗号文しか見えないので、SNIでチェーンだけを選んで、バイトを渡すしかありません。クライアント証明書は、Envoyのrequire_client_certificateとvalidation_contextの2つの値の組み合わせとして考えてください。見本の行のAUTO_PASSTHROUGHは、SNI自体に宛先が書かれていて、VirtualServiceが不要な例外です。

SIMPLE: ゲートウェイがTLSを終端して、自分の証明書を出す

/root/ist2-gw/certsにopensslで証明書を作ってください。CA(ca.crt・ca.key、サブジェクト/O=lab/CN=lab-ca)と、そのCAが署名したゲートウェイ証明書(gateway.crt・gateway.key、サブジェクト/O=gateway/CN=shop.example.com、SANDNS:shop.example.com)です。その後、/root/ist2-gw/gw-simple.yamlにEnvoyの設定を書いてください。管理ポートは9988、リスナー127.0.0.1:10088のチェーンにDownstreamTlsContext(ゲートウェイ証明書)とHCMを置き、ルート設定の名前はIstioがこのHTTPSサーバーに付ける名前https.443.https.shop-gw.default、domainsにshop.example.com、クラスターoutbound|8117||shop.default.svc.cluster.local → 127.0.0.1:8117(平文のアップストリームupstream.py 8117 ok)です。起動した後、shop.example.comでリクエストして、/root/ist2-gw/03-simple.txtに4行を書いてください。code=(HTTPコード)、body=(レスポンス本文)、seen_o=(クライアントが受け取った証明書のサブジェクトのOの値だけ)、seen_sha256=(その証明書のSHA-256フィンガープリント。openssl x509 -fingerprint -sha256の出力の=の後ろ)です。

SIMPLEのサーバー1つは、Envoyではチェーン1つにtransport_socket1つです。Istioは、証明書をファイルではなくSDS(kubernetes://shop-cert)で入れますが、ファイルで入れても、Envoyが見るものは同じです。名前で接続するには、curl --resolve 호스트:포트:127.0.0.1 --cacert(プレースホルダーはホストとポートです)を使い、クライアントが実際に受け取った証明書は、openssl s_client -connect … -servername …の出力をopenssl x509に渡して見てください。SANを証明書に引き継ぐには、署名するときに-copy_extensions copyallが必要です。ルート名のルールは、https.<포트>.<포트이름>.<Gateway이름>.<네임스페이스>(プレースホルダーはポート、ポート名、Gateway名、ネームスペースです)です。

MUTUAL: 同じチェーンにクライアント証明書の要求が加わる

同じCAでクライアント証明書/root/ist2-gw/certs/client.crt・client.key(サブジェクト/O=client/CN=client、SANDNS:client)を作ってください。/root/ist2-gw/gw-mutual.yamlは、gw-simple.yamlと同じで、DownstreamTlsContextにrequire_client_certificate: trueとvalidation_context.trusted_ca(/root/ist2-gw/certs/ca.crt)を加えたものです。起動した後、/root/ist2-gw/04-mutual.txtに4行を書いてください。without_cert_code=・without_cert_rc=(クライアント証明書なしでリクエストしたHTTPコードとcurlの終了コード)、with_cert_code=(--cert・--keyでリクエストしたHTTPコード)、fail_verify_no_cert=(統計listener.127.0.0.1_10088.ssl.fail_verify_no_certの値)です。

MUTUALは、SIMPLEの上に2つの値を載せたものです。「証明書を必ず出せ」(require_client_certificate)と「何で検証するか」(validation_context)です。Istioでは、検証用CAがcredentialNameのSecretのca.crt(または<이름>-cacert)から来ます(プレースホルダーは名前です)。証明書なしで接続すると、HTTPまで行けないので、コードは「応答がない」という意味の値になり、終了コードはTLSのバージョンによって違います。そのまま書いてください。拒否は、Envoy側の統計に残ります(/statsをgrep)。

PASSTHROUGH: ゲートウェイはSNIだけを読み、バックエンドの証明書が見える

同じCAでバックエンド証明書/root/ist2-gw/certs/backend.crt・backend.key(サブジェクト/O=backend/CN=shop.example.com、SANDNS:shop.example.com)を作り、openssl s_server -accept 8111 -cert /root/ist2-gw/certs/backend.crt -key /root/ist2-gw/certs/backend.key -www -quietでTLSアップストリームを起動してください。/root/ist2-gw/gw-pass.yamlにPASSTHROUGHゲートウェイを書いてください。管理ポートは9988、リスナー127.0.0.1:10088にtls_inspectorリスナーフィルター、チェーンはfilter_chain_match.server_names: ["shop.example.com"]とtcp_proxy1つ(transport_socketなし)、クラスターoutbound|8111||shop.default.svc.cluster.local → 127.0.0.1:8111です。起動した後、shop.example.comでリクエストして、/root/ist2-gw/05-passthrough.txtに3行を書いてください。code=、seen_o=、seen_sha256=(ステップ3と同じ方法)です。

PASSTHROUGHでは、ゲートウェイは証明書もキーも持ちません。それでも宛先を選ぶ必要があるので、TLSの最初のメッセージ(ClientHello)に平文で載っているSNIだけをtls_inspectorで読み取り、その名前をserver_namesでマッチさせてチェーンを選びます。復号していないのでHCMは使えず、tcp_proxyがバイトをそのまま渡します。そのため、ハンドシェイクの相手はバックエンドで、クライアントが受け取る証明書もバックエンドのものです。ステップ3の結果とフィンガープリントを比べてみてください。s_server -wwwは、HTTPリクエストに200とステータスページで答えます。

SNIが合わない、またはないと、選べるチェーンがない

gw-pass.yamlで起動したゲートウェイに、2回接続してみてください。①SNIをother.example.comにして(--resolve other.example.com:10088:127.0.0.1)、②SNIなしでIPで(https://127.0.0.1:10088/)です。どちらも--cacert /root/ist2-gw/certs/ca.crtを渡します。/root/ist2-gw/06-sni.txtに5行を書いてください。sni=other.example.com、sni_rc=(①のcurl終了コード)、no_sni_rc=(②の終了コード)、stat=(チェーンを選べなかった接続を数えるリスナー統計の完全な名前)、count=(2回接続した後のその統計の値)です。

PASSTHROUGHリスナーには、server_namesの付いたチェーンが1つだけで、デフォルトのチェーンがありません。名前が違うか、IPで接続してcurlがSNIをまったく送らないと、Envoyは選べるチェーンがなく、接続をすぐに閉じます。curlからはハンドシェイク失敗に見えます。リスナー統計の名前はlistener.<주소>_<포트>.(プレースホルダーはアドレスとポートです)で始まります。/statsでfilter_chainを探してみてください。

httpsRedirectはvirtual hostのrequire_tlsの1行になる

/root/ist2-gw/gw-redirect.yamlに、ステップ1のポート80のサーバーに当たる平文リスナーを書いてください。管理ポートは9988、リスナーは127.0.0.1:10098(transport_socketなし)、HCMのルート設定の名前はIstioが平文の80のサーバーに付ける名前http.80、virtual hostのdomainsにshop.example.com、そのvirtual hostにrequire_tls: ALL、ルートはoutbound|8117||shop.default.svc.cluster.local → 127.0.0.1:8117です。起動した後、curl -H 'Host: shop.example.com' localhost:10098/cartの結果を、/root/ist2-gw/07-redirect.txtに2行で書いてください。code=(HTTPコード)、location=(Locationヘッダーの値そのまま)です。

Istioは、httpsRedirect: trueのサーバーのvirtual hostにrequire_tls: ALLを入れます。すると、Envoyはルートを見る前に、平文のリクエストをhttpsへ送り返します。アップストリームまで行きません。平文のHTTPサーバーのルート名は、ゲートウェイ名なしのhttp.<포트>(プレースホルダーはポートです)なので、同じポートを使う複数のGatewayのホストが、1つのルート設定にまとまります。ヘッダーはcurl -s -D - -o /dev/nullで見てください。Locationのホストは、リクエストのHostヘッダーから来ます。

モードごとに、誰がTLSを終端するかをまとめる

/root/ist2-gw/08-report.mdに5行を書いてください。simple_seen_o=(ステップ3で見たO)、passthrough_seen_o=(ステップ5)、mutual_without_cert_code=(ステップ4)、sni_mismatch_rc=(ステップ6の①の終了コード)、redirect_code=(ステップ7)です。その下に、- で始まる説明を4行以上書いてください。

値は、前のステップのファイルから移してください。説明の行には、「このモードで、ゲートウェイEnvoyのチェーンに何ができ、そのためクライアントに何が見えるのか」を組にして書くと、本番で証明書の問題に出会ったとき、どこから見るかが整理されます。