証明書二枚で一つのポートを分ける
目標
1つのリスナーに複数のフィルターチェーンを置き、SNI・トランスポートプロトコル・ALPNで振り分けて受けます。条件が重なったときにどちらが勝つかも、自分で確認します。
なぜ重要なのか
イングレスゲートウェイ1台が数十のドメインを受ける構成では、「どのチェーンがこの接続を受けるか」がそのまま「どの証明書を提示するか」になります。この規則を順序と誤解すると、チェーンを上下に動かしながら1日を費やすことになります。実際の判定は8段階の固定された順序で具体性を比較して行われ、一致するチェーンがないとエラー応答ではなく接続がただ切れます。この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に保存してください。/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に書いてください。/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行で書いてください。openssl s_clientで10443に2回接続し、-servernameだけを変えて、それぞれどの証明書が返るかを/root/envd-listener/04-cert.txtにa=とb=の2行で書いてください(値は証明書のCN)。mixedリスナーに、transport_protocol: "raw_buffer"に一致するチェーンplainを一番前に追加し、chain=plainを返すようにしてください。同じポート10443に、平文のHTTPリクエストとTLSリクエストをそれぞれ送り、結果を/root/envd-listener/05-raw.txtにplain=とtls=の2行で書いてください。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行で書いてください。z.envd.testにHTTP/1.1のTLSリクエストを送ってみてください。どのチェーンにも一致しません。/root/envd-listener/07-nomatch.txtに、nomatch_rc=(そのリクエストのcurlの終了コード)とmatch_rc=(a.envd.testに送った同じリクエストの終了コード)の2行を書いてください。/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行以上書いてください。
参考
- Envoyを起動するときは
setsid --fork nohup envoy -c <파일> --log-level warn > <로그> 2>&1 </dev/null(プレースホルダーは設定ファイルとログファイルです)を使い、再起動する前にはpkill -x envoyで片付けてください。 - 起動を待つときは、固定の
sleepの代わりに、/readyがLIVEを返すまで回るループを使ってください。 - 自己署名証明書なので、
curlには-kが必要です。名前を付けて送るには、--resolve 이름:포트:127.0.0.1(プレースホルダーは名前とポートです)を一緒に使います。 - よくある間違い:
tls_inspectorを有効にしないと、server_namesとapplication_protocolsの条件がどの接続にも一致しません。設定は通るのに、動作しないだけです。 - よくある間違い:
-addext "subjectAltName=DNS:..."を忘れると、CNだけの証明書ができます。
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の長さを取り出すほうが正確です。