#DevOps

Kubernetes 网络策略:Pod 间防火墙

K8s 默认所有 Pod 互通——这在生产是安全隐患。NetworkPolicy 让你声明"哪些 Pod 能和哪些 Pod 通信"。本文讲清 NetworkPolicy 的 ingress/egress 规则、默认拒绝模式、以及"零信任"的 K8s 网络实践。

本文要点

  • 主题聚焦:DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年2月2日·更新于 2026年7月23日

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"的访问控制。

text
        ┌──────────────────────────────────────┐
        │      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 端口的访问:

yaml
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

yaml
from:
  - podSelector:
      matchLabels:
        app: frontend

坑:单独写 podSelector(不配 namespaceSelector)只匹配当前 policy 所在 namespace 的 Pod。如果想跨 namespace,必须配 namespaceSelector。

2. namespaceSelector —— 按 namespace 选整个命名空间

yaml
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 关系)

yaml
from:
  - namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: prod
    podSelector:
      matchLabels:
        app: frontend

最关键的语义点:当 namespaceSelector 和 podSelector 写在同一个 list 元素里,它们是 AND(交集)——"prod namespace 里 label 为 app=frontend 的 Pod"。但如果写成两个 list 元素:

yaml
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

yaml
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 —— 端口和协议

yaml
ports:
  - protocol: TCP
    port: 5432
  - protocol: UDP
    port: 53

port 可以是数字也可以是命名端口(port: http,对应容器 ports 里的名字)。省略 ports 字段 = 放行所有端口——这是第二大翻车点,你以为写得很严,其实漏了端口限制。

四、默认拒绝模式:deny-all + 按需 allow(零信任的姿势)

理解了上面两条,默认拒绝模式就很简单了。先看一份"全拒绝"策略:

yaml
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,整个服务网格直接瘫。补一条:

yaml
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监听端口
frontendapp=frontend, tier=web3000
backendapp=backend, tier=api8080
db (postgres)app=postgres, tier=data5432

另外 Ingress Controller 在 ingress-nginx namespace,label app.kubernetes.io/name=ingress-nginx。

期望的连通关系(只有这些):

text
   外网 → Ingress → frontend:3000
                       │
                       ▼
                    backend:8080
                       │
                       ▼
                    postgres:5432

db 不能主动连任何地方,backend 不能被 frontend 以外的 Pod 访问,frontend 不能直连 db(绕过 backend 是典型越权)。第一步:apply 默认拒绝(第四节的 default-deny-all + DNS 放行)。第二步:逐层 allow。

策略 1:frontend 允许 Ingress Controller 进来

yaml
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

yaml
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

yaml
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

yaml
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

yaml
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

注意这套配置的两个细节,都是实战血泪:

  1. 双向都要写。NetworkPolicy 是有方向的。frontend 出站允许 → backend:8080,但这不代表 backend 入站允许 frontend。两边的策略都得写,连接才通。
  2. 回包不用写。虽然双向都要写 allow,但 established 连接的反向回包不用单独 allow——CNI 都是基于连接跟踪(conntrack)有状态的,已建立连接的响应流量自动放行。否则 NetworkPolicy 写起来要疯。

apply 完后验证一下:起一个临时 Pod 模拟"恶意 Pod"(label 故意写成 app=evil),它不应该能访问 db:

bash
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,日志里啥也没有。下面这套流程是我多年总结的排查套路:

第一步:列出所有生效的策略

bash
kubectl get networkpolicy -A                    # 全集群视角
kubectl describe networkpolicy <name> -n prod   # 看具体规则

describe 出来的内容一定要逐字段核对——尤其是 podSelector 是不是真空到了所有 Pod、from/to 是不是 OR 还是 AND。

第二步:确认目标 Pod 真的被"隔离"了

bash
# 看 Pod 的 label 是否被某条 policy 的 podSelector 匹到
kubectl get pods -n prod --show-labels

NetworkPolicy 不生效的 80% 原因是 label 拼错(驼峰/下划线、app vs app.kubernetes.io/name 等)。--show-labels 是第一道检查。

第三步:起一个 netshoot Pod 实测连通性

bash
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

相关推荐