你把一堆微服务扔进 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: 5432podSelector:策略目标
spec.podSelector 决定策略应用到哪些
Pod,是标准的
metav1.LabelSelector,只在策略所在 Namespace
内生效。空的 podSelector: {} 选中当前 Namespace
的所有 Pod – 这是写「默认拒绝」策略的关键用法。
policyTypes:方向声明
policyTypes
声明策略管控哪个方向的流量。容易踩坑的隐式行为:如果不写
policyTypes,Kubernetes 会根据是否定义了
ingress/egress
字段自动推断。如果只写 policyTypes: [Egress]
但没定义 egress
字段,效果是拒绝所有出站流量。最佳实践:永远显式写
policyTypes。
OR 与 AND 语义陷阱
from 数组内的元素之间是 OR
关系,但同一个元素内同时写 podSelector 和
namespaceSelector 是 AND
关系:
# 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 选择器
除了 podSelector 和
namespaceSelector,还可以用
ipBlock 按 CIDR 选择外部 IP。注意
ipBlock 不会匹配 Pod IP,Pod 间通信应该用
podSelector。
规则匹配流程
核心要点:
- 没有任何 Policy 选中的 Pod:默认全放行
- 被 Policy 选中后,对应方向的默认行为变成拒绝 – 只有显式匹配的规则才放行
- 多个 Policy 选中同一 Pod 时,所有规则做并集(union)
- NetworkPolicy 是纯白名单模型,没有 deny 规则
三、默认行为的精确语义
这一节专门讲清楚「默认放行」和「默认拒绝」的精确语义。
Pod 没被任何 Policy 选中:入站出站全部放行。Policy 只声明 Ingress:入站按规则白名单,出站仍然全放行。默认拒绝所有入站:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {}
policyTypes:
- IngresspodSelector: {} 选中所有
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.23ipset 提供 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=productionPolicy 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 ALLOWeBPF 程序在 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 }namespaceSelector 和
podSelector 在同一个 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 的能力是有限的:
- 没有全局默认拒绝:策略是 Namespace 粒度,新建 Namespace 如果忘了添加默认拒绝就处于全放行状态
- 不支持 FQDN Egress:不能写「只允许访问
api.stripe.com」,
egress.to只支持 IP/CIDR - 不支持 L7 规则:只能控制 L3/L4(IP + 端口),不能按 HTTP 方法、路径或 gRPC 方法过滤
- 不支持 Deny 规则:纯白名单模型,不能「拒绝来自某个 Pod 的流量但允许其他所有」
- 没有日志审计:无法知道哪条策略导致流量被拒绝
- 无法选中 Node 或
Service:
from/to只支持 Pod label 和 IP CIDR
八、CRD 扩展:突破原生限制
Calico NetworkPolicy
Calico 自定义
NetworkPolicy(projectcalico.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 的
CiliumNetworkPolicy(cilium.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 wideNAME 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
建立统一标签体系(app、tier、env、team),这是策略可维护性的基础。
策略测试。 CI/CD 流水线里用工具验证:
# 可视化当前策略
kubectl np-viewer -n production
# 端到端连通性测试
cyclonus generate --include-namespaces=production十一、总结
- 默认行为:Kubernetes 默认全放行,NetworkPolicy 为被选中的 Pod 切换到白名单模式
- 规则语义:
from数组内是 OR,同一元素内 podSelector + namespaceSelector 是 AND;多个 Policy 取并集- Calico 实现:iptables 链 + ipset,O(1) 集合查找,增量更新
- Cilium 实现:identity-based eBPF policy map,O(1) map 查找,原子更新
- 策略模式:默认拒绝 + DNS 放行 + 精确白名单,是零信任基础范式
- 原生局限:不支持 FQDN egress、L7 规则、全局策略、deny 规则
- CRD 扩展:Calico GlobalNetworkPolicy、Cilium L7/FQDN 策略、AdminNetworkPolicy (KEP-2091)
NetworkPolicy 不只是写几行 YAML。理解默认行为、底层实现和局限性,才能在生产环境构建真正可靠的网络隔离。
下一篇我们会深入 Cilium Identity 与安全模型 – 看看 Cilium 如何用 identity 替代 IP 地址来实现更高效的策略评估。