目录

cilium-chain的host-firewall模式

概述

Cilium Host Firewall 是 Cilium 提供的一项功能,它允许管理员为主机级网络流量定义安全策略。这意味着除了能够为进入和离开容器的网络流量定义安全策略外,你也可以为进入和离开节点本身的流量定义安全策略。这样,就可以提供更全面的安全覆盖。

Host Firewall 的工作原理是通过在主机节点上应用 eBPF 程序来控制和限制网络流量。这允许管理员创建规则,例如阻止或允许特定类型的流量、基于特定的源或目标地址、端口号或协议,以及其他可能的分类。

安装

通过下面的命令可以设置 Host Firewall 开启,以及通过 devices 字段控制策略会被 attach 到哪个设备。下面的测试会基于 cilium-chain 的模式来运行。

1
helm install cilium . --set hostFirewall.enabled=true --set devices='{ethX,ethY}'

测试验证

下面将在这个集群进行测试,节点的相关信息如下。

1
2
3
4
5
# k get no -o wide
NAME    STATUS  ROLES          AGE   VERSION INTERNAL-IP   OS-IMAGE            KERNEL-VERSION                 
master  Ready   control-plane  4d13h v1.24.8 192.168.1.200 openEuler 22.03 LTS 5.10.0-60.18.0.50.oe2203.x86_64
node1   Ready   <none>         4d13h v1.24.8 192.168.1.201 openEuler 22.03 LTS 5.10.0-60.18.0.50.oe2203.x86_64
node2   Ready   <none>         4d13h v1.24.8 192.168.1.202 openEuler 22.03 LTS 5.10.0-60.18.0.50.oe2203.x86_64
1
kubectl label node node1 node-access=ssh

创建以下的主机网络策略。首先 nodeSelector 负责筛选指定节点,策略只允许 TCP/22 和 ICMP echo 请求从群集外部进行通信,同时也允许从群集到主机的所有通信。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "demo-host-policy"
spec:
  description: ""
  nodeSelector:
    matchLabels:
      node-access: ssh
  ingress:
  - fromEntities:
    - cluster
  - toPorts:
    - ports:
      - port: "22"
        protocol: TCP
  - icmps:
    - fields:
      - type: 8
        family: IPv4

查看 Endpoint 的情况。

1
2
3
4
5
# cilium endpoint list
ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value])  IPv4   STATUS
         ENFORCEMENT      ENFORCEMENT
2935     Enabled          Disabled        1        k8s:node-access=ssh                 ready
                                                   reserved:host

分别做以下的测试,可以见到主机上的网络策略是生效的。

/cilium-chain%E7%9A%84host-firewall%E6%A8%A1%E5%BC%8F/img.png /cilium-chain%E7%9A%84host-firewall%E6%A8%A1%E5%BC%8F/img_1.png

Audit模式

Cilium Host Firewall 一旦配置了,就会影响到 Node 的主机网络命名空间的流量,如果配置有问题,极有可能会影响节点本身的监控,跟 kube-apiserver 通信等问题,所以 Cilium 提供了一种 Audit 的模式,可以先在 Audit 模式下配置主机网络策略,Audit 模式可以理解即使网络策略加载到网卡了,但是策略还是会允许流量都放通,通过 cilium monitor 观察流量,如果网络策略配置成功的话,再手动 disable Endpoint 的 Audit 状态,完成主机 Firewall 的配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 开启Endpoint的Audit模式
CILIUM_NAMESPACE=kube-system
CILIUM_POD_NAME=$(kubectl -n $CILIUM_NAMESPACE get pods -l "k8s-app=cilium" -o jsonpath="{.items[?(@.spec.nodeName=='$NODE_NAME')].metadata.name}")
HOST_EP_ID=$(kubectl -n $CILIUM_NAMESPACE exec $CILIUM_POD_NAME -- cilium endpoint list -o jsonpath='{[?(@.status.identity.id==1)].id}')
kubectl -n $CILIUM_NAMESPACE exec $CILIUM_POD_NAME -- cilium endpoint config $HOST_EP_ID PolicyAuditMode=Enabled
dpoint 3353 configuration updated successfully
kubectl -n $CILIUM_NAMESPACE exec $CILIUM_POD_NAME -- cilium endpoint config $HOST_EP_ID | grep PolicyAuditMode
PolicyAuditMode          Enabled

# 取消Endpoint的Audit模式
kubectl -n $CILIUM_NAMESPACE exec $CILIUM_POD_NAME -- cilium endpoint config $HOST_EP_ID PolicyAuditMode=Disabled
Endpoint 3353 configuration updated successfully

测试挂载bond0

测试的目的和背景是,担心在开启 host firewall 的情况下,cilium-agent 会在主网卡上绑定 eBPF 程序来实现网络策略/软件防火墙的功能。由于在生产环境中,服务器的 bond0 网卡是不配置 IP 的,但是会配置一个 vlan 的子接口 bond0.212(或者是其他vlanId),并且会以此作为 ssh/netplugin 等使用的网卡,而普通容器的网卡流量是不会经过 bond0.212 的。

默认情况下,这个接口会成为 cilium-agent 的 Host Endpoint。

测试集群开启了 kube-proxy-replacement,并且设置 enable-node-port 和 enable-host-firewall 为 true,这个测试的目的在于希望将主机网卡的 eBPF 挂载到物理网卡,而不是挂载到 Vlan 网卡,并且验证这样的配置的有效性。

1
2
3
4
kube-proxy-replacement: partial
enable-node-port: "false"
enable-host-firewall: "true"
device: "bond0"

代码修改的部分如下,从代码的逻辑看,暂时通过 hard code,使得 cilium-agent 在判断是否给网卡加载 eBPF 程序的时候,会跳过我们的目的网卡 bond0.212,重新编译之后就可以放在 cilium-agent 的容器内运行了。

/cilium-chain%E7%9A%84host-firewall%E6%A8%A1%E5%BC%8F/img_3.png

但是实际上,这个操作是有问题的!现在节点已经无法通过 ssh 访问了。

/cilium-chain%E7%9A%84host-firewall%E6%A8%A1%E5%BC%8F/img_4.png

加载 eBPF 程序的日志,实际上在加载完 cil_to_netdev 这个 prog 之后,节点就「失联」了。

1
2
3
4
5
6
level=debug msg="Finished writing ELF" error="<nil>" new-elf-path=229_next/bpf_netdev_bond0.o subsys=elf template-path=229_next/bpf_host.o
level=debug msg="Loading CollectionSpec from ELF" device=bond0 ifindex=7 objPath=229_next/bpf_netdev_bond0.o subsys=datapath-loader
level=debug msg="Loading Collection into kernel" device=bond0 ifindex=7 objPath=229_next/bpf_netdev_bond0.o subsys=datapath-loader
level=debug msg="Attaching program to interface" device=bond0 direction=ingress ifindex=7 objPath=229_next/bpf_netdev_bond0.o progName=cil_from_netdev subsys=datapath-loader
level=debug msg="Successfully attached program to interface" device=bond0 direction=ingress ifindex=7 objPath=229_next/bpf_netdev_bond0.o progName=cil_from_netdev subsys=datapath-loader
level=debug msg="Attaching program to interface" device=bond0 direction=egress ifindex=7 objPath=229_next/bpf_netdev_bond0.o progName=cil_to_netdev subsys=datapath-loader

因为节点已经无法正常登录了,为了可以在节点无法访问的时候,依然可以访问到服务器,下面的测试会更换集群,不再原来的 Staging 集群进行测试了,改在测试环境的两个节点的测试集群下测试。

1
2
3
4
# k get no -o wide
NAME    STATUS   ROLES           AGE   VERSION   INTERNAL-IP     OS-IMAGE                    KERNEL-VERSION                         
node1   Ready    control-plane   18d   v1.24.8   10.199.100.34   openEuler 22.03 (LTS-SP1)   5.10.0-136.36.0.112.2.oe2203sp1.x86_64 
node2   Ready    <none>          18d   v1.24.8   10.199.100.35   openEuler 22.03 (LTS-SP1)   5.10.0-136.36.0.112.2.oe2203sp1.x86_64 
1
helm install cilium . --set hostFirewall.enabled=true --set devices=bond0

果然按照之前的部署方式,node1 和 node2 节点都无法访问了,下面我们通过服务器的管理卡登录服务器进行 debug。可以看到 bond0 上是已经挂载了 eBPF 程序,另外 bond0.1000(这个服务器的vlan子接口)上已经没有挂载 eBPF 程序了。

/cilium-chain%E7%9A%84host-firewall%E6%A8%A1%E5%BC%8F/img_5.png

那么到底是什么原因导致 bond0 的流量被阻挡了呢?下面进入到 cilium-agent 的容器内,通过 cilium monitor 进行观察。从结果看,带有 vlan tag 的流量都被 drop 了。

/cilium-chain%E7%9A%84host-firewall%E6%A8%A1%E5%BC%8F/img_6.png

Cilium enables firewalling on native devices in use and will filter all unknown traffic. VLAN 802.1q packets will always be passed through their main device with associated tag (e.g. VLAN device is eth0.4000 and its main interface is eth0). By default, Cilium will allow all tags from the native devices (i.e. if eth0.4000 is controlled by Cilium and has an eBPF program attached, then VLAN tag 4000 will be allowed on device eth0). Additional VLAN tags may be allowed with the cilium-agent flag –vlan-bpf-bypass=4001,4002 (or Helm variable –set bpf.vlanBypass="{4001,4002}").

从这里看,当我们用 --device=bond0 的时候,因为本身 bond0 没有带 vlan tag,因此默认情况下会拒绝带有 vlan tag 的流量。如果是按照正常的部署方式,--device 能够使用 bond0.1000,那么默认情况下,只要带有 vlan tag 1000的流量都不会被 drop。这也能够解释,为什么 --device=bond0 会导致节点失联,是因为办公网、测试环境只要 ssh 到服务器 IP,都会带有 vlan tag。

The list of allowed VLAN tags cannot be too big in order to keep eBPF program of predictable size. Currently this list should contain no more than 5 entries. If you need more, then there is only one way for now: you need to allow all tags with cilium-agent flag –vlan-bpf-bypass=0.

另外可以留意到,Cilium 提供一个 --vlan-bpf-bypass 选项来控制是否过滤哪些 vlan tag,于是我们可以尝试以下的部署方法。

1
 helm install cilium . --set hostFirewall.enabled=true --set devices=bond0 --set bpf.vlanBypass="{0}"

果然在调整了 --vlan-bpf-bypass 之后,Cilium 就不会再 drop 包了。

/cilium-chain%E7%9A%84host-firewall%E6%A8%A1%E5%BC%8F/img_7.png

测试一下 Host Firewall 的网络策略,创建跟上面一样的网络策略。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "demo-host-policy"
spec:
  description: ""
  nodeSelector:
    matchLabels:
      node-access: ssh
  ingress:
  - fromEntities:
    - cluster
  - toPorts:
    - ports:
      - port: "22"
        protocol: TCP
  - icmps:
    - fields:
      - type: 8
        family: IPv4

从日志看,网络策略已经创建成功。

1
2
3
4
5
6
level=debug msg="Adding CiliumNetworkPolicy" ciliumNetworkPolicyName=demo-host-policy k8sApiVersion= k8sNamespace= subsys=k8s-watcher
level=info msg="Policy Add Request" ciliumNetworkPolicy="[&{EndpointSelector:{} NodeSelector:{\"matchLabels\":{\"any:node-access\":\"ssh\"}} Ingress:[{IngressCommonRule:{FromEndpoints:[] FromRequires:[] FromCIDR: FromCIDRSet:[] FromEntities:[cluster] aggregatedSelectors:[{LabelSelector:0xc0003f5e60 requirements:0xc000450618 cachedLabelSelectorString:&LabelSelector{MatchLabels:map[string]string{reserved.host: ,},MatchExpressions:[]LabelSelectorRequirement{},}} {LabelSelector:0xc0003f5ec0 requirements:0xc000450738 cachedLabelSelectorString:&LabelSelector{MatchLabels:map[string]string{reserved.remote-node: ,},MatchExpressions:[]LabelSelectorRequirement{},}} {LabelSelector:0xc0003f5e80 requirements:0xc000450678 cachedLabelSelectorString:&LabelSelector{MatchLabels:map[string]string{reserved.init: ,},MatchExpressions:[]LabelSelectorRequirement{},}} {LabelSelector:0xc0003f5ea0 requirements:0xc0004506d8 cachedLabelSelectorString:&LabelSelector{MatchLabels:map[string]string{reserved.ingress: ,},MatchExpressions:[]LabelSelectorRequirement{},}} {LabelSelector:0xc0003f5ee0 requirements:0xc000450798 cachedLabelSelectorString:&LabelSelector{MatchLabels:map[string]string{reserved.health: ,},MatchExpressions:[]LabelSelectorRequirement{},}} {LabelSelector:0xc0003f5f20 requirements:0xc000450858 cachedLabelSelectorString:&LabelSelector{MatchLabels:map[string]string{reserved.unmanaged: ,},MatchExpressions:[]LabelSelectorRequirement{},}} {LabelSelector:0xc0003f5f40 requirements:0xc0004508b8 cachedLabelSelectorString:&LabelSelector{MatchLabels:map[string]string{reserved.kube-apiserver: ,},MatchExpressions:[]LabelSelectorRequirement{},}} {LabelSelector:0xc0015d7240 requirements:0xc0007ad4e8 cachedLabelSelectorString:&LabelSelector{MatchLabels:map[string]string{k8s.io.cilium.k8s.policy.cluster: default,},MatchExpressions:[]LabelSelectorRequirement{},}}]} ToPorts:[] ICMPs:[] Authentication:<nil>} {IngressCommonRule:{FromEndpoints:[] FromRequires:[] FromCIDR: FromCIDRSet:[] FromEntities:[] aggregatedSelectors:[]} ToPorts:[{Ports:[{Port:22 Protocol:TCP}] TerminatingTLS:<nil> OriginatingTLS:<nil> ServerNames:[] Listener:<nil> Rules:<nil>}] ICMPs:[] Authentication:<nil>} {IngressCommonRule:{FromEndpoints:[] FromRequires:[] FromCIDR: FromCIDRSet:[] FromEntities:[] aggregatedSelectors:[]} ToPorts:[] ICMPs:[{Fields:[{Family:IPv4 Type:8}]}] Authentication:<nil>}] IngressDeny:[] Egress:[] EgressDeny:[] Labels:[k8s:io.cilium.k8s.policy.derived-from=CiliumClusterwideNetworkPolicy k8s:io.cilium.k8s.policy.name=demo-host-policy k8s:io.cilium.k8s.policy.uid=8159e55d-5913-4b5f-84ce-977e83167e1b] Description:}]" policyAddRequest=0402e088-a4e0-467a-b74b-5e759ff4923e subsys=daemon
level=debug msg="Policy imported via API, found CIDR prefixes..." policyAddRequest=0402e088-a4e0-467a-b74b-5e759ff4923e prefixes="[]" subsys=daemon
level=info msg="Policy imported via API, recalculating..." policyAddRequest=0402e088-a4e0-467a-b74b-5e759ff4923e policyRevision=2 subsys=daemon
level=debug msg="EventQueue event processing statistics" eventConsumeOffQueueWaitTime="16.816µs" eventEnqueueWaitTime="4.076µs" eventHandlingDuration="247.426µs" eventType="*cmd.PolicyAddEvent" name=repository-change-queue subsys=eventqueue
level=info msg="Imported CiliumNetworkPolicy" ciliumNetworkPolicyName=demo-host-policy k8sApiVersion= k8sNamespace= subsys=k8s-watcher

测试过程中发现,这个 Host Firewall 的策略并不生效,正常来说,集群内的节点可以正常访问 node1 的 80 端口的 http 服务,而集群外的机器是不能的,实验结果表示设置了 --device=bond0,导致策略不生效。由于 eBPF 加载到 bond0,因此流量从 bond0 进入后,因为没有相关的 ip 等信息,因此流量是完全放通的,进入到主机网络栈后,bond0 到 bond0.1000,然后 bond0.1000 上没有 eBPF 程序,因此这个 Host Firewall 就无效了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cil_to_netdev bpf/bpf_host.c
|-ENABLE_HOST_FIREWALL
  |-ENABLE_IPV4
    |-handle_to_netdev_ipv4
      |-resolve_srcid_ipv4
        |-ipv4_host_policy_egress
          |-__ipv4_host_policy_egress
            |-lookup_ip4_remote_endpoint
              |-policy_can_egress4
                |-__policy_can_access

总结

Cilium Host Firewall 可以在 cilium-chain 模式下以及启动 kubeProxyReplacement 的条件下单独开通。

关于能否将 Host Firewall 的 eBPF 程序挂载到 bond0 上,上面的测试过程显示,虽然可以做到修改 eBPF 程序的绑定,但是会导致主机网络策略实际起不到效果,因此后续生产环境部署的时候,可以考虑将 Host Firewall 相关的 eBPF 程序挂载到 bond0.501 子接口上。

参考资料

  1. Cilium host firewall
  2. Cilium的Host Firewall功能
警告
本文最后更新于 2023年11月12日,文中内容可能已过时,请谨慎参考。