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

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

平文が弾かれるのはフィルタチェーンが一つしかないから

TT Labで続きを見る

一言でいうと

PeerAuthenticationは、受け取る側のサイドカーの受信リスナーを変えます。STRICTは、TLSと判定された接続だけを受け付けるフィルターチェーン1つ(クライアント証明書が必須で、メッシュCAで検証)になり、PERMISSIVEは、そこにマッチ条件のない平文のチェーンがもう1つ加わったものです。証明書のSANにあるSPIFFE IDが身元であり、先頭のspiffe://を除いた文字列が、AuthorizationPolicyのprincipalになります。

なぜ必要なのか

Kubernetesのネットワークは、デフォルトでは平文で、Pod IPは身元ではありません。Podが再起動するとIPが変わり、同じIPを別のワークロードが引き継ぎます。そのため、「ordersだけがreviewsを呼び出せる」をIPで書くと、すぐに破綻します。Istioは、身元をサービスアカウントに結び付けました。istiodがPodのサービスアカウントトークンを確認した後、spiffe://cluster.local/ns/default/sa/ordersのようなURIをSANに入れた証明書に署名し、サイドカー同士は、この証明書でお互いを確認するmTLSで通信します。

問題は、メッシュが一度にできあがらないことです。サイドカーは、ネームスペースごと、デプロイごとに、順番に入っていきます。その途中で受け取る側が平文を拒否すると、まだサイドカーのないPodからの呼び出しがすべて切れます。そのため、デフォルトはPERMISSIVEです。mTLSも平文も受け付けます。呼び出す側のサイドカーは、相手にサイドカーがあれば自動でmTLSで送る(auto mTLS)ので、注入が終わってからSTRICTに締めればよいのです。PeerAuthenticationは、その締め付けを書くリソースです。

どう動くのか

istiodは、PeerAuthenticationを、受け取る側のサイドカーのvirtualInbound(15006)リスナーに変換します。核心はリスナーフィルターのtls_inspectorです。接続の最初のバイトを見て、TLSのClientHelloならtransport_protocolをtlsと表示し、Envoyはその表示でフィルターチェーンを選びます。

PeerAuthentication Envoyのinboundリスナー
STRICT transport_protocol: tlsのチェーン1つ。require_client_certificate: trueで、trusted_caはメッシュのルートです
PERMISSIVE(デフォルト) 上のチェーンに、マッチ条件のない平文のチェーンを加えたものです
DISABLE 平文のチェーンだけです
portLevelMtls そのポートをdestination_portで指定するチェーンが、別に1つできます

STRICTでは、平文の接続に合うチェーンがないので、EnvoyはHTTPレスポンスも返さずに閉じます。クライアントからはconnection resetに見え、Envoyにはno_filter_chain_matchの統計だけが残ります。portLevelMtlsの番号は、Serviceのportではなく、コンテナが待ち受けるワークロードポートです。iptablesが元の宛先ポートを保ったまま15006へ振り向けるため、チェーンが見る番号はワークロードポートになります。portLevelMtlsは、selectorのあるポリシーでしか受け付けられません。

身元の確認は2段階です。署名の検証は、「メッシュCAが署名したか」だけを見ます。同じCAから出た証明書なら、誰のものでも通ります。「誰なのか」は、SANが答えます。IstioはSANの照合を主に呼び出す側で行います。クライアントが、サーバー証明書のSPIFFE IDを、サービスが期待するアカウントと照合する、secure namingです。受け取る側で「誰が呼び出せるのか」は、AuthorizationPolicyが担当します。mTLSが終わると、Envoyは相手の証明書のURI SANを接続のprincipalとして記憶し、source.principals: ["cluster.local/ns/default/sa/orders"]は、それと照合されます。表記からスキームを除いただけの、同じ値です。

現場での姿

STRICTに変えたら、一部の呼び出しだけconnection reset by peerになる。呼び出す側にサイドカーがないPodです(CronJob、サイドカーを外してあるバッチ、メッシュ外のモニタリング)。受け取る側のログには、リクエストがそもそも残りません。チェーンを選ぶ段階で切れたからです。no_filter_chain_matchが増えていれば、このケースです。

ヘルスチェックやメトリクスのポートだけ、平文のまま残したい。portLevelMtlsで、そのポートだけDISABLEにします。このとき、Serviceのポート番号を書くと、どのチェーンにも当たらず、黙って無視されます。targetPortを書きます。

AuthorizationPolicyを設定したら、すべて403になる。principalsにspiffe://を付けたか、トラストドメインが違う(cluster.localでないインストール)か、呼び出しが平文で入ってきてprincipalが空になっている場合です。3つとも、istioctl validateは通ります。

公式ドキュメント: PeerAuthentication・Security concepts・AuthorizationPolicy・Envoy TLS

次のラボですること

STRICTのPeerAuthenticationを書いてオフラインで検査した後、opensslでメッシュCAとSPIFFE証明書3枚を作ります。その証明書で、STRICT・SANマッチャー・PERMISSIVE・ポート単位の例外を、Envoyのチェーンとして手で組み立て、平文とmTLSがそれぞれどうなるかを確認し、SANからAuthorizationPolicyのprincipalを導き出します。