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

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

SPIFFE 証明書を作り STRICT・PERMISSIVE のチェーンを組む

TT Labで続きを見る

目標

PeerAuthenticationを書き、opensslでメッシュCAとSPIFFE証明書を作り、各モードに当たるEnvoyのフィルターチェーンを手で組み立てて、平文とmTLSがどう分かれるかを確認した後、証明書のSANからAuthorizationPolicyのprincipalを導き出します。

なぜ重要なのか

STRICTに変えた日に切れる呼び出し、1つのポートだけを平文のまま残そうとして黙って無視されるポリシー、principalを間違えて書いてすべてが拒否される認可ポリシー。3つとも、istioctl validateは通ります。モードがEnvoyのどのチェーンになり、身元が証明書のどこから来るのかがわかれば、統計1行と証明書1枚で原因を突き止められます。

ステップ

  1. /root/ist2-mtls/pa.yamlにPeerAuthenticationを書いてください。apiVersion: security.istio.io/v1、名前はreviews-strict、ネームスペースはdefault、selector.matchLabels.app: reviews、mtls.mode: STRICTです。istioctl validate -f pa.yamlの出力と終了コードを、/root/ist2-mtls/01-validate.txtに入れてください(最後の行はrc=)。
  2. /root/ist2-mtlsでopensslを使い、自己署名CA1枚(ca.crt・ca.key)と、そのCAで署名した証明書3枚を作ってください。reviews.crt/reviews.key(サーバー)、orders.crt/orders.key(許可するクライアント)、intruder.crt/intruder.key(同じCAが署名したものの、許可しないクライアント)です。各証明書のSANはURI1つで、spiffe://cluster.local/ns/default/sa/<이름>です(プレースホルダーはサービスアカウント名です)。その後、証明書から実際に読み取ったSANを、/root/ist2-mtls/02-ids.txtに、reviews.crt=…、orders.crt=…、intruder.crt=…の3行で書いてください。
  3. /root/ist2-mtls/strict.yamlにEnvoyの設定を書いてください。管理ポートは9986、リスナーvirtualInboundが127.0.0.1:10086で待ち受け、リスナーフィルターとしてenvoy.filters.listener.tls_inspectorを置きます。フィルターチェーンは1つで、filter_chain_match.transport_protocol: tlsとし、DownstreamTlsContextに、require_client_certificate: true、サーバー証明書/root/ist2-mtls/reviews.crt・/root/ist2-mtls/reviews.key、信頼するCA/root/ist2-mtls/ca.crtを与えます(SANマッチャーはまだ入れません)。そのチェーンは、すべてのパスをクラスターinbound|8109||(127.0.0.1:8109)へ送ります。アップストリームを8109でokとして起動し、Envoyを起動してから、/root/ist2-mtls/03-strict.txtに4行を書いてください。plain=(平文のhttp://localhost:10086/strictのコード)、mtls=(ordersの証明書を使ったhttps://localhost:10086/strictのコード)、mtls_body=(そのレスポンス本文)、no_filter_chain_match=(統計listener.127.0.0.1_10086.no_filter_chain_matchの値)です。
  4. /root/ist2-mtls/strict.yamlを/root/ist2-mtls/strict-san.yamlにコピーしてから、そのコピーにだけvalidation_contextにmatch_typed_subject_alt_namesを1つ追加してください。san_type: URI、matcher.exactはステップ2で読み取ったorders.crtのSANそのままです。その設定でEnvoyを起動し直し、/root/ist2-mtls/04-san.txtに3行を書いてください。orders=(ordersの証明書を使ったhttps://localhost:10086/sanのコード)、intruder=(intruderの証明書での同じリクエストのコード)、fail_verify_san=(統計listener.127.0.0.1_10086.ssl.fail_verify_sanの値)です。
  5. /root/ist2-mtls/strict.yamlをもとに/root/ist2-mtls/permissive.yamlを作ってください。ステップ3のTLSチェーンはそのままにして、filter_chain_matchもtransport_socketもない平文のチェーンを1つ加えます(同じクラスターinbound|8109||へ)。その設定でEnvoyを起動し直し、/root/ist2-mtls/05-permissive.txtに3行を書いてください。plain=(平文のhttp://localhost:10086/permissiveのコード)、plain_body=(その本文)、mtls=(ordersの証明書を使ったhttps://localhost:10086/permissiveのコード)です。
  6. /root/ist2-mtls/pa-port.yamlにドキュメントを2つ書いてください。(1) Servicereviews(ネームスペースdefault、selectorapp: reviews、port: 80 → targetPort: 10096)、(2) PeerAuthenticationreviews-port(同じselector、mtls.mode: STRICT、portLevelMtlsで1つのポートだけDISABLE)です。そのポート番号を何で書くべきかは、ヒントを見てください。その後、/root/ist2-mtls/portlevel.yamlにEnvoyの設定を書いてください。ステップ3と同じリスナー(10086)に、additional_addressesで127.0.0.1:10096も待ち受けさせ、filter_chain_match.destination_port: 10096の平文のチェーン1つと、ステップ3のTLSチェーン1つを置きます。Envoyを起動し直し、/root/ist2-mtls/06-portlevel.txtに4行を書いてください。validate_rc=(istioctl validate -f pa-port.yamlの終了コード)、plain_10096=、plain_10086=(各ポートへの平文の/portのコード)、mtls_10086=(ordersの証明書を使ったhttps://localhost:10086/portのコード)です。
  7. /root/ist2-mtls/orders.crtのSANをopensslで読み取り、/root/ist2-mtls/07-principal.txtに2行を書いてください。san=(読み取ったURIそのまま)、principal=(AuthorizationPolicyのprincipalsに入れる文字列)です。そして、/root/ist2-mtls/authz.yamlにAuthorizationPolicyを書いてください。apiVersion: security.istio.io/v1、名前はreviews-from-orders、ネームスペースはdefault、selector.matchLabels.app: reviews、action: ALLOW、ルール1つのfrom[0].source.principalsに、そのprincipal1つだけを入れます。istioctl validate -f authz.yamlが通る必要があります。
  8. /root/ist2-mtls/08-report.mdに、default_mode=(PeerAuthenticationが1つもないときのモード)、strict_plain=(ステップ3で平文のリクエストが受け取ったコード)、permissive_chains=(ステップ5のリスナーのフィルターチェーンの数)、orders_principal=(ステップ7のprincipal)の4行を書き、その下に、- で始まる説明を4行以上書いてください。

参考

STRICTのPeerAuthenticationを書いてオフラインで検査する

/root/ist2-mtls/pa.yamlにPeerAuthenticationを書いてください。apiVersion: security.istio.io/v1、名前はreviews-strict、ネームスペースはdefault、selector.matchLabels.app: reviews、mtls.mode: STRICTです。istioctl validate -f pa.yamlの出力と終了コードを、/root/ist2-mtls/01-validate.txtに入れてください(最後の行はrc=)。

PeerAuthenticationは、受け取る側のワークロードの設定です。reviewsを呼び出す側ではなく、reviews自身に「平文は受け取らない」と設定するので、selectorは受け取る側のラベルでなければなりません。selectorを省くとネームスペース全体、ルートネームスペース(istio-system)に置くとメッシュ全体が対象になります。istioctl validateは、クラスターなしでスキーマだけを見ます。モードの値を打ち間違えると、ここで引っかかります。

メッシュCAとSPIFFE証明書3枚を作る

/root/ist2-mtlsでopensslを使い、自己署名CA1枚(ca.crt・ca.key)と、そのCAで署名した証明書3枚を作ってください。reviews.crt/reviews.key(サーバー)、orders.crt/orders.key(許可するクライアント)、intruder.crt/intruder.key(同じCAが署名したものの、許可しないクライアント)です。各証明書のSANはURI1つで、spiffe://cluster.local/ns/default/sa/<이름>です(プレースホルダーはサービスアカウント名です)。その後、証明書から実際に読み取ったSANを、/root/ist2-mtls/02-ids.txtに、reviews.crt=…、orders.crt=…、intruder.crt=…の3行で書いてください。

Istioの身元は、名前(CN)ではなく、SANのURIです。形はspiffe://<신뢰 도메인>/ns/<네임스페이스>/sa/<서비스 계정>です(プレースホルダーはトラストドメイン、ネームスペース、サービスアカウントです)。istiodは、Podのサービスアカウントトークンを確認した後、まさにこの形の証明書に署名してくれます。CSRに-addext "subjectAltName=URI:…"でSANを入れても、署名するときにopenssl x509 -reqに-copy_extensions copyallを渡さないと、拡張が捨てられます。署名の後に、openssl x509 -noout -ext subjectAltNameで必ず確認し、openssl verify -CAfile ca.crtで証明書チェーンも見てください。

STRICTはTLSチェーン1つだけのリスナーになる

/root/ist2-mtls/strict.yamlにEnvoyの設定を書いてください。管理ポートは9986、リスナーvirtualInboundが127.0.0.1:10086で待ち受け、リスナーフィルターとしてenvoy.filters.listener.tls_inspectorを置きます。フィルターチェーンは1つで、filter_chain_match.transport_protocol: tlsとし、DownstreamTlsContextに、require_client_certificate: true、サーバー証明書/root/ist2-mtls/reviews.crt・/root/ist2-mtls/reviews.key、信頼するCA/root/ist2-mtls/ca.crtを与えます(SANマッチャーはまだ入れません)。そのチェーンは、すべてのパスをクラスターinbound|8109||(127.0.0.1:8109)へ送ります。アップストリームを8109でokとして起動し、Envoyを起動してから、/root/ist2-mtls/03-strict.txtに4行を書いてください。plain=(平文のhttp://localhost:10086/strictのコード)、mtls=(ordersの証明書を使ったhttps://localhost:10086/strictのコード)、mtls_body=(そのレスポンス本文)、no_filter_chain_match=(統計listener.127.0.0.1_10086.no_filter_chain_matchの値)です。

istiodは、STRICTを受け取ると、受信リスナーから平文のチェーンをなくします。残るのは、tls_inspectorが「TLSだ」と判定した接続だけを受け付けるチェーンで、そのチェーンはクライアント証明書を要求し、メッシュCAで検証します。平文の接続には合うチェーンがなく、Envoyはそのまま閉じます。HTTPレスポンスさえないので、curlのコードは000で、その痕跡がno_filter_chain_matchの統計です。サーバー証明書のSANはURIだけなので、curlのホスト名の検査は通りません。-kでサーバーの確認だけをオフにし、--cert・--keyでクライアント証明書は提示してください。(Istioのクライアントは、ホスト名の代わりに、サーバーSANのSPIFFE IDを確認します。)

同じCAが署名しても、SANが違えば拒否する

/root/ist2-mtls/strict.yamlを/root/ist2-mtls/strict-san.yamlにコピーしてから、そのコピーにだけvalidation_contextにmatch_typed_subject_alt_namesを1つ追加してください。san_type: URI、matcher.exactはステップ2で読み取ったorders.crtのSANそのままです。その設定でEnvoyを起動し直し、/root/ist2-mtls/04-san.txtに3行を書いてください。orders=(ordersの証明書を使ったhttps://localhost:10086/sanのコード)、intruder=(intruderの証明書での同じリクエストのコード)、fail_verify_san=(統計listener.127.0.0.1_10086.ssl.fail_verify_sanの値)です。

ステップ3のチェーンは、「メッシュCAが署名したか」だけを見ます。そのため、同じCAから出たintruderも通りました。SANマッチャーを設定すると、署名に加えて「誰の証明書なのか」まで見ます。ただし、Istioがこのマッチャーを使うのは、主にクライアント側です。呼び出す側が「相手が本当にreviewsの身元なのか」を確認するもの(secure naming)で、受け取る側で「誰が呼び出せるのか」は、ステップ7のAuthorizationPolicyが担当します。ここでは、同じ原理を受け取る側に設定して、EnvoyがSANをどう照合するかを見ます。拒否はTLSハンドシェイクで起こるので、コードは000です。

PERMISSIVEはチェーン2つになる

/root/ist2-mtls/strict.yamlをもとに/root/ist2-mtls/permissive.yamlを作ってください。ステップ3のTLSチェーンはそのままにして、filter_chain_matchもtransport_socketもない平文のチェーンを1つ加えます(同じクラスターinbound|8109||へ)。その設定でEnvoyを起動し直し、/root/ist2-mtls/05-permissive.txtに3行を書いてください。plain=(平文のhttp://localhost:10086/permissiveのコード)、plain_body=(その本文)、mtls=(ordersの証明書を使ったhttps://localhost:10086/permissiveのコード)です。

PERMISSIVEは「両方受け付ける」です。Envoyでは、チェーンを1つ加えることで表現されます。tls_inspectorが最初のバイトを見て、TLSならtransport_protocol: tlsのチェーンへ、そうでなければマッチ条件のないチェーンへ送ります。Istioが実際に作るチェーンには、ALPN(istio-peer-exchange、istio)の条件も付きますが、原理は同じです。平文のチェーンにtransport_socketを付けると、そのチェーンもTLSを待つようになり、平文が再び拒否されます。

portLevelMtlsはワークロードポートに設定するチェーン1つになる

/root/ist2-mtls/pa-port.yamlにドキュメントを2つ書いてください。(1) Servicereviews(ネームスペースdefault、selectorapp: reviews、port: 80 → targetPort: 10096)、(2) PeerAuthenticationreviews-port(同じselector、mtls.mode: STRICT、portLevelMtlsで1つのポートだけDISABLE)です。そのポート番号を何で書くべきかは、ヒントを見てください。その後、/root/ist2-mtls/portlevel.yamlにEnvoyの設定を書いてください。ステップ3と同じリスナー(10086)に、additional_addressesで127.0.0.1:10096も待ち受けさせ、filter_chain_match.destination_port: 10096の平文のチェーン1つと、ステップ3のTLSチェーン1つを置きます。Envoyを起動し直し、/root/ist2-mtls/06-portlevel.txtに4行を書いてください。validate_rc=(istioctl validate -f pa-port.yamlの終了コード)、plain_10096=、plain_10086=(各ポートへの平文の/portのコード)、mtls_10086=(ordersの証明書を使ったhttps://localhost:10086/portのコード)です。

iptablesは、Podへ来る接続を15006へ曲げるとき、元の宛先ポートを保ちます。そのポートは、Serviceのportではなく、コンテナが実際に待ち受けるtargetPortで、istiodは、portLevelMtlsの番号をそのままdestination_portで指定するチェーンにします。そのため、ドキュメントも「ワークロードポート」と書いています。また、portLevelMtlsは、selectorのあるポリシーでしか受け付けられません。省いてみると、istioctl validateが拒否します。このPodでは、iptablesの代わりに、1つのリスナーに2つのポートを待ち受けさせて、同じ形を作ります。

SANからspiffe://を除くとprincipalになる

/root/ist2-mtls/orders.crtのSANをopensslで読み取り、/root/ist2-mtls/07-principal.txtに2行を書いてください。san=(読み取ったURIそのまま)、principal=(AuthorizationPolicyのprincipalsに入れる文字列)です。そして、/root/ist2-mtls/authz.yamlにAuthorizationPolicyを書いてください。apiVersion: security.istio.io/v1、名前はreviews-from-orders、ネームスペースはdefault、selector.matchLabels.app: reviews、action: ALLOW、ルール1つのfrom[0].source.principalsに、そのprincipal1つだけを入れます。istioctl validate -f authz.yamlが通る必要があります。

mTLSが終わると、受け取る側のEnvoyは、相手の証明書のURI SANを接続の身元として記憶します。AuthorizationPolicyのsource.principalsは、その身元と照合されますが、表記からスキーム(spiffe://)を除いた<신뢰 도메인>/ns/<네임스페이스>/sa/<서비스 계정>の形を使います(プレースホルダーはトラストドメイン、ネームスペース、サービスアカウントです)。シェルでは${san#spiffe://}で除けます。注意してください。スキームを付けたまま書いても、istioctl validateは通ります。そのポリシーは誰ともマッチせず、ALLOWポリシーなら全員を拒否します。また、principalはmTLSで入ってきた接続にだけ生まれるので、平文を許可するPERMISSIVEのポートでは、このルールに当たるリクエストがありません。

モードごとにEnvoyに生じるものを1枚にまとめる

/root/ist2-mtls/08-report.mdに、default_mode=(PeerAuthenticationが1つもないときのモード)、strict_plain=(ステップ3で平文のリクエストが受け取ったコード)、permissive_chains=(ステップ5のリスナーのフィルターチェーンの数)、orders_principal=(ステップ7のprincipal)の4行を書き、その下に、- で始まる説明を4行以上書いてください。

値は、前のステップのファイルから移してください。説明には、「モード → Envoyに生じるもの → 運用で見える症状」を組にして書くとよいでしょう。デフォルトのモードがなぜその値なのかは、メッシュにサイドカーを順番に入れていく途中を思い浮かべてみてください。