土法炼钢兴趣小组的算法知识备份

Network Policy 入门到精通:不只是 YAML 游戏

目录

你把一堆微服务扔进 Kubernetes 集群,每个 Pod 之间网络全通。某天安全团队过来问:「前端 Pod 为什么能直接访问数据库?」你一看,确实 – Kubernetes 的默认网络模型就是 全放行,任意 Pod 可以和集群内任意 Pod 通信,不需要任何授权。

这就是 NetworkPolicy 存在的意义:在 Kubernetes 原生的扁平网络之上加一层 白名单防火墙,控制哪些流量可以进出 Pod。听起来简单,但实际用起来有很多坑 – 语义不直观、默认行为容易搞混、原生 API 能力有限、底层实现因 CNI 而异。

这篇文章会从规范层面讲清楚 NetworkPolicy 的每一个字段,然后深入 Calico 和 Cilium 两种实现的底层机制,再给出生产可用的策略模式,最后用一组微服务实验验证隔离效果。

本文基于 Kubernetes 1.30,Calico v3.28,Cilium v1.15。实验环境:Ubuntu 22.04, kernel 6.5。


一、Kubernetes 网络模型的安全缺失

K8s 网络模型里我们讲过,Kubernetes 对网络的核心要求是:每个 Pod 有独立 IP,Pod 之间可以直接通信不经 NAT,Node 与 Pod 之间可以直接通信。注意这三条规则里 没有任何关于「访问控制」的要求 – 只管「能不能到」,不管「该不该到」。

这意味着前端 Pod 可以直连数据库的 3306 端口,被攻破的 Pod 可以横向扫描整个集群网络。在传统网络架构里用 VLAN 隔离和防火墙规则解决的问题,在 Kubernetes 里对应的原语就是 NetworkPolicy


二、NetworkPolicy 规范详解

NetworkPolicy 是 networking.k8s.io/v1 里的资源,核心思想:用 podSelector 选中一组 Pod,声明允许哪些入站/出站流量,没被规则显式放行的流量默认拒绝。

完整结构

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      role: api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              role: frontend
        - namespaceSelector:
            matchLabels:
              env: production
      ports:
        - protocol: TCP
          port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              role: database
      ports:
        - protocol: TCP
          port: 5432

podSelector:策略目标

spec.podSelector 决定策略应用到哪些 Pod,是标准的 metav1.LabelSelector,只在策略所在 Namespace 内生效。空的 podSelector: {} 选中当前 Namespace 的所有 Pod – 这是写「默认拒绝」策略的关键用法。

policyTypes:方向声明

policyTypes 声明策略管控哪个方向的流量。容易踩坑的隐式行为:如果不写 policyTypes,Kubernetes 会根据是否定义了 ingress/egress 字段自动推断。如果只写 policyTypes: [Egress] 但没定义 egress 字段,效果是拒绝所有出站流量。最佳实践:永远显式写 policyTypes

OR 与 AND 语义陷阱

from 数组内的元素之间是 OR 关系,但同一个元素内同时写 podSelectornamespaceSelectorAND 关系:

# OR 语义:frontend Pod 或 production namespace 的任意 Pod
from:
  - podSelector:
      matchLabels:
        role: frontend
  - namespaceSelector:
      matchLabels:
        env: production

# AND 语义:production namespace 内的 frontend Pod
from:
  - podSelector:
      matchLabels:
        role: frontend
    namespaceSelector:
      matchLabels:
        env: production

这是 NetworkPolicy 最臭名昭著的坑。 一个 - 的差异导致语义完全不同,OR 语义可能意外放行整个 Namespace 的所有 Pod。

ipBlock:CIDR 选择器

除了 podSelectornamespaceSelector,还可以用 ipBlock 按 CIDR 选择外部 IP。注意 ipBlock 不会匹配 Pod IP,Pod 间通信应该用 podSelector

规则匹配流程

NetworkPolicy 规则匹配流程

核心要点:

  1. 没有任何 Policy 选中的 Pod:默认全放行
  2. 被 Policy 选中后,对应方向的默认行为变成拒绝 – 只有显式匹配的规则才放行
  3. 多个 Policy 选中同一 Pod 时,所有规则做并集(union)
  4. NetworkPolicy 是纯白名单模型,没有 deny 规则

三、默认行为的精确语义

这一节专门讲清楚「默认放行」和「默认拒绝」的精确语义。

Pod 没被任何 Policy 选中:入站出站全部放行。Policy 只声明 Ingress:入站按规则白名单,出站仍然全放行。默认拒绝所有入站

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress

podSelector: {} 选中所有 Pod,policyTypes: [Ingress] 管控入站,ingress 字段未定义 – 没有放行规则,所有入站流量被拒绝。

零信任起点 – 默认拒绝所有出入站

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

四、底层实现:Calico 的 iptables 路径

关于 Calico 的整体架构可以参考Calico 那篇,这里只聚焦策略引擎 Felix 的工作流程。

Felix 的策略处理流水线

K8s API --> Felix watch --> Policy 解析 --> 规则计算 --> iptables 编程 + ipset 更新

Felix 从 Kubernetes API watch NetworkPolicy 变更,解析 podSelector 确定受影响的 workload endpoint(每个 Pod 的 veth 接口),计算需要插入的 iptables 规则,把 selector 匹配的 Pod IP 维护为 ipset。

iptables 链结构

Calico 在 filter 表创建了精心设计的链结构:

# 查看 Calico 创建的 iptables 链
sudo iptables -t filter -L -n --line-numbers | grep -i cali

# 主链结构
# cali-FORWARD -> cali-from-wl-dispatch (egress)
#              -> cali-to-wl-dispatch   (ingress)
# 每个 endpoint 有自己的链
# cali-tw-cali1234abc  (to-workload: ingress)
# cali-fw-cali1234abc  (from-workload: egress)

一个 endpoint 的 ingress 链示例:

Chain cali-tw-cali1234abc (1 references)
num  target     prot opt source    destination
1    MARK       all  --  0.0.0.0/0  0.0.0.0/0  MARK and 0xfffff
2    cali-pri-knp.default.allow-fe  all  -- 0.0.0.0/0  0.0.0.0/0
3    RETURN     all  --  0.0.0.0/0  0.0.0.0/0  mark match 0x10000/0x10000
4    DROP       all  --  0.0.0.0/0  0.0.0.0/0

逻辑:清除标记 -> 跳转策略链 -> 匹配则 RETURN 放行 -> 不匹配则 DROP。

ipset 的作用

Felix 把匹配的 Pod IP 集合放进 ipset,iptables 规则通过 match-set 引用:

# 示例 ipset
Name: cali40s:knp-default-allow-frontend-0
Type: hash:net
Members:
10.244.1.15
10.244.2.23

ipset 提供 O(1) 集合查找,不管集合内有 10 个还是 10000 个 IP,查找时间一样。Felix 做增量更新 – Pod 创建/删除时只修改 ipset,不重写 iptables 链。


五、底层实现:Cilium 的 eBPF 路径

Cilium 完全不用 iptables,把策略逻辑编译成 eBPF 程序挂载到 Pod 的 veth 上。详见Cilium 那篇

Identity-Based 策略模型

Cilium 基于 identity(安全身份) 而非 IP 地址。每组相同标签的 Pod 被分配一个数字 identity ID:

cilium identity list
# IDENTITY   LABELS
# 12345      k8s:app=frontend,k8s:io.kubernetes.pod.namespace=production
# 12346      k8s:app=api,k8s:io.kubernetes.pod.namespace=production

Policy Map 结构

每个 endpoint 有一个 eBPF policy map,key 是 (direction, identity, port, protocol) 四元组:

cilium bpf policy get 1234
# DIRECTION  IDENTITY  PORT/PROTO  ENTRY
# Ingress    12345     8080/TCP    ALLOW
# Ingress    0         0/ANY       DENY (default)
# Egress     12347     5432/TCP    ALLOW
# Egress     0         53/UDP      ALLOW

eBPF 程序在 tc hook 上查这个 map,O(1) 时间复杂度。更新策略时用 bpf_map_update_elem 原子操作,不需要像 iptables 那样获取全局锁。

与 iptables 方案的对比

维度 Calico (iptables) Cilium (eBPF)
匹配方式 iptables 链 + ipset BPF policy map 查表
更新开销 修改规则需要锁 原子 map 操作
规则数量影响 链越长性能越差 map 大小对查找无影响
调试手段 iptables -L, iptables-save cilium bpf policy, cilium monitor

六、常见策略模式

模式一:默认拒绝 + DNS 放行 + 白名单

先拒绝所有流量,放行 DNS,再为每个合法通信路径创建白名单:

# 第一步:默认拒绝
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
# 第二步:放行 DNS(否则所有 DNS 解析失败)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector: {}
      ports:
        - { protocol: UDP, port: 53 }
        - { protocol: TCP, port: 53 }
---
# 第三步:精确白名单
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-to-api
  namespace: production
spec:
  podSelector:
    matchLabels: { app: api }
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels: { app: frontend }
      ports:
        - { protocol: TCP, port: 8080 }

模式二:Namespace 隔离

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-cross-namespace
  namespace: team-a
spec:
  podSelector: {}
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector: {}

只有 podSelector: {}(没有 namespaceSelector),只匹配本 Namespace 内的 Pod。

模式三:允许 Ingress Controller

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-controller
  namespace: production
spec:
  podSelector:
    matchLabels: { app: api }
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
          podSelector:
            matchLabels:
              app.kubernetes.io/name: ingress-nginx
      ports:
        - { protocol: TCP, port: 8080 }

namespaceSelectorpodSelector 在同一个 from 元素里 – AND 语义。

模式四:限制出站到内网 CIDR

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: egress-internal-only
  namespace: production
spec:
  podSelector:
    matchLabels: { app: backend }
  policyTypes: [Egress]
  egress:
    - to:
        - ipBlock: { cidr: 10.0.0.0/8 }
        - ipBlock: { cidr: 172.16.0.0/12 }
        - ipBlock: { cidr: 192.168.0.0/16 }
    - to: [{ namespaceSelector: {} }]
      ports: [{ protocol: UDP, port: 53 }]

七、NetworkPolicy 的局限

Kubernetes 原生 NetworkPolicy 的能力是有限的:

  1. 没有全局默认拒绝:策略是 Namespace 粒度,新建 Namespace 如果忘了添加默认拒绝就处于全放行状态
  2. 不支持 FQDN Egress:不能写「只允许访问 api.stripe.com」,egress.to 只支持 IP/CIDR
  3. 不支持 L7 规则:只能控制 L3/L4(IP + 端口),不能按 HTTP 方法、路径或 gRPC 方法过滤
  4. 不支持 Deny 规则:纯白名单模型,不能「拒绝来自某个 Pod 的流量但允许其他所有」
  5. 没有日志审计:无法知道哪条策略导致流量被拒绝
  6. 无法选中 Node 或 Servicefrom/to 只支持 Pod label 和 IP CIDR

八、CRD 扩展:突破原生限制

Calico NetworkPolicy

Calico 自定义 NetworkPolicyprojectcalico.org/v3)增加了 Deny 规则、规则排序、GlobalNetworkPolicy:

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: default-deny-all
spec:
  selector: all()
  types: [Ingress, Egress]
  ingress:
    - action: Deny
  egress:
    - action: Deny

一条策略覆盖整个集群 – 解决了原生 NetworkPolicy 没有全局默认拒绝的问题。

Cilium NetworkPolicy

Cilium 的 CiliumNetworkPolicycilium.io/v2)支持 L7 规则:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: l7-api-policy
  namespace: production
spec:
  endpointSelector:
    matchLabels: { app: api }
  ingress:
    - fromEndpoints:
        - matchLabels: { app: frontend }
      toPorts:
        - ports: [{ port: "8080", protocol: TCP }]
          rules:
            http:
              - method: GET
                path: "/api/v1/.*"
              - method: POST
                path: "/api/v1/orders"

只允许 frontend 用 GET 访问 /api/v1/*,或用 POST 创建订单。

Cilium DNS-Based Egress

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: allow-external-api
  namespace: production
spec:
  endpointSelector:
    matchLabels: { app: payment }
  egress:
    - toEndpoints:
        - matchLabels:
            io.kubernetes.pod.namespace: kube-system
            k8s-app: kube-dns
      toPorts:
        - ports: [{ port: "53", protocol: ANY }]
          rules:
            dns:
              - matchPattern: "*.stripe.com"
    - toFQDNs:
        - matchPattern: "*.stripe.com"
      toPorts:
        - ports: [{ port: "443", protocol: TCP }]

Cilium DNS proxy 拦截 DNS 查询,只允许 *.stripe.com,把解析出的 IP 动态添加到 egress 放行列表 – 解决了域名 IP 会变的问题。

AdminNetworkPolicy (KEP-2091)

Kubernetes 社区正在推进的原生集群级方案(policy.networking.k8s.io/v1alpha1):

apiVersion: policy.networking.k8s.io/v1alpha1
kind: AdminNetworkPolicy
metadata:
  name: cluster-baseline
spec:
  priority: 100
  subject:
    namespaces:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: [kube-system, kube-node-lease]
  ingress:
    - name: "deny-all-ingress"
      action: Deny
      from:
        - pods:
            namespaces: {}
            podSelector: {}
  egress:
    - name: "allow-dns"
      action: Allow
      to:
        - pods:
            namespaces:
              matchLabels:
                kubernetes.io/metadata.name: kube-system
            podSelector:
              matchLabels: { k8s-app: kube-dns }
      ports:
        - portNumber: { port: 53, protocol: UDP }
    - name: "deny-all-egress"
      action: Deny
      to:
        - pods:
            namespaces: {}
            podSelector: {}

核心设计:集群粒度、priority 字段解决多策略冲突、三种 action(Allow / Deny / Pass),其中 Pass 表示交给 Namespace 级 NetworkPolicy 决定。


九、实验:微服务隔离验证

部署三个微服务

kubectl create namespace netpol-lab
kubectl label namespace netpol-lab env=lab
# lab-deployments.yaml
---
apiVersion: v1
kind: Pod
metadata:
  name: frontend
  namespace: netpol-lab
  labels: { app: frontend, tier: web }
spec:
  containers:
    - name: app
      image: nicolaka/netshoot:latest
      command: ["python3", "-m", "http.server", "80"]
---
apiVersion: v1
kind: Pod
metadata:
  name: api
  namespace: netpol-lab
  labels: { app: api, tier: backend }
spec:
  containers:
    - name: app
      image: nicolaka/netshoot:latest
      command: ["python3", "-m", "http.server", "8080"]
---
apiVersion: v1
kind: Pod
metadata:
  name: database
  namespace: netpol-lab
  labels: { app: database, tier: data }
spec:
  containers:
    - name: app
      image: nicolaka/netshoot:latest
      command: ["python3", "-m", "http.server", "5432"]
kubectl apply -f lab-deployments.yaml
kubectl -n netpol-lab wait --for=condition=Ready pod --all --timeout=60s
kubectl -n netpol-lab get pods -o wide
NAME       READY   STATUS    IP            NODE
frontend   1/1     Running   10.244.1.10   node-1
api        1/1     Running   10.244.2.20   node-2
database   1/1     Running   10.244.1.30   node-1

验证一:默认全放行

# frontend -> api (成功)
kubectl -n netpol-lab exec frontend -- curl -s --connect-timeout 3 http://10.244.2.20:8080
# 输出: <!DOCTYPE html>...

# frontend -> database (成功)
kubectl -n netpol-lab exec frontend -- curl -s --connect-timeout 3 http://10.244.1.30:5432
# 输出: <!DOCTYPE html>...

验证二:默认拒绝所有

# default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: netpol-lab
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
kubectl apply -f default-deny.yaml

# frontend -> api (超时)
kubectl -n netpol-lab exec frontend -- curl -s --connect-timeout 3 http://10.244.2.20:8080
# 预期: curl: (28) Connection timed out

# database -> frontend (超时)
kubectl -n netpol-lab exec database -- curl -s --connect-timeout 3 http://10.244.1.10:80
# 预期: curl: (28) Connection timed out

验证三:白名单放行 frontend -> api

# allow-frontend-to-api.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-egress-to-api
  namespace: netpol-lab
spec:
  podSelector:
    matchLabels: { app: frontend }
  policyTypes: [Egress]
  egress:
    - to:
        - podSelector:
            matchLabels: { app: api }
      ports: [{ protocol: TCP, port: 8080 }]
    - to: [{ namespaceSelector: {} }]
      ports: [{ protocol: UDP, port: 53 }]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-ingress-from-frontend
  namespace: netpol-lab
spec:
  podSelector:
    matchLabels: { app: api }
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels: { app: frontend }
      ports: [{ protocol: TCP, port: 8080 }]
kubectl apply -f allow-frontend-to-api.yaml

# frontend -> api:8080 (成功)
kubectl -n netpol-lab exec frontend -- curl -s --connect-timeout 3 http://10.244.2.20:8080
# 输出: <!DOCTYPE html>...

# frontend -> database:5432 (拒绝 -- egress 没放行)
kubectl -n netpol-lab exec frontend -- curl -s --connect-timeout 3 http://10.244.1.30:5432
# 预期: curl: (28) Connection timed out

# database -> api:8080 (拒绝 -- ingress 只允许 frontend)
kubectl -n netpol-lab exec database -- curl -s --connect-timeout 3 http://10.244.2.20:8080
# 预期: curl: (28) Connection timed out

验证四:继续放行 api -> database

# allow-api-to-database.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-egress-to-db
  namespace: netpol-lab
spec:
  podSelector:
    matchLabels: { app: api }
  policyTypes: [Egress]
  egress:
    - to:
        - podSelector:
            matchLabels: { app: database }
      ports: [{ protocol: TCP, port: 5432 }]
    - to: [{ namespaceSelector: {} }]
      ports: [{ protocol: UDP, port: 53 }]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-db-ingress-from-api
  namespace: netpol-lab
spec:
  podSelector:
    matchLabels: { app: database }
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels: { app: api }
      ports: [{ protocol: TCP, port: 5432 }]
kubectl apply -f allow-api-to-database.yaml

# api -> database:5432 (成功)
kubectl -n netpol-lab exec api -- curl -s --connect-timeout 3 http://10.244.1.30:5432
# 输出: <!DOCTYPE html>...

# frontend -> database:5432 (仍然拒绝)
kubectl -n netpol-lab exec frontend -- curl -s --connect-timeout 3 http://10.244.1.30:5432
# 预期: curl: (28) Connection timed out

通信拓扑:frontend --> api --> database。前端不能直连数据库 – 三层架构隔离。

查看底层规则

# Calico: 查看 iptables 链
sudo iptables -t filter -L | grep cali-tw

# Cilium: 查看 policy map
cilium endpoint list | grep api
cilium monitor --type policy-verdict -n netpol-lab
# 清理
kubectl delete namespace netpol-lab

十、生产最佳实践

永远从默认拒绝开始。 用准入控制器(Kyverno / OPA Gatekeeper)自动为新建 Namespace 注入 default-deny-all 策略。

永远放行 DNS。 默认拒绝 Egress 后第一条白名单必须是 DNS,否则所有 Service 名称解析失败。

双向策略。 同时配置源端 Egress 和目标端 Ingress。只配一边不够 – 如果目标 Pod 的 Ingress 被其他策略拒绝,光放行源端 Egress 也没用。

标签规范。 为 Pod 和 Namespace 建立统一标签体系(apptierenvteam),这是策略可维护性的基础。

策略测试。 CI/CD 流水线里用工具验证:

# 可视化当前策略
kubectl np-viewer -n production

# 端到端连通性测试
cyclonus generate --include-namespaces=production

十一、总结

  1. 默认行为:Kubernetes 默认全放行,NetworkPolicy 为被选中的 Pod 切换到白名单模式
  2. 规则语义from 数组内是 OR,同一元素内 podSelector + namespaceSelector 是 AND;多个 Policy 取并集
  3. Calico 实现:iptables 链 + ipset,O(1) 集合查找,增量更新
  4. Cilium 实现:identity-based eBPF policy map,O(1) map 查找,原子更新
  5. 策略模式:默认拒绝 + DNS 放行 + 精确白名单,是零信任基础范式
  6. 原生局限:不支持 FQDN egress、L7 规则、全局策略、deny 规则
  7. CRD 扩展:Calico GlobalNetworkPolicy、Cilium L7/FQDN 策略、AdminNetworkPolicy (KEP-2091)

NetworkPolicy 不只是写几行 YAML。理解默认行为、底层实现和局限性,才能在生产环境构建真正可靠的网络隔离。

下一篇我们会深入 Cilium Identity 与安全模型 – 看看 Cilium 如何用 identity 替代 IP 地址来实现更高效的策略评估。


参考资料


By .