NetworkPolicyだけでLoadBalancerを制御することが難しい理由

# はじめに

今回、KubernetesでNetworkPolicyを利用する主な目的はnamespace間の通信を抑制することです。

とはいえ、手元の環境ではグローバルIPからのリクエストはIngressを経由しなくてはいけないので、NetworkPolicyでは自分のnamespaceとingress-nginx namespaceからの通信は許可する設定にしています。

この時にLoadBalancerとの関係について調べたので、その顛末をメモしておきます。

# 参考資料

* [StackOverflow::Kubernetes NetworkPolicy allow loadbalancer](https://stackoverflow.com/questions/47327554/kubernetes-networkpolicy-allow-loadbalancer)
* [How to Configure NetworkPolicy for NodePort in Kubernetes](https://www.sobyte.net/post/2022-03/k8s-nodeport-networkpolicy/)

# 前提

1. kubesprayで導入したオンプレミスのk8sクラスター(v1.25.6)が対象
1. 複数ユーザーが利用する環境である
1. ユーザー毎に一意のnamespaceがあらかじめ作成されている (namespace名は認証時のユーザー名と同一)
1. NetworkPolicyのデフォルト設定では、Ingress(INPUT)は拒否し、Egress(OUTPUT)は許可する設定となっている
1. 自身のnamespaceにある全てのPodからの通信(Ingress)は許可する
1. namespace: ingress-nginxにある全てのPodからの通信(Ingress)は許可する
1. adminユーザーのみがNetworkPolicyを定義し、ユーザーのNetworkPolicyリソースに対する"create", "delete"権限は剥奪している
1. あらかじめユーザーにNetworkPolicyを定義することとして、後からNetworkPolicyの生成・適用は行わない
1. LoadBalancerはk8sの各ノードも接続している192.168.1.0/24ネットワークのIPアドレスを割り当てる

LoadBalancerにはmetallbを利用していて、Serviceオブジェクトにtype: loadBalancerを指定するだけで、あらかじめ指定したrangeからLAN上のIPv4アドレスを払出してくれています。

# 期待する振舞い

1. ユーザーがServiceオブジェクトに対しtype: loadBalancerを指定した場合、全てのPodとローカルネットワークからの接続を許可する
2. LoadBalancer経由でアクセスできないPodには外部からの通信を拒否したい (デフォルト設定の維持)

## 問題

実際にはloadBalancer経由でだけ外部からの通信を許可するといった構成はできません。

あらかじめユーザーが公開したいPodに特定のラベルをつけることで、そのPodへの通信が全て許可される方法を応用する必要があります。

1. Serviceオブジェクトにtype: loadBalancerを指定する
2. ServiceオブジェクトのselectorにマッチするPodに公開を許可する特別なlabelを付与する

このためloadBalancerを経由しない &lt;namespace&gt;.svc.local ドメインを使用することで、他のnamespaceとの通信も可能となります。

## 理想的な解決手段

type: LoadBalancerを追加したタイミングで、そのServiceオブジェクトのselectorに対応するPodに対するIngressの通信を許可する設定を加えたい。

このような挙動はNetworkPolicyオブジェクトの設定だけでは実現不可能です。

# テスト環境の構築

まず本来やりたい自分のnamespaceとingressだけの接続を許可したいというところは、次のような設定で制御できています。

```yaml:デフォルトのIngress・Egressルール(Ingressは不許可、Egressは許可)
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-networkpolicy
  namespace: yasu
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  egress:
  - {}
```

```yaml:自分のnamespace(yasu)のみを許可するNetworkPolicy
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-within-same-namespace
  namespace: yasu
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
    - from:
      - namespaceSelector:
          matchLabels:
            name: yasu
```

Ingressも利用しているので、次のようにingress-nginx namespaceからの通信は全て許可しています。

```yaml:Ingress-nginxからの接続を許可するNetworkPolicy
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-ingress
  namespace: yasu
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
    - from:
      - namespaceSelector:
          matchLabels:
            name: ingress-nginx
```

ここまでで、namespace 間の分離と求める可用性の実現には成功しています。


## 課題

kubesprayから導入したMetallbを利用しているので、L2モードで動作し、type: loadBalancerを設定するだけで、あらかじめ指定したrange内のIPアドレスをServiceオブジェクトに設定してくれます。

しかしLANからもPod内部からもこのIP(192.168.1.162/32)への接続はできませんが、Podが稼動しているk8sノード(localhost)上からは接続が可能という状況になります。

このLoadBalancerへの通信を許可する通常の手順は、NetworkPolicyのpodSelectorで、このPodを指定するだけです。

```yaml:LoadBalancerを設定したPod(app:my-nginx)への接続を許可するNetworkPolicy
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-my-nginx
  namespace: yasu
spec:
  podSelector:
    matchLabels:
      app: my-nginx
  policyTypes:
  - Ingress
  ingress:
    - {}
```

この設定自体はLoadBalancerの設定の有無とは関係なく、全ての通信元から経路に依らずmy-nginx Podへの通信を許可する設定になっています。

この設定を管理者が制御するためには、ユーザーがどのようなlabelを設定したPodへの接続を許可したいのか、事後に決定される項目をあらかじめ把握する必要があるため、不可能ではありませんが、あらかじめ設定しておきたいという要件に抵触します。

今回は ``app: my-nginx`` というアプリ名のようなlabelを想定しましたが、例えば ``role: shared`` のようなlabelを想定すれば、利用者が他のnamespaceからのアクセスを許可する場合に ``role: shared`` を付与すれば、loadBalancerを経由するかどうかに依らず第三者のアクセスを許可できます。

いずれにしてもServiceオブジェクトのtype: loadBalancerを設定した接続についてだけ、外部からの接続を許可したいところですが、そのような制御は基本的な機能だけではできません。

## ipBlockの挙動

ipBlockを使うと、CIDR形式(x.x.x.x/netmask)での **許可リスト** を作成することができます。

LoadBalancerのネットワーク制限について検索すると、ipBlockが紹介されている事例が確認できます。

「ブロック(障害物)」という名称なので、最初は拒否するネットワーク範囲を指定するのかと思ったのですが、あくまでも許可するIPアドレスを「ブロック(塊)」で指定するものです。

これを使ってもCalicoはipipモードで動いているので、Podからは元々の接続元IPを把握することはできません。あくまでもMasqueradeされたIPアドレスだけを知ることができるので、IPアドレスベースでの制御への利用は難しいです。

## LoadBalancerで割り当てられたIPアドレスのルーティング

arp -aでみると、loadBalancerで割り当てられたIPアドレスのMACアドレスは、Podが稼動するノードのNICに割り当てられています。trafficがkernelに渡された時点でCalicoに制御が移り、tunl0@NONE を経由してPodとの通信が確立するようです。

```bash:Podが稼動するノード上のtunl0のMASQUERADE設定
$ sudo iptables-save |grep tunl0
-A cali-POSTROUTING -o tunl0 -m comment --comment "cali:SXWvdsbh4Mw7wOln" -m addrtype ! --src-type LOCAL --limit-iface-out -m addrtype --src-type LOCAL -j MASQUERADE --random-fully
```

tunl0にMASQUERADE設定がされていて、--random-fullyオプションが指定されているので、Pod側から接続元を把握することはできないでしょう。この前後で通信に介入できれば良いのですが、NetworkPolicyが適用される前段と思われますので、別の方法を検討しなければ難しいだろうと思われます。

ここまできて、再度検索してみると似たような記事を見つけました。

* [How to Configure NetworkPolicy for NodePort in Kubernetes](https://www.sobyte.net/post/2022-03/k8s-nodeport-networkpolicy/)

tunl0からの接続を許可するのは現実的とはいえなさそうです。

事後で選択的に接続を許可すれば目的は達成できますが、あらかじめnamespaceをまたぐ通信は不許可、LoadBalancerを指定してもらえればできますよ、という構成は諦めることにしました。

## もう少し詳しく

ターゲットのPodが動作しているノードのtunl0をtcpdumpでモニターしてみます。

```bash:
$ sudo tcpdump -i tunl0 -n port 80
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tunl0, link-type RAW (Raw IP), snapshot length 262144 bytes
12:12:15.534237 IP 10.233.115.0.62322 > 10.233.113.139.80: Flags [S], seq 2902369325, win 64240, options [mss 1460,sackOK,TS val 91263191 ecr 0,nop,wscale 7], length 0
12:12:15.534369 IP 10.233.113.139.80 > 10.233.115.0.62322: Flags [S.], seq 2207202264, ack 2902369326, win 64260, options [mss 1440,sackOK,TS val 2157588932 ecr 91263191,nop,wscale 7], length
 0
12:12:15.534990 IP 10.233.115.0.62322 > 10.233.113.139.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 91263191 ecr 2157588932], length 0
12:12:15.534990 IP 10.233.115.0.62322 > 10.233.113.139.80: Flags [P.], seq 1:80, ack 1, win 502, options [nop,nop,TS val 91263192 ecr 2157588932], length 79: HTTP: GET / HTTP/1.1
12:12:15.535067 IP 10.233.113.139.80 > 10.233.115.0.62322: Flags [.], ack 80, win 502, options [nop,nop,TS val 2157588933 ecr 91263192], length 0
12:12:15.535219 IP 10.233.113.139.80 > 10.233.115.0.62322: Flags [P.], seq 1:237, ack 80, win 502, options [nop,nop,TS val 2157588933 ecr 91263192], length 236: HTTP: HTTP/1.1 200 OK
12:12:15.535280 IP 10.233.113.139.80 > 10.233.115.0.62322: Flags [P.], seq 237:294, ack 80, win 502, options [nop,nop,TS val 2157588933 ecr 91263192], length 57: HTTP
12:12:15.535573 IP 10.233.115.0.62322 > 10.233.113.139.80: Flags [.], ack 237, win 501, options [nop,nop,TS val 91263192 ecr 2157588933], length 0
12:12:15.535573 IP 10.233.115.0.62322 > 10.233.113.139.80: Flags [.], ack 294, win 501, options [nop,nop,TS val 91263192 ecr 2157588933], length 0
12:12:15.535733 IP 10.233.115.0.62322 > 10.233.113.139.80: Flags [F.], seq 80, ack 294, win 501, options [nop,nop,TS val 91263192 ecr 2157588933], length 0
12:12:15.535819 IP 10.233.113.139.80 > 10.233.115.0.62322: Flags [F.], seq 294, ack 81, win 502, options [nop,nop,TS val 2157588934 ecr 91263192], length 0
12:12:15.536293 IP 10.233.115.0.62322 > 10.233.113.139.80: Flags [.], ack 295, win 501, options [nop,nop,TS val 91263193 ecr 2157588934], length 0
```

10.233.113.139はターゲットとなるPodのClusterIPです。

10.233.115.0が通信元ですが、別ノードのtunl0に割り当てられているアドレスです。

ingress-nginx経由でアクセスすると、10.233.115.0の部分は次のように変化します。これは ingress-nginx → 自前reverse proxy → ターゲットpod (nginx) という接続を示しています。

```bash:
$ sudo tcpdump -i tunl0 -n port 80
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tunl0, link-type RAW (Raw IP), snapshot length 262144 bytes
12:16:35.482046 IP 10.233.113.122.42502 > 10.233.115.242.80: Flags [S], seq 4085234817, win 64800, options [mss 1440,sackOK,TS val 1207484718 ecr 0,nop,wscale 7], length 0
12:16:35.482213 IP 10.233.115.242.80 > 10.233.113.122.42502: Flags [S.], seq 1536221329, ack 4085234818, win 64260, options [mss 1440,sackOK,TS val 1157776404 ecr 1207484718,nop,wscale 7], length 0
12:16:35.482309 IP 10.233.113.122.42502 > 10.233.115.242.80: Flags [.], ack 1, win 507, options [nop,nop,TS val 1207484718 ecr 1157776404], length 0
12:16:35.482353 IP 10.233.113.122.42502 > 10.233.115.242.80: Flags [P.], seq 1:1377, ack 1, win 507, options [nop,nop,TS val 1207484718 ecr 1157776404], length 1376: HTTP: GET / HTTP/1.1
12:16:35.482443 IP 10.233.115.242.80 > 10.233.113.122.42502: Flags [.], ack 1377, win 501, options [nop,nop,TS val 1157776405 ecr 1207484718], length 0
12:16:35.489886 IP 10.233.115.242.48498 > 10.233.113.139.80: Flags [S], seq 3304093625, win 64800, options [mss 1440,sackOK,TS val 826392586 ecr 0,nop,wscale 7], length 0
12:16:35.489962 IP 10.233.113.139.80 > 10.233.115.242.48498: Flags [S.], seq 441513319, ack 3304093626, win 64260, options [mss 1440,sackOK,TS val 3703613729 ecr 826392586,nop,wscale 7], length 0
12:16:35.490110 IP 10.233.115.242.48498 > 10.233.113.139.80: Flags [.], ack 1, win 507, options [nop,nop,TS val 826392586 ecr 3703613729], length 0
12:16:35.490143 IP 10.233.115.242.48498 > 10.233.113.139.80: Flags [P.], seq 1:1394, ack 1, win 507, options [nop,nop,TS val 826392586 ecr 3703613729], length 1393: HTTP: GET / HTTP/1.0
12:16:35.490165 IP 10.233.113.139.80 > 10.233.115.242.48498: Flags [.], ack 1394, win 501, options [nop,nop,TS val 3703613729 ecr 826392586], length 0
12:16:35.490277 IP 10.233.113.139.80 > 10.233.115.242.48498: Flags [P.], seq 1:175, ack 1394, win 501, options [nop,nop,TS val 3703613729 ecr 826392586], length 174: HTTP: HTTP/1.1 304 Not Modified
12:16:35.490328 IP 10.233.113.139.80 > 10.233.115.242.48498: Flags [F.], seq 175, ack 1394, win 501, options [nop,nop,TS val 3703613729 ecr 826392586], length 0
12:16:35.490409 IP 10.233.115.242.48498 > 10.233.113.139.80: Flags [.], ack 175, win 506, options [nop,nop,TS val 826392587 ecr 3703613729], length 0
12:16:35.490978 IP 10.233.115.242.48498 > 10.233.113.139.80: Flags [F.], seq 1394, ack 176, win 506, options [nop,nop,TS val 826392587 ecr 3703613729], length 0
12:16:35.491018 IP 10.233.113.139.80 > 10.233.115.242.48498: Flags [.], ack 1395, win 501, options [nop,nop,TS val 3703613730 ecr 826392587], length 0
12:16:35.491027 IP 10.233.115.242.80 > 10.233.113.122.42502: Flags [P.], seq 1:180, ack 1377, win 501, options [nop,nop,TS val 1157776413 ecr 1207484718], length 179: HTTP: HTTP/1.1 304 Not Modified
12:16:35.491061 IP 10.233.113.122.42502 > 10.233.115.242.80: Flags [.], ack 180, win 506, options [nop,nop,TS val 1207484727 ecr 1157776413], length 0
```

10.233.113.122は、namespace:ingress-nginxのingress-nginx-controllerです。

10.233.115.242は、nginxをproxyにしている自前のpodを示しています。

この結果からtunl0のmasqueradeされている10.233.115.0などのアドレスをNetworkPolicyのipBlockに指定すれば通信自体は許可できると思いますが、将来的に変化する可能性もありますし、ノードの増減によって設定を変化させないと問題が発生するので、あまり安定的な解決策とはいえなさそうです。

またこの方法では他のPodからloadBalancerに割り当てられたIPアドレスにはアクセスできない点も期待とは違う動作になります。

# まとめ

外部に公開したいサービスはIngressから "ユーザー名"-svc という名前のServiceオブジェクトを経由して、あらかじめ外部からアクセスできるようにしています。

これは80・443番ポート限定ですし、URLのcontext-rootが'/'ではなくなるため、アプリケーションの構成に制約が発生します。DVWAのような'/'で動作することを期待するContainerは正常に動作させることができません。

(厳密にはcontext-rootを'/'として転送することは可能ですが、副作用が発生する場合があるため、Reverse Proxyサーバーと転送先Webサーバーとのcontext-rootは一致させることが理想的です)

利用者にはDeploymentやStatefullSetの定義で特定のlabelを付与してネットワークアクセスを許可した上で、ServiceオブジェクトのtpyeをloadBalancerにすることでIPの割り当てを受けるよう案内することを考えています。

二度手間だとは思いますが、ミドルウェアなどを複数namespaceで共有したい場合には &lt;namespace&gt;.svc.localドメインを利用したFQDNでアクセスすれば良いので、LoadBalancerは不要です。

多様なユースケースに対応するためには、現状のソリューションが最適なのかと思いました。