Project CalicoをKubernetesで使ってみる:ネットワークポリシー編
はじめに
これまでの回で、Calicoのアーキテクチャや構築方法を説明してきました。実際に商用環境などで使うことを考えると、必ず検討しなくてはならないのがセキュリティです。Calicoではコンテナなどのワークロードに対するネットワークセキュリティへのアプローチとして、「Network Policy」(ネットワークポリシー)という機能を備えています。
今回はCalicoのNetwork Policyについて説明し、前回までに構築したKubernetes環境を用いて実際にNetwork Policyを使ってみたいと思います。
マイクロサービスにおけるセキュリティ
Calicoがセキュリティ機能を備えているのはなぜでしょうか。Webサービスを中心としたクラウドネイティブアプリケーションはマイクロサービスアーキテクチャで実装されることが増えてきています。マイクロサービスアーキテクチャとは、機能単位に分割されたマイクロサービスの集合体として1つのサービスを構築してしまう考え方です。例えば、あるWebサービスを提供するためにフロントエンドサービスとバックエンドサービスがある場合、マイクロサービスアーキテクチャになると、フロントエンドサービスも複数のマイクロサービスの集合体で構成されます。このように実装されたサービスは、さらに他のサービスと連携して新たなサービスを提供するようになります。
複数のサービスが相互に通信する環境においては、従来のようにネットワークドメインの境界にファイアーウォールを設置するようなセキュリティの設計では、さまざまな脅威に対する対策として不十分になってきます。そこで、マイクロサービス単位にファイアーウォールを設定し、アクセスを制限するという考え方が重要になってきます。
しかしながら、この考え方に基づいてファイアーウォールの設定を一つ一つのサービスおよびPodに対して手動で投入していくというのは、Podのライフサイクルの面から考えても現実的ではありません。Kubernetesではこの課題に対してNetwork Policyという機能を持っています。そして、CalicoはKubernetesのネットワークプラグインとしてこの機能の実装を担っています。
Kubernetes Network Policy
CalicoのNetwork Policyを理解するために、KubernetesのNetwork Policyを説明します。
Kubernetes Network Policyとは、Podレベルのファイアーウォールルールを提供するセキュリティ機能です。これを使用することにより、クラスタ内でどのPodやサービスが相互にアクセスできるかを制御できます。
Kubernetes Network Policyを使用するには、ネットワークプラグイン側がそれに対応した機能を持っている必要があります。現在対応しているネットワークプラグインは、Calico、Weave、Ciliumなどがあります。Kubernetesのネットワークプラグインとしてよく目にするFlannelは、Flannel単体ではNetwork Policyに対応していません。そこで、CalicoをNetwork Policy機能のみで動作させ、Flannelと統合した「Canal」というプラグインも開発されています。Kubernetesのドキュメントで、Network Policyに対応したプラグインとそれぞれの特徴が紹介されています。
Kubernetes Network Policyの書き方
それではKubernetesのNetwork Policyの書き方を解説します。
Kubernetesにおける設定ファイルはYAMLかJSONで記述できますが、よりユーザーフレンドリーであるYAMLでの記述が一般的なので、今回もYAMLで書いていきます。
Kubernetes Network Policyの特徴は、許可された通信以外を拒否するホワイトリスト方式であるということです。specのpolicyTypesで指定した方向の通信を全て拒否する動作となっており、specのingress/egressに記載した条件を許可する動作となっています。ルールの詳細な記述方法はKubernetesのドキュメントに記載されています。
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: ”ポリシー名” namespace: ”ポリシーのネームスペース名” spec: podSelector: ”適用するPod” policyTypes: - Ingress ”Ingressトラフィックをすべて拒否する” - Egress ”Egressトラフィックをすべて拒否する” ingress: ”許可するIngressトラフィックの条件” egress: ”許可するEgressトラフィックの条件”
CalicoのNetwork Policyについて
Kubernetesで定義されたNetwork Policyは、Calicoが解釈できるデータモデルでetcdに書き込まれ、最終的にはCalicoのNetwork Policyとしてワークロードエンドポイントに適用されます。CalicoのNetwork Policyが特徴的なのは、ワークロードエンドポイントだけではなくホストのインターフェースに適用できる点です。このCalicoの機能はHost Protection(ホストプロテクション)と呼ばれています。この機能を使用することで、ホストのインターフェースに対して、トラフィックの方向(Ingress/Egress)や制限方式(ホワイトリスト/ブラックリスト)、プロトコル(TCP/UDP/ICMP)、ポート番号など柔軟な制限をかけることが可能になります。
Calicoでの通信に制限をかける方法にはPolicyモデルとProfileモデルがあります。
Policyモデルでは、ルールの中でラベルを指定することで、ラベルに紐づいたエンドポイントに対してまとめて制限をかけることができます。Profileモデルでは、事前にルールを宣言し、エンドポイントの設定でProfileを指定することでエンドポイントに制限をかけることができます。どちらの方法を使用しても定義できるルールは同じですが、OpenStackやKubernetesなどのさまざまなオーケストレーターのプラグインとして使われることが想定されているため、オーケストレーター毎に適した定義方法を利用できるようになっています。
ちなみに、KubernetesやCalicoのNetwork PolicyはPod間の通信をTCP/IP(L3-4)で制限します。HTTPメソッド(GETやPOSTなど)といったアプリケーションレイヤー(L7)での制限や暗号化などには対応していません。アプリケーションレイヤーでルールを適用する、つまりマイクロサービスのセキュリティ対策としては、Istioやlinkerdなどを使用する必要が出てきます。
Kubernetes Network PolicyとCalicoのネットワークポリシーとの使い分け
CalicoのNetwork Policyが使用される場面は2つあります。
1つ目は、オーケストレーターのセキュリティ機能のプラグインとして暗黙的に使用される場合です。この場合、Network Policyの利用者はCalicoを直接操作することはありません。2つ目は、CalicoのNetwork Policyを直接使用する場合です。
-
- オーケストレーターからプラグインとして使用される場合
- 例を挙げると、Calicoプラグインを使用したKubernetesでネットワークポリシーを使用した場合などがこれに当たります。PodやDeploymentのルールを定義する際には、基本的にはKubernetesのネットワークポリシーを使用することが推奨されています。
-
- CalicoのNetwork Policyを直接使用する場合
- Kubernetesのネットワークポリシーでは設定できない、Host ProtectionなどといったCalicoの機能を使用する場合のみ、Calicoのネットワークポリシーを使用する必要があります。
GKEとEKSの事例
これまで説明したように、CalicoのNetwork Policyの機能は、コンテナのセキュリティを考慮する上で非常に重要な役割を果たします。すでに商用環境にCalicoを使用している事業者もあります。ここでは、実際にCalicoのNetwork Policyが使用されている事例を2つ紹介します。
Google Kubernetes Engine(GKE)の事例(Google)
Google Cloud Platform Blogでは、「既存のFWはNorthSouthトラフィックに対して境界セキュリティを提供するが、コンテナ間のEastWestトラフィックのセキュリティには適していないという問題があり、Calicoが解決策となっている」と紹介されています。
Elastic Container Service(EKS)の事例(Amazon)
AWS Open Source Blogでは、どのネットワークのアプローチを選択しても、Calicoのセキュリティ機能を使用できることが紹介されています。
実際にポリシーをかけてみる
当連載の第3回ではdefaultのネームスペースに対してNginxのコンテナをデプロイし、同じネームスペースにデプロイした、cURLがインストールされているPodからのアクセスを確認しました。今回はKubernetsのNetwork PolicyとCalicoのNetwork Policyをそれぞれ設定していきます。
Kubernetes Network Policyをかけてみる
Kubernetesのデフォルトでは、全てのPodが相互に通信できるようになっているため、アクセス制限をかけたい場合には明示的に設定する必要があります。
まずはじめに、「test」というネームスペースにデプロイしたPodからNginxにアクセスできることを確認します。その後に、KubernetesのNetwork PolicyでdefaultネームスペースのPodに対して、全てのアクセスを制限するルールと、同じネームスペースからのアクセスは許可するルールを設定します。そうすることにより、defaultネームスペースのPodからはcurlで(HTTP)アクセス可能ですが、testネームスペースのPodからはcURLで(HTTP)アクセスできなくなることを確認していきます。
まず、第3回の環境の確認として、NginxのPodがデプロイされていることを確認します。
k8s-master:~$ kubectl get pods -n default -o wide NAME READY STATUS RESTARTS AGE IP NODE my-nginx-9d5677d94-48pmz 1/1 Running 0 3d 192.168.154.193 k8s-node-01 my-nginx-9d5677d94-z655m 1/1 Running 0 3d 192.168.44.193 k8s-node-02
Nginxのコンテナが「k8s-node-01」と「k8s-node-02」にデプロイされているのがわかります。それではまず、「test」ネームスペースを作成します。
k8s-master:~$ kubectl create namespace test namespace "test" created
ネームスペースが作成できたか確認します。
k8s-master:~$ kubectl get namespaces NAME STATUS AGE default Active 3d kube-public Active 3d kube-system Active 3d test Active 1m
「test」ネームスペースを作成できていることが確認できます。次に、作成したネームスペースに対してラベルを設定します。今回はtestネームスペースに対して、「test-label=default」というラベルを設定します。
k8s-master:~$ kubectl label namespace/default test-label=default namespace "default" labeled
ラベルが設定できたか確認します。
k8s-master:~$ kubectl get namespaces --show-labels NAME STATUS AGE LABELS default Active 4d test-label=default kube-public Active 4d <none> kube-system Active 4d <none> test Active 2h <none>
defaultネームスペースに対して「test-label=default」というラベルを設定できていることが確認できます。次にtestネームスペースにcURLがインストールされているPodを立ち上げます。
k8s-master:~$ kubectl run curl --image=radial/busyboxplus:curl -i --tty --rm -n test If you don't see a command prompt, try pressing enter. [ root@curl-545bbf5f9c-gdww6:/ ]$
Podを立ち上げた状態で、別の画面から下記のコマンドを実行することで、testネームスペースにcURLがインストールされているPodが立ち上がっていることを確認できます。
k8s-master:~$ kubectl get pods --all-namespaces NAMESPACE NAME READY STATUS RESTARTS AGE default my-nginx-9d5677d94-48pmz 1/1 Running 0 3d default my-nginx-9d5677d94-z655m 1/1 Running 0 3d kube-system calico-etcd-dxb8s 1/1 Running 0 4d kube-system calico-kube-controllers-559b575f97-29m7j 1/1 Running 0 4d kube-system calico-node-2mttf 2/2 Running 0 3d kube-system calico-node-6snck 2/2 Running 1 3d kube-system calico-node-bggsx 2/2 Running 0 4d kube-system etcd-k8s-master 1/1 Running 0 4d kube-system kube-apiserver-k8s-master 1/1 Running 0 4d kube-system kube-controller-manager-k8s-master 1/1 Running 0 4d kube-system kube-dns-6f4fd4bdf-8xwwz 3/3 Running 0 4d kube-system kube-proxy-22gs4 1/1 Running 0 3d kube-system kube-proxy-f5mpg 1/1 Running 0 4d kube-system kube-proxy-tc24n 1/1 Running 0 3d kube-system kube-scheduler-k8s-master 1/1 Running 0 4d test curl-545bbf5f9c-gdww6 1/1 Running 0 43s
それではcURLがインストールされたPodからNginxのPod IPに対して、curlでアクセスを行います。ここではk8s-node-01上のNginx(192.168.154.193)に対してのみ確認を行います。
[ root@curl-545bbf5f9c-6fj7w:/ ]$ curl 192.168.154.193 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>
testネームスペースのPodからdefaultネームスペースのNginxへのアクセスが確認できました。KubernetesではデフォルトとしてPod間の通信は制限されておらず、別のネームスペースに存在するPod間でも通信が可能なことがわかります。
それではdefaultネームスペースに対して、他のネームスペースからのアクセスを制限するネットワークポリシーを設定していきます。「deny-all-allow-default.yaml」というファイルを作成し、下記の設定を記述します。
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: deny-all-allow-default namespace: default spec: podSelector: {} policyTypes: - Ingress ingress: - from: - namespaceSelector : matchLabels: test-label: default
今回の設定内容は、defaultネームスペースに「deny-all-allow-default」という名前のネームスペースからの入力トラフィックを許可するものです。「deny-all-allow-default」ネームスペースは全てのPodに対して入力トラフィックを拒否し、「test-label=default」にマッチするラベルを持っています。つまり、defaultネームスペースのPodへアクセスできるのはdefaultネームスペースのPodからのみで、他のネームスペースからのアクセスは拒否する設定になります。
それでは作成したネットワークポリシー(deny-all-allow-default.yaml)を適用していきましょう。
k8s-master:~$ kubectl apply -f deny-all-allow-default.yaml networkpolicy "deny-all-allow-default" created
ネットワークポリシーが作成された確認を行います。
k8s-master:~$ kubectl get networkpolicy -o wide --all-namespaces NAMESPACE NAME POD-SELECTOR AGE default deny-all-allow-default <none> 46s
defaultというネームスペースに対して「deny-all-allow-default」というポリシーを作成できていることが確認できます。
それではdefaultネームスペースと、cURLがインストールされているtestネームスペースのPodからcurlでアクセスを行い、アクセスが制限されているかの確認を行います。まずはdefaultネームスペースから確認します。
k8s-master:~$ kubectl run curl --image=radial/busyboxplus:curl -i --tty --rm -n default If you don't see a command prompt, try pressing enter. [ root@curl-545bbf5f9c-fb7hs:/ ]$ [ root@curl-545bbf5f9c-fb7hs:/ ]$ curl 192.168.154.193 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>
defaultネームスペースでは問題なくアクセスできていることがわかります。次にtestネームスペースも確認していきます。
k8s-master:~$ kubectl run curl --image=radial/busyboxplus:curl -i --tty --rm -n test If you don't see a command prompt, try pressing enter. [ root@curl-545bbf5f9c-wm5cg:/ ]$ [ root@curl-545bbf5f9c-wm5cg:/ ]$ curl 192.168.154.193 curl: (7) Failed to connect to 192.168.154.193 port 80: Connection timed out
testネームスペースのPodからは、defaultネームスペースのNginx Podに対してアクセスが制限されていることを確認できました。
calicoctlからworkloadのNetwork Policyを確認する
calicoctlコマンドを使用して、kubernetesでdefaultネームスペースに作成した「deny-all-allow-default」というネットワークポリシーがCalicoで設定されていることを確認していきます。
k8s-master:~$ ./calicoctl get networkpolicy NAME knp.default.deny-all-allow-default
Calicoに「knp.default.deny-all-allow-default」というネットワークポリシーが作成されていることがわかります。「knp.default.deny-all-allow-default」というCalicoのネットワークポリシーの設定を確認していきましょう。
/calicoctl get networkpolicy knp.default.deny-all-allow-default -o yaml apiVersion: projectcalico.org/v3 kind: NetworkPolicy metadata: creationTimestamp: 2018-03-19T02:26:46Z name: knp.default.deny-all-allow-default namespace: default resourceVersion: "4241" uid: f8af771d-2b1c-11e8-b6bd-525400073a83 spec: ingress: - action: Allow destination: {} source: namespaceSelector: test-label == 'default' selector: projectcalico.org/orchestrator == 'k8s' order: 1000 selector: projectcalico.org/orchestrator == 'k8s' types: - Ingress
defaultネームスペースに対して、「test-label=default」のラベルを持ったネームスペースからの通信を許可する設定がされており、KubernetesのNetwork PolicyがCalicoのNetwork Policyで設定されていると確認できました。
iptablesを確認してみる
次に、CalicoのNetwork Policyがiptablesで実装されているかを確認していきます。
CalicoのNetwork Policyの設定は、iptablesとipsetを使用して行われます。ipsetとはIPアドレスやMACアドレスなどを集合として扱うiptablesの補助技術で、ipsetの情報を表示するにはipsetのパッケージをインストールする必要があります。Network PolicyはPodが存在するホストに設定されるため、今回はk8s-node-01で確認していきます。
先ほど設定したPolicyではdefaultネームスペースに対しての入力トラフィックを拒否する設定を行ったため、defaultネームスペースに存在するNginxPodのIPへの通信を拒否する設定がiptablesに設定されます。
ipsetコマンドを使用することで、NginxPodのIPを「cali4-s:Hcn0P2TmkHTJcBdhlFhs23C」という名前で集合として扱えるようにしていることがわかります。
k8s-node-01:~$ sudo ipset list cali4-s:Hcn0P2TmkHTJcBdhlFhs23C Name: cali4-s:Hcn0P2TmkHTJcBdhlFhs23C Type: hash:ip Revision: 4 Header: family inet hashsize 1024 maxelem 1048576 Size in memory: 224 References: 3 Members: 192.168.154.193 192.168.44.193
以下は「sudo iptables -L」コマンドの抜粋になりますが、ipsetで設定した「cali4-s:Hcn0P2TmkHTJcBdhlFhs23C」を条件に処理していることがわかります。
Chain cali-pi-_2ZqniN3ecx1Jf94lHPG (1 references) target prot opt source destination MARK all -- anywhere anywhere /* cali:75z4Tuuv5eJR6QB2 */ match-set cali4-s:Hcn0P2TmkHTJcBdhlFhs23C src MARK or 0x1000000 RETURN all -- anywhere anywhere /* cali:ALJPOl24E6kB7wLn */ mark match 0x1000000/0x1000000
今回はCalicoでiptablesが設定されていることのみを確認し、iptablesの内容の詳細については割愛します。
ここまではKubernetesのNetwork Policyを使用しました。次はCalicoのNetwork Policyを使用しHostProtectionの設定を行います。
HostProtectionをかけてみる
KubernetesのNetwork Policyはワークロードエンドポイントに適用されますが、CalicoのNetwork Policyはホストのインターフェースにも適用できます。実際にHostProtectionを使用する例としては、Calicoノードへの不正アクセスを防ぐために、CalicoノードへSSH接続できるIPを制限したい場合などがあります。
ちなみにCalicoには、Calicoノードの管理に必要な通信を制限できないようにする「Failsafe rules」という機能があります。SSHのポートはFailsafe rulesに含まれているため、SSH接続を制限するにはFailsafe rulesを無効にする必要があります。Failsafe rulesに設定されている通信の内容はCalicoのドキュメントに記載されています。今回はマスターに対しHostProtectionを設定し、ノードからマスターへのICMPを制限していきます。
実際にICMPの制限をかける前に、ノードからマスターに対してICMPの疎通確認を行います。
k8s-node-01:~$ ping -c 5 172.16.0.10 PING 172.16.0.10 (172.16.0.10) 56(84) bytes of data. 64 bytes from 172.16.0.10: icmp_seq=1 ttl=64 time=1.09 ms 64 bytes from 172.16.0.10: icmp_seq=2 ttl=64 time=5.28 ms 64 bytes from 172.16.0.10: icmp_seq=3 ttl=64 time=0.809 ms 64 bytes from 172.16.0.10: icmp_seq=4 ttl=64 time=1.08 ms 64 bytes from 172.16.0.10: icmp_seq=5 ttl=64 time=0.771 ms --- 172.16.0.10 ping statistics --- 5 packets transmitted, 5 received, 0% packet loss, time 4003ms rtt min/avg/max/mdev = 0.771/1.810/5.286/1.743 ms
ノード1からマスターに対してICMP疎通できていると確認できました。それでは実際に設定を行っていきます。Profileでルールを宣言し、HostEndpointでProfileを紐付けることでHostProtectionをかけることができます。まずProfileの設定を「deny-icmp-profile.yaml」というファイルに記述します。
apiVersion: projectcalico.org/v3 kind: Profile metadata: name: deny-icmp-profile spec: egress: - action: Allow ingress: - action: Deny protocol: ICMP - action: Allow
ICMPの入力トラフィックを拒否し、他の入力トラフィックと出力トラフィックを許可するProfileの設定になります。Profileを作成するだけではどこにも紐づかないため、このProfileを有効にするためにはHostEndopointの設定でProfileを指定する必要があります。HostEndpointの設定を「master-hostendpoint.yaml」というファイルに記述します。
apiVersion: projectcalico.org/v3 kind: HostEndpoint metadata: name: master-hostendpoint spec: interfaceName: enp0s3 node: k8s-master expectedIPs: - 172.16.0.10 profiles: - deny-icmp-profile
マスターのipが172.16.0.10でインターフェース名がenp0s3であるインターフェースに「deny-icmp-profile」というprofileを紐付ける設定になります。まずProfileを適用します。
k8s-master:~$ ./calicoctl apply -f deny-icmp-profile.yaml Successfully applied 1 'Profile' resource(s)
Profileが作成された確認を行います。
./calicoctl get profiles NAME deny-icmp-profile kns.default kns.kube-public kns.kube-system kns.test
「deny-icmp-profile」というProfileが作成されていることが確認できます。次に、HostEndpointを適用していきます。
k8s-master:~$ ./calicoctl apply -f master-hostendpoint.yaml Successfully applied 1 'HostEndpoint' resource(s)
HostEndpontが作成された確認を行います。
k8s-master:~$ ./calicoctl get hostendpoint NAME NODE master-hostendpoint k8s-master
「master-hostendpoint」というHostEndpointが作成されていることが確認できます。それでは実際にマスターに対してICMPの疎通を確認しましょう。
k8s-node-01:~$ ping -c 5 172.16.0.10 PING 172.16.0.10 (172.16.0.10) 56(84) bytes of data. --- 172.16.0.10 ping statistics --- 5 packets transmitted, 0 received, 100% packet loss, time 4030ms
ホストエンドポイントへの通信が制限できていることが確認できました。
終わりに
今回はCalicoのNetwork Policyについて紹介しました。コンテナなどのワークロードに対するネットワークセキュリティへのアプローチとしてCalicoが重要な役割を担っていることがお分かりいただけたと思います。
本記事ではあまり触れていませんが、CalicoはIstioなどのサービスメッシュを実現するOSSなどとも連携して、より堅牢なセキュリティ機能を実現することを目指しています。コンテナネットワークにおけるセキュリティは今後ますます重要になってくるので、その取っ掛かりとしてCalicoを使ってみるのも良いかもしれません。