Kubernetes 网络策略:Pod 间防火墙
K8s 默认所有 Pod 互通——这在生产是安全隐患。NetworkPolicy 让你声明"哪些 Pod 能和哪些 Pod 通信"。本文讲清 NetworkPolicy 的 ingress/egress 规则、默认拒绝模式、以及"零信任"的 K8s 网络实践。
本文要点
- 主题聚焦:DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
Kubernetes 网络策略:Pod 间防火墙
讲一个我亲历的故障。某团队一个支付服务跑在 K8s 上,被一个跑在同一集群的内部报表脚本"误伤"——脚本遍历了所有 *:5432 端口扫数据,正好打到了支付服务的旁路数据库,把连接池占满,支付接口 502 了十分钟。事后复盘第一句话是:"不是同一个 namespace 吗?为什么它还能连过来?"
这是 K8s 新手最普遍的认知误区之一:namespace 不是网络边界,Pod 之间默认全互通。任何一个 Pod,只要知道 IP,就能 curl 集群里任何另一个 Pod 的任何端口。集群里的某个被攻破的 Pod,等于拿到了内网通行证。
让网络收敛到"最小必要连通"的工具,就是 NetworkPolicy。本文讲清它的规则模型、四种匹配维度、默认拒绝的零信任姿势,并带你为一条 前端 → 后端:8080 → DB:5432 的调用链配上完整的策略。读完你会知道:为什么光写 yaml 还不够,还得挑对 CNI。
一、K8s 默认网络模型:Pod 全互通这个"特性"是隐患
先确立心智模型。K8s 的网络模型有一条著名的"四规则",其中最关键的一条是:
K8s 不做端口冲突隔离,任何 Pod 可以用任何协议访问任何其他 Pod 的任何端口,NAT 不拦截。
实现上,K8s 给每个 Pod 分配一个扁平的、全网唯一的 IP(RFC 1918 私网段),所有 Pod 在同一个 L2/L3 overlay 里。kube-proxy 只负责"Service 到 Pod"的负载均衡,不负责"Pod 到 Pod"的访问控制。
┌──────────────────────────────────────┐
│ K8s 集群扁平网络 (10.244.0.0/16) │
│ │
│ podA ──→ podB podC ──→ podD │
│ 10.0.1 10.0.2 10.0.3 10.0.4
│ │
│ 默认:谁都能连谁,任何端口。零控制。 │
└──────────────────────────────────────┘
这意味着几件危险的事:
- 一个被攻陷的 Pod(比如有 RCE 漏洞的 web 服务)可以直接扫内网、连数据库、连 etcd(如果它没做好 mTLS)
- 一个出 bug 的脚本可以打爆任意服务的连接池
- "我有三个 namespace(dev/staging/prod)所以是隔离的" —— 不是,dev 里的 Pod 能直连 prod 的 DB
- 横向移动(lateral movement)几乎零阻力
namespace 只是个逻辑分组(配合 RBAC、资源配额有用),不是网络隔离。把 prod 和 dev 放一起还指望它俩网络不通,跟把两个房间贴不同门牌就指望它们物理隔音一样天真。
二、NetworkPolicy 是什么:声明式的 Pod 防火墙
NetworkPolicy 是 K8s 内置的 API(networking.k8s.io/v1),让你声明某个 Pod 的入站(ingress)和出站(egress)允许哪些流量。注意它是个声明,K8s 自己不执行,而是把声明下发给 CNI 插件(见第六节),由 CNI 在内核里把它翻译成 iptables / eBPF 规则。
先看一个最简单的例子,让 backend 这个 Pod 只接受 frontend Pod 在 8080 端口的访问:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
namespace: prod
spec:
podSelector:
matchLabels:
app: backend # 这条策略作用于谁(受保护方)
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend # 允许谁进来(来源)
ports:
- protocol: TCP
port: 8080 # 只允许这个端口
这五行字段是 NetworkPolicy 的全部精髓,拆开看:
| 字段 | 含义 |
|---|---|
spec.podSelector | 作用对象:这条策略保护哪些 Pod(受控方)。空 {} 表示该 namespace 内所有 Pod |
spec.policyTypes | 声明管哪些方向:Ingress / Egress / 两者 |
spec.ingress[].from | 入站规则:允许哪些来源(Pod / namespace / IP 段)。多条规则是"或"关系 |
spec.egress[].to | 出站规则:允许去往哪些目标 |
spec.ingress[].ports / spec.egress[].ports | 限定端口。省略 = 所有端口 |
隔离语义是 NetworkPolicy 最容易踩坑的点,务必记住:
一旦某个 Pod 被任何一条 NetworkPolicy 选中(通过
podSelector),并且那条策略的policyTypes包含某方向,那么这个 Pod 在该方向上就进入"隔离态"。隔离后,只允许所有选中它的策略的并集允许的流量通过。
换句话说,隔离不是"写一条策略=加一道限制",而是"写一条策略=触发白名单模式"。一旦触发,默认就是拒绝,只有显式 from/to 列出的才放行。
这条语义衍生出两种典型的写法,对应两种工程姿势:
- 针对性策略:给具体 Pod 写细粒度规则(上面的例子)
- 默认拒绝策略:给整个 namespace 写一条"全拒绝",再按需补 allow(见第四节,零信任的基础)
三、四种匹配维度:podSelector / namespaceSelector / ipBlock / ports
from 和 to 里面能放四种"选择器",组合使用决定了规则的精度。这一节把它们逐个拆透,附带最容易踩的坑。
1. podSelector —— 按 label 选 Pod
from:
- podSelector:
matchLabels:
app: frontend
坑:单独写 podSelector(不配 namespaceSelector)只匹配当前 policy 所在 namespace 的 Pod。如果想跨 namespace,必须配 namespaceSelector。
2. namespaceSelector —— 按 namespace 选整个命名空间
from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
这表示"整个 ingress-nginx namespace 的所有 Pod 都能进来"(典型用法:放行 Ingress Controller)。
坑:K8s 1.21+ 会给每个 namespace 自动打 kubernetes.io/metadata.name=<名字> 这个 label,这是按名字选 namespace 的唯一稳定方式。别手写 name: xxx——namespace 的 name 不是 label。
3. namespaceSelector + podSelector 组合 —— 跨 namespace 选具体 Pod(AND 关系)
from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: prod
podSelector:
matchLabels:
app: frontend
最关键的语义点:当 namespaceSelector 和 podSelector 写在同一个 list 元素里,它们是 AND(交集)——"prod namespace 里 label 为 app=frontend 的 Pod"。但如果写成两个 list 元素:
from:
- namespaceSelector: # 元素 1:prod 所有 Pod
matchLabels:
kubernetes.io/metadata.name: prod
- podSelector: # 元素 2:本 namespace 里 app=frontend 的 Pod
matchLabels:
app: frontend
这就是 OR(并集)了。缩进差一行,语义天差地别,这是写 NetworkPolicy 第一大翻车点。
4. ipBlock —— 按 CIDR 选 IP
to:
- ipBlock:
cidr: 10.0.0.0/8
except:
- 10.0.5.0/24 # 排除这一段
用来放行外部服务(比如托管数据库的 IP 段)或集群外客户端。坑:ipBlock 用的是 Pod 的真实 IP,不是 Service IP。Service IP(ClusterIP)是虚拟的,规则不会匹配——所以放行"访问某 Service"这件事,本质是放行它后面的 Pod,写 ipBlock 是写错地方。跨 Service 访问的正确写法是用 label 选 Pod。
5. ports —— 端口和协议
ports:
- protocol: TCP
port: 5432
- protocol: UDP
port: 53
port 可以是数字也可以是命名端口(port: http,对应容器 ports 里的名字)。省略 ports 字段 = 放行所有端口——这是第二大翻车点,你以为写得很严,其实漏了端口限制。
四、默认拒绝模式:deny-all + 按需 allow(零信任的姿势)
理解了上面两条,默认拒绝模式就很简单了。先看一份"全拒绝"策略:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: prod
spec:
podSelector: {} # 选中 prod 里所有 Pod
policyTypes:
- Ingress
- Egress
# 注意:没有 ingress / egress 字段 = 该方向全部拒绝
podSelector: {} 是"全选",没有 ingress/egress 字段是"全拒"。这一条一 apply,prod namespace 瞬间变成黑箱:所有进出的流量都被切断。
这是零信任的起点。然后你再一条条加 allow,把必要的通路打开。这个顺序很重要:
先 deny-all,再按需 allow —— 这叫白名单模式,是生产环境唯一正确的姿势。
反过来"先 allow-all,再按需 deny"是黑名单模式,理论上也能写,但 K8s NetworkPolicy 根本不支持黑名单(没有 deny 规则,只有 allow 规则)。设计上 K8s 就是要逼你走白名单。这点 Cilium 和 Calico 的扩展 CRD 才能绕过(见第六节)。
先 deny 再 allow 的最小可用补丁:放行 DNS。这是默认拒绝后第一个会坏的东西——Pod 解析不了 backend.default.svc.cluster.local,整个服务网格直接瘫。补一条:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: prod
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns # CoreDNS 的 label,因部署而异
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
凡是 apply 了 default-deny-all 的 namespace,这条 DNS 放行一定要同步加上。否则你会看到一整片 Pod 起不来、连接超时、报 i/o timeout 或 no such host,排查半天才发现是 DNS 被自己掐了。
五、实战:给 前端 → 后端:8080 → DB:5432 配一条完整链路
把前面所有东西串起来。假设 prod namespace 里有三个 Deployment,label 如下:
| 角色 | label | 监听端口 |
|---|---|---|
| frontend | app=frontend, tier=web | 3000 |
| backend | app=backend, tier=api | 8080 |
| db (postgres) | app=postgres, tier=data | 5432 |
另外 Ingress Controller 在 ingress-nginx namespace,label app.kubernetes.io/name=ingress-nginx。
期望的连通关系(只有这些):
外网 → Ingress → frontend:3000
│
▼
backend:8080
│
▼
postgres:5432
db 不能主动连任何地方,backend 不能被 frontend 以外的 Pod 访问,frontend 不能直连 db(绕过 backend 是典型越权)。第一步:apply 默认拒绝(第四节的 default-deny-all + DNS 放行)。第二步:逐层 allow。
策略 1:frontend 允许 Ingress Controller 进来
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-allow-ingress
namespace: prod
spec:
podSelector:
matchLabels: { app: frontend }
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: 3000
策略 2:frontend 允许出站到 backend:8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-egress-to-backend
namespace: prod
spec:
podSelector:
matchLabels: { app: frontend }
policyTypes: [Egress]
egress:
- to:
- podSelector:
matchLabels: { app: backend }
ports:
- protocol: TCP
port: 8080
策略 3:backend 允许 frontend 进来 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
namespace: prod
spec:
podSelector:
matchLabels: { app: backend }
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels: { app: frontend }
ports:
- protocol: TCP
port: 8080
策略 4:backend 允许出站到 postgres:5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-egress-to-db
namespace: prod
spec:
podSelector:
matchLabels: { app: backend }
policyTypes: [Egress]
egress:
- to:
- podSelector:
matchLabels: { app: postgres }
ports:
- protocol: TCP
port: 5432
策略 5:postgres 只允许 backend 进来 5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow-backend-only
namespace: prod
spec:
podSelector:
matchLabels: { app: postgres }
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels: { app: backend }
ports:
- protocol: TCP
port: 5432
注意这套配置的两个细节,都是实战血泪:
- 双向都要写。NetworkPolicy 是有方向的。frontend 出站允许 → backend:8080,但这不代表 backend 入站允许 frontend。两边的策略都得写,连接才通。
- 回包不用写。虽然双向都要写 allow,但 established 连接的反向回包不用单独 allow——CNI 都是基于连接跟踪(conntrack)有状态的,已建立连接的响应流量自动放行。否则 NetworkPolicy 写起来要疯。
apply 完后验证一下:起一个临时 Pod 模拟"恶意 Pod"(label 故意写成 app=evil),它不应该能访问 db:
kubectl run evil --image=nicolaka/netshoot -it --rm --labels=app=evil -n prod -- curl -v postgres:5432
# 应该超时,而不是连接拒绝——因为 CNI 在 SYN 阶段就 drop 了
如果它能连上,先怀疑两件事:CNI 是否支持 NetworkPolicy(见第六节),或策略里的 label 跟 Pod 实际 label 对不对得上(kubectl get pods --show-labels)。
六、CNI 支持:光写 yaml 没用,得挑对网络插件
这是 NetworkPolicy 最隐性的坑:K8s 只定义了 NetworkPolicy 这个 API 和数据结构,自己一行都不会执行。真正把这些 yaml 翻译成内核规则的是 CNI(Container Network Interface)插件。如果你的 CNI 不支持 NetworkPolicy,你 apply 这些 yaml 不会报错,但也完全不生效——这比直接拒绝更危险,因为它给了你虚假的安全感。
主流 CNI 对原生 NetworkPolicy 的支持情况:
| CNI | 原生 NetworkPolicy | 特点 |
|---|---|---|
| Calico | ✅ 完整支持 | 业界事实标准,外加扩展 CRD GlobalNetworkPolicy(跨 namespace、deny 规则、order) |
| Cilium | ✅ 完整支持 | 基于 eBPF,性能好,扩展 CiliumNetworkPolicy 支持 L7(HTTP/method/path 级别) |
| Weave Net | ✅ 支持 | 老牌方案,维护活跃度下降 |
| Antrea | ✅ 支持 | 基于 OVS,VMware 系 |
| kube-router | ✅ 支持 | 轻量 |
| Flannel | ❌ 不支持 | 最常见的"坑"——纯 Flannel 上 apply NetworkPolicy 是无效的 |
| 云厂商默认 | 看情况 | GKE 用 Calico(可选启用);EKS 用 VPC CNI(默认不支持,需装 Calico);AKS 用 Azure CNI(部分支持) |
验证你的集群到底支不支持,最直接的办法是写一条 deny-all apply 上去,然后实测(第五节那个 evil pod 实验)。如果 deny-all 之后 Pod 还能互通,你的 CNI 就是不支持(或者没正确安装),yaml 全是纸上谈兵。
生产环境选 CNI,我直接给结论:
- 中小集群、想要稳:Calico(生态最成熟,文档最全,问题搜得到答案)
- 性能敏感、想玩 L7 / 可观测性:Cilium(eBPF 是未来,Hubble 能直接看流量拓扑,调试 NetworkPolicy 神器)
- 别用纯 Flannel 上生产(除非你明确不需要网络策略)
除了原生 NetworkPolicy,Calico 和 Cilium 还各自有扩展能力,能干原生 API 干不了的事:
- deny 规则:原生只有 allow,扩展 CRD 能写"显式拒绝某段流量"
- 全局策略:原生按 namespace 隔离,
GlobalNetworkPolicy能写一条全集群生效 - L7 策略:原生只到 L4(端口),Cilium 能写"只允许 GET /api/health,拒绝 DELETE /api/users"这种 HTTP 级规则
- 顺序控制:原生靠并集语义,扩展能指定 rule order
到了这个阶段,你已经不是在写 NetworkPolicy,而是在做 service mesh 的简化版了。不是越大越好——多数团队把原生 NetworkPolicy + 一个可靠的 CNI 用好,就能挡住 95% 的横向移动风险。
七、调试与排错:怎么验证策略真的生效
NetworkPolicy 的调试痛苦在于它是"沉默的"——生效时表现为连接 drop,日志里啥也没有。下面这套流程是我多年总结的排查套路:
第一步:列出所有生效的策略
kubectl get networkpolicy -A # 全集群视角
kubectl describe networkpolicy <name> -n prod # 看具体规则
describe 出来的内容一定要逐字段核对——尤其是 podSelector 是不是真空到了所有 Pod、from/to 是不是 OR 还是 AND。
第二步:确认目标 Pod 真的被"隔离"了
# 看 Pod 的 label 是否被某条 policy 的 podSelector 匹到
kubectl get pods -n prod --show-labels
NetworkPolicy 不生效的 80% 原因是 label 拼错(驼峰/下划线、app vs app.kubernetes.io/name 等)。--show-labels 是第一道检查。
第三步:起一个 netshoot Pod 实测连通性
kubectl run debug --image=nicolaka/netshoot -it --rm -n prod \
--labels=app=debug -- bash
# 在里面测:
curl -v backend:8080 # 该通
curl -v postgres:5432 # 不该通(app=debug 没被 allow)
nc -zv postgres 5432 # 端口探测
dig kubernetes.default # DNS 是否正常
用不同 label 起 debug Pod,模拟不同身份,能精准测出"哪个角色能到哪里"。这套手法比看 yaml 推理快十倍。
第四步:用 CNI 工具看真实规则
- Calico:
calicoctl get policy -o yaml,或kubectl logs -n kube-system calico-node-xxx看 Felix 日志 - Cilium:
cilium monitor、cilium policy get,以及 Hubble——hubble observe -f --pod prod/debug能实时看到每个包是被哪条策略 drop 还是 forward 的,这是 NetworkPolicy 调试的最强工具
第五步:连接超时 vs 连接拒绝,含义不同
- 超时(timeout):CNI 在默默 drop,策略生效中(你被挡了)
- 拒绝(connection refused):包到了目标 Pod,但目标进程没监听该端口——策略没生效,是 Pod 本身的问题
这个区别能省你半个通宵。
小结
- K8s 默认 Pod 全互通,namespace 不是网络边界。一个被攻陷的 Pod = 内网通行证,这是默认网络模型最大的安全隐患
- NetworkPolicy 是声明式的 Pod 防火墙,分 ingress(入站)和 egress(出站),靠
podSelector选受控 Pod,靠from/to列出允许的来源/目标。一旦 Pod 被选中且声明了方向,它就进入白名单隔离态 - 四个匹配维度:
podSelector(同 namespace)/namespaceSelector(跨 namespace)/ipBlock(外部 IP,不是 Service IP)/ports。注意同一 list 元素内是 AND、跨元素是 OR,缩进差一行语义全变 - 零信任姿势 = 先 deny-all 再按需 allow,并且第一时间放行 DNS。K8s 不支持黑名单,原生 API 只有 allow 没有 deny,这是被设计逼出来的好习惯
- 光写 yaml 没用,CNI 必须支持。Flannel 不支持,EKS 默认 VPC CNI 不支持。生产首选 Calico(稳)或 Cilium(强 + 可观测),用 Hubble 调试策略是降维打击
一句话收尾:安全不是"加一道防火墙",而是"让每一个连接都拿得出授权理由"。NetworkPolicy 让你把这句话写进 yaml,CNI 替你把它执行进内核——中间任何一个环节缺位,安全感都是假的。
Related
相关推荐
文章
Docker 部署 Next.js 社区项目的注意事项
围绕 DevOps、Next.js 的技术趋势、工程实践和独立开发视角。
文章
OpenTelemetry 如何帮助你观察 AI 应用?
围绕 DevOps、AI 的技术趋势、工程实践和独立开发视角。
文章
长运行工具调用:Operation、Poll 与 Cancel 的终态协议
初始请求超时后,工具可能仍在服务端运行;发出取消也不等于副作用已停止。本文为单次长运行工具调用建立 operation 身份、轮询水位、取消竞态和唯一终态接受点。
文章
Agent 记忆删除传播:从用户遗忘请求到可验证清除
删除主表一行不会清掉向量、摘要、缓存和恢复后的备份。本文用删除状态机、代际墓碑、派生血缘与端到端 canary 验证真正遗忘。