jimyag's Blog

Kubernetes NetworkPolicy:入站与出站隔离、策略叠加及 DNS 放行

NetworkPolicy 已经创建成功,客户端却仍然能访问服务;把出站流量也限制起来后,直接访问 Pod IP 成功,通过服务名访问却失败。仅检查 YAML 是否提交成功,解释不了这些现象。

本文在 zj 的 kind 集群中使用 Calico,依次验证默认访问、入站隔离、白名单、出站隔离和 DNS 放行。每一步都发起新的 HTTP 请求,用实际访问结果判断策略是否生效。

策略对象和策略执行不是一回事

Kubernetes API 接受 NetworkPolicy 对象,实际网络隔离需要支持该能力的网络实现执行。本次使用 Calico v3.31.2,集群配置关闭默认 kindnet。只在 API 中看到策略,不能证明当前 CNI 已经实施它。官方 NetworkPolicy 文档说明了这种依赖。

实验环境为 kind v0.33.0、Kubernetes v1.35.1,在 dd-network-policy namespace 中创建三个 Pod:server 在 8080 端口提供 HTTP 服务,good 和 other 作为两个客户端。另有一个 Service 指向 server。

没有隔离策略时,两边都能访问

第一步不给三个 Pod 配任何 NetworkPolicy。脚本从 good 和 other 分别请求 server 的 Pod IP,两个请求都成功。

这一步很重要:如果基线已经失败,后续超时可能来自 DNS、Service、应用监听或路由,不能直接归因于刚加入的策略。每次故障实验都应先建立可工作的对照。

一个空入站规则会改变什么

然后对 server 创建入站隔离策略:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 选中 server 并启用入站隔离,但没有允许任何入站来源。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-server
spec:
  podSelector:
    matchLabels:
      app: server
  policyTypes:
    - Ingress # 此策略只约束进入 server 的流量。

策略应用后,脚本持续尝试新连接,直到 good 的请求失败。这种基于条件的等待避免把“API 已保存”误认为“数据面已经同步完成”。

这里的 podSelector 选的是受保护的 Pod,不是访问者。只写 app: good 会变成隔离 good 的入站流量,并不会表达“只允许 good 访问 server”。

再加允许策略,为什么没有被先前的拒绝覆盖

第二份策略同样选中 server,允许来自 app=good 的 TCP 8080 流量。实验观察到 good 恢复访问,other 仍然失败,good 通过 Service ClusterIP 也能访问。

对同一个 Pod 和方向,多份标准 NetworkPolicy 的允许范围取并集。deny-server 选中 server 并声明 Ingress,使它进入入站隔离;它没有提供允许来源。allow-good 又选中同一 server,贡献了 good 到 TCP 8080 的允许范围。最终可访问集合由全部选中策略共同决定,创建顺序不参与优先级计算。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 以下是 allow-good 的 spec;来源和目标端口在同一条入站规则内。
podSelector:
  matchLabels:
    app: server
policyTypes: [Ingress]
ingress:
  - from:
      - podSelector:
          matchLabels:
            app: good # 这个 selector 默认匹配同 namespace 的来源 Pod。
    ports:
      - protocol: TCP
        port: 8080 # 目标服务实际监听的端口。

Calico 还有自己的策略 CRD 和其他能力,本文只验证 Kubernetes networking.k8s.io/v1 NetworkPolicy,不能把这里的合并规则扩展到所有厂商策略接口。

server 允许入站,为什么 good 还是访问不了

实验接着给 good 添加空的 Egress 策略。虽然 server 的白名单完全没变,good 的请求仍然失败。

源和目标各自计算允许集合:源未被 Egress 隔离,或源的某条出站规则允许;同时,目标未被 Ingress 隔离,或目标的某条入站规则允许。两端条件都满足,新连接才具备策略上的放行条件。good 被空 Egress 策略选中后,第一项不再满足;增加到 server TCP 8080 的出站允许,Pod IP 请求才恢复。

每条规则内部也有组合关系:多个 from 或 to 元素提供可选来源或目的地;同一个元素中的 namespaceSelector 与 podSelector 则同时约束同一组 Pod。把两个 selector 写在同一元素,和拆成两个列表元素,会得到不同的允许范围。下面的 DNS 配置使用前一种写法。

这一组对照可以排除“server 的 ingress 规则失效”这个误判。面对连接超时,需要同时检查源 Pod 的 egress 和目标 Pod 的 ingress,而不是只在服务端不断添加允许规则。

Pod IP 能通,服务名为什么不通

允许 good 访问 server 后,脚本继续用短服务名 server 发请求,仍然失败。因为请求需要先完成 DNS 查询,而 good 此时没有到 DNS 服务的出站允许。

随后添加一条规则,同时选择 kube-system namespace 和 k8s-app=kube-dns 的 Pod,放行 UDP/TCP 53。服务名请求恢复成功。配置中的两个 selector 放在同一个 to 元素内,要求它们同时满足:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 这是 DNS 出站允许规则的 egress 部分。
egress:
  - to:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: kube-system
        podSelector:
          matchLabels:
            k8s-app: kube-dns # 与 namespaceSelector 共同约束目的 Pod。
    ports:
      - protocol: UDP
        port: 53
      - protocol: TCP
        port: 53

真实集群应先确认 DNS 流量实际发往哪里。启用 NodeLocal DNSCache、使用自定义 DNS 或不同网络实现时,不能直接照抄这组目标 selector。

标签选择器怎样变成包过滤条件

数据包里没有 Pod label。执行策略的组件必须先把“哪些 Pod 匹配选择器”转换为数据面可以匹配的地址、端口和方向,再随着 Pod 创建、删除、地址变化更新规则。API Server 保存 NetworkPolicy,只完成了这条链路的输入部分。

以实验使用的 Calico v3.31.2 为例,Felix 的 RuleScanner把选择器相关条件转换成 IP set 标识,记录规则引用哪些集合,并通知集合何时开始或停止被使用。在其 iptables 规则生成路径中,规则可以引用这些集合,而不必为每个匹配 Pod 展开一份完整策略。这样,成员变化与策略结构变化就能分别处理;这是理解 label 更新为什么也会影响连通性的关键。

不过,API 对象写入成功和节点完成数据面更新仍然隔着观察、计算与下发。本文脚本等待连通性达到预期后才记录结果,不用 apply 成功作为策略已经生效的证据。实验验证的是标准 NetworkPolicy 的访问语义,没有抓取 iptables 规则,也没有比较 Calico 的 eBPF 数据面;上面的实现分析限定在对应源码路径。

标准 NetworkPolicy 采用允许规则并集,也使“默认拒绝”可以和业务允许策略分开维护。空规则策略让被选中的 Pod 进入隔离状态,却不会创建一条能够压过后续允许规则的全局 deny。更换规则顺序无法解决选错 Pod 或漏掉 DNS 的问题,必须检查选择器实际匹配到谁、两个方向分别允许什么。

把访问结果放回同一张表

实验阶段发起的访问检查实测结果
没有策略good 和 other → server Pod IP都成功
server 入站隔离good → server Pod IP失败
加入 good 白名单good、other → Pod IP;good → ClusterIPgood 成功,other 失败;ClusterIP 成功
good 出站隔离good → server Pod IP失败
允许 good 到 servergood → Pod IP 和服务名Pod IP 成功,服务名失败
再允许 DNSgood → 服务名成功

表格只列脚本实际检查的结果。它没有测量已建立连接在策略更新后何时被切断,也没有覆盖所有 CNI 的 Service NAT 实现;故障定位时应保留客户端、目的地址和连接是否新建这些条件。

实验代码与使用说明见 k8sdev/deep-dive-network-policy。

#Kubernetes #Kind #Troubleshooting