はじめに
今回、KubernetesでNetworkPolicyを利用する主な目的はnamespace間の通信を抑制することです。
とはいえ、手元の環境ではグローバルIPからのリクエストはIngressを経由しなくてはいけないので、NetworkPolicyでは自分のnamespaceとingress-nginx namespaceからの通信は許可する設定にしています。
この時にLoadBalancerとの関係について調べたので、その顛末をメモしておきます。
参考資料
- StackOverflow::Kubernetes NetworkPolicy allow loadbalancer
- How to Configure NetworkPolicy for NodePort in Kubernetes
前提
- kubesprayで導入したオンプレミスのk8sクラスター(v1.25.6)が対象
- 複数ユーザーが利用する環境である
- ユーザー毎に一意のnamespaceがあらかじめ作成されている (namespace名は認証時のユーザー名と同一)
- NetworkPolicyのデフォルト設定では、Ingress(INPUT)は拒否し、Egress(OUTPUT)は許可する設定となっている
- 自身のnamespaceにある全てのPodからの通信(Ingress)は許可する
- namespace: ingress-nginxにある全てのPodからの通信(Ingress)は許可する
- adminユーザーのみがNetworkPolicyを定義し、ユーザーのNetworkPolicyリソースに対する"create", "delete"権限は剥奪している
- あらかじめユーザーにNetworkPolicyを定義することとして、後からNetworkPolicyの生成・適用は行わない
- LoadBalancerはk8sの各ノードも接続している192.168.1.0/24ネットワークのIPアドレスを割り当てる
LoadBalancerにはmetallbを利用していて、Serviceオブジェクトにtype: loadBalancerを指定するだけで、あらかじめ指定したrangeからLAN上のIPv4アドレスを払出してくれています。
期待する振舞い
- ユーザーがServiceオブジェクトに対しtype: loadBalancerを指定した場合、全てのPodとローカルネットワークからの接続を許可する
- LoadBalancer経由でアクセスできないPodには外部からの通信を拒否したい (デフォルト設定の維持)
問題
実際にはloadBalancer経由でだけ外部からの通信を許可するといった構成はできません。
あらかじめユーザーが公開したいPodに特定のラベルをつけることで、そのPodへの通信が全て許可される方法を応用する必要があります。
- Serviceオブジェクトにtype: loadBalancerを指定する
- ServiceオブジェクトのselectorにマッチするPodに公開を許可する特別なlabelを付与する
このためloadBalancerを経由しない <namespace>.svc.local ドメインを使用することで、他のnamespaceとの通信も可能となります。
理想的な解決手段
type: LoadBalancerを追加したタイミングで、そのServiceオブジェクトのselectorに対応するPodに対するIngressの通信を許可する設定を加えたい。
このような挙動はNetworkPolicyオブジェクトの設定だけでは実現不可能です。
テスト環境の構築
まず本来やりたい自分のnamespaceとingressだけの接続を許可したいというところは、次のような設定で制御できています。
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-networkpolicy
namespace: yasu
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
egress:
- {}
---
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からの通信は全て許可しています。
---
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を指定するだけです。
---
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との通信が確立するようです。
$ 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が適用される前段と思われますので、別の方法を検討しなければ難しいだろうと思われます。
ここまできて、再度検索してみると似たような記事を見つけました。
tunl0からの接続を許可するのは現実的とはいえなさそうです。
事後で選択的に接続を許可すれば目的は達成できますが、あらかじめnamespaceをまたぐ通信は不許可、LoadBalancerを指定してもらえればできますよ、という構成は諦めることにしました。
もう少し詳しく
ターゲットのPodが動作しているノードのtunl0をtcpdumpでモニターしてみます。
$ 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) という接続を示しています。
$ 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で共有したい場合には <namespace>.svc.localドメインを利用したFQDNでアクセスすれば良いので、LoadBalancerは不要です。
多様なユースケースに対応するためには、現状のソリューションが最適なのかと思いました。