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

Envoyの内部構造

証明書二枚で一つのポートを分ける

TT Labで続きを見る

目標

1つのリスナーに複数のフィルターチェーンを置き、SNI・トランスポートプロトコル・ALPNで振り分けて受けます。条件が重なったときにどちらが勝つかも、自分で確認します。

なぜ重要なのか

イングレスゲートウェイ1台が数十のドメインを受ける構成では、「どのチェーンがこの接続を受けるか」がそのまま「どの証明書を提示するか」になります。この規則を順序と誤解すると、チェーンを上下に動かしながら1日を費やすことになります。実際の判定は8段階の固定された順序で具体性を比較して行われ、一致するチェーンがないとエラー応答ではなく接続がただ切れます。この2つを手で経験しておけば、「このドメインだけ動かない」という報告を数分で片付けられます。

ステップ

  1. /root/envd-listener/listener.yamlにリスナーを2つ置いてください。edge-aは127.0.0.1:10021でlistener=aを、edge-bは127.0.0.1:10022でlistener=bを、direct_responseで返します(管理ポートは9921)。起動したら、/listeners?format=jsonを/root/envd-listener/01-listeners.jsonに保存してください。
  2. /root/envd-listener/tls/に自己署名証明書を2枚作ってください。a.crt/a.keyはCNとSANがa.envd.test、b.crt/b.keyはb.envd.testです。2つの証明書のsubjectとSANを/root/envd-listener/02-certs.txtに書いてください。
  3. /root/envd-listener/listener.yamlにリスナーmixed(127.0.0.1:10443)を追加してください。tls_inspectorリスナーフィルターを有効にし、TLSチェーン2つをserver_namesで振り分けます。a.envd.testはchain=sni-aを、b.envd.testはchain=sni-bを返します。2つの名前でそれぞれリクエストした結果を、/root/envd-listener/03-sni.txtにa=とb=の2行で書いてください。
  4. openssl s_clientで10443に2回接続し、-servernameだけを変えて、それぞれどの証明書が返るかを/root/envd-listener/04-cert.txtにa=とb=の2行で書いてください(値は証明書のCN)。
  5. mixedリスナーに、transport_protocol: "raw_buffer"に一致するチェーンplainを一番前に追加し、chain=plainを返すようにしてください。同じポート10443に、平文のHTTPリクエストとTLSリクエストをそれぞれ送り、結果を/root/envd-listener/05-raw.txtにplain=とtls=の2行で書いてください。
  6. mixedに4つ目のチェーンalpn-h2を追加してください。transport_protocol: "tls"とapplication_protocols: ["h2"]だけに一致し、chain=alpn-h2をHTTP/2で返します。その後、SNIをa.envd.testにしてHTTP/2でリクエストしてどのチェーンが受けるか、SNIをz.envd.testにしてHTTP/2でリクエストしたらどのチェーンが受けるかを確認して、/root/envd-listener/06-alpn.txtにsni_and_h2=とh2_only=の2行で書いてください。
  7. z.envd.testにHTTP/1.1のTLSリクエストを送ってみてください。どのチェーンにも一致しません。/root/envd-listener/07-nomatch.txtに、nomatch_rc=(そのリクエストのcurlの終了コード)とmatch_rc=(a.envd.testに送った同じリクエストの終了コード)の2行を書いてください。
  8. /root/envd-listener/08-report.mdにchains=(mixedリスナーのチェーンの数)、same_port_plain_and_tls=(yesまたはno)、sni_beats_alpn=(ステップ6でSNIとALPNが同時に一致したときに勝ったチェーンの名前)、nomatch_rc=の4行を書き、その下に学んだことを4行以上書いてください。

参考

1つのプロセスが2つのポートを待ち受ける

/root/envd-listener/listener.yamlにリスナーを2つ置いてください。edge-aは127.0.0.1:10021でlistener=aを、edge-bは127.0.0.1:10022でlistener=bを、direct_responseで返します(管理ポートは9921)。起動したら、/listeners?format=jsonを/root/envd-listener/01-listeners.jsonに保存してください。

リスナーは「アドレスとポートの組」が単位です。プロセス1つが複数を持つことができ、各リスナーはそれぞれ独自のフィルターチェーンのリストを持ちます。direct_responseを使うとアップストリームなしでも応答が返るので、このラボではバックエンドを起動する必要がありません。管理ポートの/listenersはデフォルトがテキストなので、?format=jsonを付けないと構造が出ません。

名前の違う証明書を2枚作る

/root/envd-listener/tls/に自己署名証明書を2枚作ってください。a.crt/a.keyはCNとSANがa.envd.test、b.crt/b.keyはb.envd.testです。2つの証明書のsubjectとSANを/root/envd-listener/02-certs.txtに書いてください。

SNIでチェーンを振り分けるには、名前の違う証明書が最低2枚必要です。openssl req -x509 -newkey rsa:2048 -nodesの1行で、鍵と証明書が一緒にできます。-addext "subjectAltName=DNS:..."を忘れないでください。最近のクライアントはCNを見ず、SANだけを見ます。確認はopenssl x509 -noout -subject -ext subjectAltNameで行います。

同じポートなのに、名前によって別のチェーンが受ける

/root/envd-listener/listener.yamlにリスナーmixed(127.0.0.1:10443)を追加してください。tls_inspectorリスナーフィルターを有効にし、TLSチェーン2つをserver_namesで振り分けます。a.envd.testはchain=sni-aを、b.envd.testはchain=sni-bを返します。2つの名前でそれぞれリクエストした結果を、/root/envd-listener/03-sni.txtにa=とb=の2行で書いてください。

SNIはTLSハンドシェイクの最初のメッセージに平文で入っています。そのため復号の前でも読め、その役割を担うのがtls_inspectorリスナーフィルターです。このフィルターを有効にしないと、server_namesの条件はどの接続にも一致しません。リクエストはcurl -k --resolve a.envd.test:포트:127.0.0.1 https://...(プレースホルダーはポート番号です)で送ります。

チェーンが分かれると、提示する証明書も分かれる

openssl s_clientで10443に2回接続し、-servernameだけを変えて、それぞれどの証明書が返るかを/root/envd-listener/04-cert.txtにa=とb=の2行で書いてください(値は証明書のCN)。

チェーンごとにtransport_socketが別にあるので、どのチェーンが選ばれるかが、そのままどの証明書を提示するかになります。これが、イングレスゲートウェイ1台が数十のドメインの証明書を持てる理由です。echo | openssl s_client -connect 127.0.0.1:포트 -servername 이름 2>/dev/null | openssl x509 -noout -subject(プレースホルダーはポートと名前です)で確認してください。

1つのポートで平文とTLSを一緒に受ける

mixedリスナーに、transport_protocol: "raw_buffer"に一致するチェーンplainを一番前に追加し、chain=plainを返すようにしてください。同じポート10443に、平文のHTTPリクエストとTLSリクエストをそれぞれ送り、結果を/root/envd-listener/05-raw.txtにplain=とtls=の2行で書いてください。

tls_inspectorは最初のバイトをのぞいて、この接続がTLSかどうかを判定し、transport_protocolをtlsまたはraw_bufferに設定します。そのため、同じポートで両方を受けられます。平文のリクエストはcurl http://127.0.0.1:포트/(プレースホルダーはポート番号です)、TLSのリクエストは前のステップと同じ方法です。チェーンの順序はマッチングに影響しません。条件の具体性が決めます。

条件が両方一致したら、どちらが勝つか

mixedに4つ目のチェーンalpn-h2を追加してください。transport_protocol: "tls"とapplication_protocols: ["h2"]だけに一致し、chain=alpn-h2をHTTP/2で返します。その後、SNIをa.envd.testにしてHTTP/2でリクエストしてどのチェーンが受けるか、SNIをz.envd.testにしてHTTP/2でリクエストしたらどのチェーンが受けるかを確認して、/root/envd-listener/06-alpn.txtにsni_and_h2=とh2_only=の2行で書いてください。

フィルターチェーンのマッチングは、8つの段階を決まった順序で絞り込みます。宛先ポート、宛先IP、サーバー名(SNI)、トランスポートプロトコル、アプリケーションプロトコル(ALPN)、その後に送信元の条件が続きます。SNIがALPNより前であることが、このステップの核心です。2つの条件が同時に一致したとき、何が残るかを自分で確かめてください。curl --http2 -k --resolve ...でALPNをh2にします。

どのチェーンにも一致しなければ、応答ではなく沈黙になる

z.envd.testにHTTP/1.1のTLSリクエストを送ってみてください。どのチェーンにも一致しません。/root/envd-listener/07-nomatch.txtに、nomatch_rc=(そのリクエストのcurlの終了コード)とmatch_rc=(a.envd.testに送った同じリクエストの終了コード)の2行を書いてください。

一致するチェーンがないと、Envoyは404を返しません。HTTPレイヤーまで届いていないからです。接続をそのまま閉じます。クライアント側にはTLSハンドシェイクの失敗に見え、curlは0以外の終了コードを返します。本番運用で「証明書を入れたのに接続がただ切れる」として届く報告の大きな割合が、これです。curlの終了コードは$?で受け取ります。

チェーン設計のメモを残す

/root/envd-listener/08-report.mdにchains=(mixedリスナーのチェーンの数)、same_port_plain_and_tls=(yesまたはno)、sni_beats_alpn=(ステップ6でSNIとALPNが同時に一致したときに勝ったチェーンの名前)、nomatch_rc=の4行を書き、その下に学んだことを4行以上書いてください。

このメモは、次にゲートウェイを設計するときに自分が読む文章です。値だけを書かず、「だから何に気をつけるか」を書いてください。チェーンの数は目で数えず、yqでfilter_chainsの長さを取り出すほうが正確です。