Using Network Policy with Istio


やりたいこと

  • Istio上のワークロードたちのNamespace横断の通信をNetwork Policyで遮断したい

Using Network Policy with Istio ※DeepL

Kubernetes上で動作するアプリケーションを保護するためにNetwork Policyを使用することは、現在では業界のベストプラクティスとして広く受け入れられています。Istioもポリシーをサポートしていることから、Istio PolicyとKubernetes Network Policyがどのように相互作用し、アプリケーションを安全に提供するために互いにサポートし合っているかを説明することに時間を費やしたいと思います。

基本的なことから始めましょう:
なぜIstioとKubernetes Network Policyの両方を使いたいと思うのでしょうか?簡単な答えは、両者が異なることに長けているからです。IstioとNetwork Policyの主な違いを考えてみましょう(ここではCalicoなどの「典型的な」実装を説明しますが、実装の詳細はネットワークプロバイダーによって異なる可能性があります)。

Istio Policy Network Policy
Layer “Service” — L7 “Network” — L3-4
Implementation User space Kernel
Enforcement Point Pod Node

Layer

Istio Policyは、ネットワーク・アプリケーションの「サービス」層で動作します。これは OSI モデルから見るとレイヤー 7(アプリケーション)ですが、クラウドネイティブアプリケーションのデファクトモデルでは、レイヤー 7 は実際には少なくともサービス層とコンテンツ層の 2 つの層で構成されているとされています。サービス層は通常HTTPであり、実際のアプリケーションデータ(コンテンツ層)をカプセル化する。IstioのEnvoyプロキシが動作するのは、このHTTPのサービス層である。これに対して、Network Policyは、OSIモデルの第3層(Network)と第4層(Transport)で動作する。

サービス層で動作することで、Envoy プロキシは、現在 HTTP/1.1 と HTTP/2 (gRPC は HTTP/2 で動作) を含む理解できるプロトコルに対して、ポリシー決定の根拠となる豊富な属性セットを得ることができます。そのため、バーチャルホストやURL、その他のHTTPヘッダに基づいてポリシーを適用することができます。将来的には、Istioはレイヤー7のプロトコルを幅広くサポートし、汎用的なTCPやUDPのトランスポートにも対応する予定です。

一方、ネットワーク層での運用は、すべてのネットワークアプリケーションがIPを使用するため、汎用性が高いという利点があります。ネットワーク層では、レイヤー7のプロトコルに関係なく、ポリシーを適用することができます。DNS、SQLデータベース、リアルタイムストリーミング、HTTPを使用しないその他多くのサービスを保護することができます。ネットワーク・ポリシーは、古典的なファイアウォールのIPアドレス、プロト、ポートのタプルに限定されるものではありません。IstioとNetwork Policyの両方は、ポッドエンドポイントを記述するためのリッチなKubernetesラベルを意識しています。

Implementation

IstioのプロキシはEnvoyをベースにしており、データプレーンにユーザースペースデーモンとして実装され、標準ソケットを使ってネットワーク層とやり取りをします。このため、処理に大きな柔軟性があり、コンテナで配布(とアップグレード!)することができます。

ネットワークポリシーデータプレーンは通常、カーネル空間に実装されます(例えば、iptables、eBPF フィルタ、あるいはカスタムカーネルモジュールを使用します)。カーネル空間にあることで、非常に高速になりますが、Envoy プロキシのような柔軟性はありません。

Enforcement point

Envoyプロキシを使ったポリシー実行は、ポッド内部で、同じネットワークのネームスペースにあるサイドカーコンテナとして実装されています。これにより、シンプルなデプロイメントモデルが実現する。一部のコンテナには、そのポッド内のネットワークを再構成する権限(CAP_NET_ADMIN)が与えられている。このようなサービスインスタンスが侵害されたり、(悪意のあるテナントのように)誤動作したりすると、プロキシがバイパスされる可能性がある。

攻撃者が他のIstio対応ポッドにアクセスすることはできませんが、それらが正しく構成されている限り、いくつかの攻撃ベクトルを開くことになります。

  • 保護されていないポッドへの攻撃
  • 大量のトラフィックを送信し、保護されたポッドへのサービスを拒否しようとする。
  • ポッドで収集したデータの流出
  • クラスタインフラ(サーバやKubernetesサービス)への攻撃
  • データベース、ストレージアレイ、レガシーシステムなど、メッシュの外側のサービスを攻撃する。

ネットワーク・ポリシーは、通常、ゲスト・ポッドのネットワーク・ネームスペースの外側にあるホスト・ノードで実施されます。これは、侵害されたポッドや不正な動作をするポッドが、強制を回避するためにルート名前空間に侵入しなければならないことを意味します。Kubernetes 1.8でegressポリシーが追加される予定ですが、この違いにより、ネットワークポリシーは、侵害されたワークロードからインフラを保護するための重要な部分となります。

Examples

Istio対応アプリケーションのKubernetes Network Policyで何をしたいのか、いくつかの例を挙げて説明します。Bookinfoのサンプルアプリケーションを考えてみましょう。ここでは、Network Policyの次のような使用例を取り上げます。

  • アプリケーションのイングレスの攻撃面を減らす
  • アプリケーション内のきめ細かな分離を強制する

Reduce attack surface of the application ingress

アプリケーションのイングレスコントローラーは、外界から我々のアプリケーションへの主なエントリポイントです。istio.yamlをちょっと覗いてみると、Istioのingressはこのように定義されています。

apiVersion: v1
kind: Service
metadata:
  name: istio-ingress
  labels:
    istio: ingress
spec:
  type: LoadBalancer
  ports:
  - port: 80
    name: http
  - port: 443
    name: https
  selector:
    istio: ingress

istio-ingressは80番と443番ポートを公開しています。受信トラフィックをこの2つのポートだけに制限してみましょう。Envoyには管理用インターフェイスが組み込まれており、誤った設定のistio-ingressイメージで誤って管理用インターフェイスを外部に公開してしまわないようにしたい。これは深層防護の例です。適切に設定されたイメージはインタフェースを公開しないはずで、適切に設定されたネットワークポリシーは、誰かがそれに接続するのを防ぎます。どちらかが失敗したり、間違って設定されたりしても、私たちはまだ保護されています。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: istio-ingress-lockdown
  namespace: default
spec:
  podSelector:
    matchLabels:
      istio: ingress
  ingress:
  - ports:
    - protocol: TCP
      port: 80
    - protocol: TCP
      port: 443

Enforce fine-grained isolation within the application

以下は、Bookinfoアプリケーションのサービスグラフです。

このグラフは、正しく機能するアプリケーションが行うことを許可されるべきすべての接続を示しています。それ以外の接続、例えばIstio Ingressから直接Ratingサービスに接続するような接続は、アプリケーションの一部ではありません。これらの余計な接続をロックアウトして、攻撃者に利用されないようにしましょう。例えば、Ingressポッドが攻撃者に任意のコードを実行させるエクスプロイトによって侵害されたと想像してください。ネットワーク・ポリシーを使用してProduct Pageポッドへの接続のみを許可すると、攻撃者はサービス・メッシュのメンバーを侵害しても、私のアプリケーション・バックエンドへのアクセス権を得ることはできなくなります。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: product-page-ingress
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: productpage
  ingress:
  - ports:
    - protocol: TCP
      port: 9080
    from:
    - podSelector:
        matchLabels:
          istio: ingress

各サービスに対して同様のポリシーを記述し、他のポッドがそれぞれへのアクセスを許可されるように強制することができますし、そうする必要があります。

Summary

私たちは、IstioとNetwork Policyでは、ポリシーを適用する上での強みが異なると考えています。Istioはアプリケーションプロトコルを認識し、非常に柔軟であるため、サービスルーティング、リトライ、サーキットブレーキングなどの運用目標や、トークン検証などのアプリケーション層で動作するセキュリティに対応したポリシー適用に最適である。ネットワークポリシーは、普遍的で非常に効率的であり、ポッドから分離されているため、ネットワークセキュリティの目標をサポートするポリシーの適用に理想的です。さらに、ネットワークスタックの異なるレイヤーで動作するポリシーを持つことは、状態を混同することなく各レイヤーに特定のコンテキストを与え、責任の分離を可能にするため、本当に良いことです。

つまるとこどういうことか

  • 多層防御の観点からも、Authorization Policyだけじゃなくて、Network Policyはやっておいた方がいい。
    • うんうん、
  • きめ細かくはできないけど、Network Policyのほうが高速でもある。
    • うんうん、

Service Mesh上のNamespaceがdefault1とdefault2の間の通信を確認する

仕込み

$ default1_src=`kubectl -n default1 get pod | grep ratings | awk '{print $1}'`
$ default2_src=`kubectl -n default2 get pod | grep ratings | awk '{print $1}'`
$ default1_dst=`kubectl -n default1 get svc | grep prod | awk '{print $3":"$5}' | sed 's/\/.*//g'`
$ default2_dst=`kubectl -n default2 get svc | grep prod | awk '{print $3":"$5}' | sed 's/\/.*//g'`
$ echo $default1_src
ratings-v1-b6994bb9-s5s4l
$ echo $default2_src
ratings-v1-b6994bb9-j5hlv
$ echo $default1_dst
10.107.136.55:9080
$ echo $default2_dst
10.110.26.100:9080

テスト

$ kubectl -n default1 exec $default1_src -- curl -s -o /dev/null -w %{http_code} http://$default1_dst
200
$ kubectl -n default1 exec $default1_src -- curl -s -o /dev/null -w %{http_code} http://$default2_dst
200
$ kubectl -n default2 exec $default2_src -- curl -s -o /dev/null -w %{http_code} http://$default1_dst
200
$ kubectl -n default2 exec $default2_src -- curl -s -o /dev/null -w %{http_code} http://$default2_dst
200

結果

宛先がdefault1のproductpage 宛先がdefault2のproductpage
送信元がdefault1のratings
送信元がdefault2のratings

Network Policyを効かせて、Service Mesh上のNamespaceがdefault1とdefault2の間の通信を遮断する

仕込み1

まず、全遮断。

$ cat default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
$ kubectl -n default2 apply -f default-deny.yaml

テスト1

$ kubectl -n default1 exec $default1_src -- curl -s -o /dev/null -w %{http_code} http://$default1_dst
200
$ kubectl -n default1 exec $default1_src -- curl -s -o /dev/null -w %{http_code} http://$default2_dst
200
$ kubectl -n default2 exec $default2_src -- curl -s -o /dev/null -w %{http_code} http://$default1_dst
200
$ kubectl -n default2 exec $default2_src -- curl -s -o /dev/null -w %{http_code} http://$default2_dst
200

結果1

んー、なるほど?SVC目掛けてだとそもそも効かない?

宛先がdefault1のproductpage 宛先がdefault2のproductpage
送信元がdefault1のratings
送信元がdefault2のratings

仕込み2

NamespaceSelectorだとどうなるか?

$ cat namespaceSelector.yaml 
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: nsselector
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          project: hogehoge
    ports:
    - protocol: TCP
      port: 7777
$ kubectl -n default2 apply -f namespaceSelector.yaml 
networkpolicy.networking.k8s.io/nsselector created

テスト2

$ kubectl -n default1 exec $default1_src -- curl -s -o /dev/null -w %{http_code} http://$default1_dst
200
$ kubectl -n default1 exec $default1_src -- curl -s -o /dev/null -w %{http_code} http://$default2_dst
200
$ kubectl -n default2 exec $default2_src -- curl -s -o /dev/null -w %{http_code} http://$default1_dst
200
$ kubectl -n default2 exec $default2_src -- curl -s -o /dev/null -w %{http_code} http://$default2_dst
200

結果2

んー、なるほど?やっぱSVC目掛けてだと効かないか。Authorization Policyなら効くだろうけど。
上のドキュメントはIngress、Egressに言及しているから、ns横断の通信とはスコープが違うのかも。もうちょっと勉強しよ。

宛先がdefault1のproductpage 宛先がdefault2のproductpage
送信元がdefault1のratings
送信元がdefault2のratings

ToDo

  • Authorization Policyでやる(多分できる)
  • Authorization Policyでやったとき、EnvoyがInjectionされていないワークロードからの通信はブロックできるの?(ここがわからん)