1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-05-31

はじめに

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

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

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

参考資料

前提

  1. kubesprayで導入したオンプレミスのk8sクラスター(v1.25.6)が対象
  2. 複数ユーザーが利用する環境である
  3. ユーザー毎に一意のnamespaceがあらかじめ作成されている (namespace名は認証時のユーザー名と同一)
  4. NetworkPolicyのデフォルト設定では、Ingress(INPUT)は拒否し、Egress(OUTPUT)は許可する設定となっている
  5. 自身のnamespaceにある全てのPodからの通信(Ingress)は許可する
  6. namespace: ingress-nginxにある全てのPodからの通信(Ingress)は許可する
  7. adminユーザーのみがNetworkPolicyを定義し、ユーザーのNetworkPolicyリソースに対する"create", "delete"権限は剥奪している
  8. あらかじめユーザーにNetworkPolicyを定義することとして、後からNetworkPolicyの生成・適用は行わない
  9. 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を経由しない <namespace>.svc.local ドメインを使用することで、他のnamespaceとの通信も可能となります。

理想的な解決手段

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

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

テスト環境の構築

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

デフォルトのIngress・Egressルール(Ingressは不許可、Egressは許可)
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-networkpolicy
  namespace: yasu
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  egress:
  - {}
自分の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からの通信は全て許可しています。

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を指定するだけです。

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との通信が確立するようです。

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が適用される前段と思われますので、別の方法を検討しなければ難しいだろうと思われます。

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

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は不要です。

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?