kubernetes的网络模型,只是关注容器之间网络的“连通”,却不关心容器之间网络的“隔离”。如何实现网络的隔离来满足多租户的需求。
在kubernetes中,网络隔离能力的定义,依靠专门的API对象来描述,NetworkPolicy。它定义的规则其实是白名单。
一个完整的NetworkPolicy对象的示例如下:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: default
spec:
podSelector: # 定义这个NetworkPolicy的限制范围
matchLabels: # 在当前Namespace中携带role=db标签的Pod
role: db # podSelector: {} 表示当前Namespace下所有的Pod
policyTypes:
- Ingress # 影响流入请求
- Egress # 影响流出请求
ingress:
- from: # 允许流入的白名单,并列的三种情况(满足一个即生效):
# ipBlock,namespaceSelector,podSelector
# 不用`-`分隔字段时,表示条件需要同时满足
- ipBlock:
cidr: 172.17.0.0/16 # 源地址网段
except:
- 172.17.1.0/24
- namespaceSelector:
matchLabels:
project: myproject
- podSelector:
matchLabels:
role: frontend
ports: # 允许流入的端口
- protocol: TCP
port: 6379
egress:
- to: # 允许流出的白名单
- ipBlock:
cidr: 10.0.0.0/24 # 目的地址网段
ports: # 允许流出的端口
- protocol: TCP
port: 5978
kubernetes里的Pod默认都是“允许所有(Accept All)”,即:
如果要对这个情况做出限制,就必须通过NetworkPolicy对象来指定。
一旦pod被NetworkPolicy选中,那么就会进入“拒绝所有”(Deny All)的状态,即:
上述NetworkPolicy独享指定的隔离规则如下:
10.0.0.0/24
网段,并且访问的是该网段地址的5978端口白名单对象包括:
172.17.0.0/16
网段,且不属于172.17.1.0/24
网段的请求要使用上述定义的NetworkPolicy在kubernetes集群中真正产生作用,需要CNI网络插件支持kubernetes的NetworkPolicy。
凡是支持NetworkPolicy的CNI网络插件,都维护着一个NetworkPolicy Controller,通过控制循环的方式对NetworkPolicy对象的增删改查做出响应,然后在宿主机上完成iptables规则的配置工作。
目前实现NetworkPolicy的网络插件包括Calico、Weave和kube-router等。在使用Flannel的同时要使用NetworkPolicy的话,就需要在额外安装一个网络插件,如Calico来负责执行NetworkPolicy。
以三层网络插件(Calico和kube-router)为例,分析一下实现原理。
创建一个NetworkPolicy对象,如下:
apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
ingress:
- from:
- namespaceSelector:
matchLabels:
project: myproject
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: tcp
port: 6379
kubernetes网络插件使用上述NetworkPolicy定义,在宿主机上生成iptables规则,具体过程如下:
for dstIP := range 所有被 networkpolicy.spec.podSelector 选中的 Pod 的 IP 地址 {
for srcIP := range 所有被 ingress.from.podSelector 选中的 Pod 的 IP 地址 {
for port, protocol := range ingress.ports {
iptables -A KUBE-NWPLCY-CHAIN -s $srcIP -d $dstIP -p $protocol -m $protocol --dport $port -j ACCEPT
// 规则的名字 KUBE-NWPLCY-CHAIN
// 含义:当IP包的源地址是srcIP,目的地址是dstIP,协议是protocol,目的端口是port的时候,就运行它通过(ACCEPT)
}
}
}
// 通过匹配条件决定下一步iptables规则
// 匹配这条规则所需要的四个参数都是从NetworkPolicy对象里读取出来
kubernetes网络插件对Pod进行隔离,其实是靠宿主机上生成的NetworkPolicy对应的iptables规则来实现的。在设置好上述“隔离”规则之后,网络插件还需要想办法,将所有对被隔离Pod的访问请求,都转发到上述KUBE-NWPLCY-CHAIN规则上去进行匹配,如果匹配不通过这个请求应该被“拒绝”。
负责拦截对被隔离Pod的访问请求,生成这一组规则的伪代码如下:
for pod := range 该 Node 上的所有 Pod {
if pod 是 networkpolicy.spec.podSelector 选中的 {
iptables -A FORWARD -d $podIP -m physdev --physdev-is-bridged -j KUBE-POD-SPECIFIC-FW-CHAIN
// FORWARD链拦截一种特殊情况,它对应的是同一台宿主机上的容器之间经过CNI网桥进行通信的流入数据包
// --physdev-is-bridged表示这个FORWARD链匹配的是,通过本机上的网桥设备,发往目的地址的IP包
iptables -A FORWARD -d $podIP -j KUBE-POD-SPECIFIC-FW-CHAIN
// FORWARD链拦截普通情况,容器跨主机通信,流入容器的数据包都是经过路由转发(FORWARD检查点)来的
// 这些规则最后都跳转到了KUBE-POD-SPECIFIC-FW-CHAIN规则上,这是NetworkPolicy设置的第二组规则
...
}
}
// iptables规则使用了内置链:FORWARD
iptables只是一个操作Linux内核Netfilter子系统的“界面”,Netfilter子系统的作用,就是Linux内核里挡在“网卡”和”用户态进程"之间的一道”防火墙“。他们之间的关系如下所示。
IP包一进一出的两条路径上,有几个关键的”检查点“,它们正是Netfilter设置”防火墙“的地方。在iptables中,这些检查点被称为链(Chain)。这些检查点对应的iptables规则是按照定义顺序依次进行匹配的。具体工作原理如下图所示。
在Linux内核实现里,所谓”检查点“实际上就是内核网络协议代码里的Hook(比如,在执行路由判断的代码之前,内核会先调用PREROUTING的Hook)。
POSTROUTING的作用,是上述两条路径,最终汇聚在一起的”最终检查点”。
在网桥参与的情况下,上述Netfilter设置检查点的流程,实际上也会出现在链路层(二层),并且会跟上面的网络层(三层)的流程有交互。链路层的检查点对应的操作界面叫作ebtables。数据包在Linux Netfilter子系统里完整的流动过测井如下图所示。
上述过程是途中绿色部分,即网络层的iptables链的工作流程。
KUBE-POD-SPECIFIC-FW-CHAIN,做出允许或拒绝的判断,这部分功能的实现,类似如下的iptables规则:
iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j KUBE-NWPLCY-CHAIN
# 把IP包转交给前面定义的KUBE-NEPLCY-CHAIN规则去进行匹配,匹配成功运行通过,匹配失败进入下一条规则
iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j REJECT --reject-with icmp-port-unreachable
# REJECT规则,通过这条规则,不满足NetworkPolicy定义的请求会被拒绝掉,从而实现了该容器的“隔离”
NetworkPolicy只是宿主机上一系列iptables规则,与传统IaaS的安全组类似。
kubernetes的网络模型以及大多数容器网络实现,即不会保证容器之间二层网络的互通,也不会实现容器之间二层网络的隔离,这与IaaS项目管理的虚拟机是完全不同的。kubernetes从底层的设计和实现上,更倾向于假设你已经有一套完整的物理基础设施,kubernetes负责在此基础上提供“弱多租户”的能力。