Kubernetes NetworkPolicy 实战:默认全通的坑与用标签做零信任隔离
很多人第一次意识到 K8s 网络的默认行为时都会愣一下:同一个集群里,任意 Pod 都能访问任意其他 Pod。你的前端 Pod 能直连数据库 Pod,一个被攻破的边缘服务能横向扫遍整个命名空间。生产环境里这几乎等于没有网络隔离。
NetworkPolicy 就是给 Pod 之间的流量加防火墙规则的。这篇从「默认全通」的现场讲起,一步步收敛到「默认拒绝 + 白名单放行」的零信任模型。
前提:你的 CNI 得支持 NetworkPolicy
先泼一盆冷水:NetworkPolicy 是个「声明」,真正执行它的是 CNI 插件。如果你的集群用的是不支持NetworkPolicy 的 CNI(比如默认配置的 flannel),你写的策略会被 API Server 乖乖收下,然后完全不生效——这是最坑的地方,你以为隔离了,其实没有。
支持的 CNI:Calico、Cilium、Weave Net 等。快速自查:
# 看用的什么 CNIkubectl get pods-nkube-system|grep-E'calico|cilium|weave|flannel'如果是 flannel,要么换 CNI,要么叠加 Calico 的 policy-only 模式。下面的例子假设你用的是 Calico/Cilium。
先看默认全通的现场
建两个 Pod,一个当「数据库」,一个当「攻击者」,验证默认能不能通:
kubectl create ns demo kubectl run db--image=nginx--labels="app=db"-ndemo kubectl run attacker--image=busybox-ndemo--command--sleep3600拿到 db 的 IP,从 attacker 去连:
DB_IP=$(kubectl get pod db-ndemo-ojsonpath='{.status.podIP}')kubectlexec-ndemo attacker --wget-qO---timeout=3http://$DB_IP会正常返回 nginx 首页 HTML。attacker 和 db 毫无关系,却能直连——这就是默认全通。
第一步:命名空间级默认拒绝
零信任的第一原则是「先关死,再开口子」。给整个命名空间加一条「拒绝所有入站流量」的兜底策略:
apiVersion:networking.k8s.io/v1kind:NetworkPolicymetadata:name:default-deny-ingressnamespace:demospec:# podSelector 为空 = 选中命名空间内所有 PodpodSelector:{}policyTypes:-Ingress# 只管入站;不写 Ingress 规则 = 拒绝所有入站关键理解 NetworkPolicy 的语义:
- 一旦有任何策略选中了某个 Pod,该 Pod 就从「默认全通」切换到「默认拒绝,只放行被明确允许的」。
podSelector: {}匹配命名空间内所有 Pod。policyTypes: [Ingress]且没有ingress:规则 → 所有入站被拒。
应用后再连一次:
kubectl apply-fdefault-deny.yaml kubectlexec-ndemo attacker --wget-qO---timeout=3http://$DB_IP# wget: download timed out —— 通了,现在被拒了第二步:只放行该放行的
现在 db 谁都连不上,包括合法的后端服务。我们只想让带app=backend标签的 Pod访问 db 的 80 端口。用标签精确开口子:
apiVersion:networking.k8s.io/v1kind:NetworkPolicymetadata:name:allow-backend-to-dbnamespace:demospec:podSelector:matchLabels:app:db# 这条策略作用在 db 上policyTypes:-Ingressingress:-from:-podSelector:matchLabels:app:backend# 只允许 app=backend 的 Podports:-protocol:TCPport:80验证:attacker(没有 backend 标签)仍然连不上,而新建一个带app=backend的 Pod 就能连:
kubectl run backend--image=busybox-ndemo--labels="app=backend"\--command--sleep3600kubectlexec-ndemo backend --wget-qO---timeout=3http://$DB_IP# 正常返回 nginx 首页 —— 白名单放行成功这就是零信任:基于身份(标签)而非 IP 放行。Pod IP 会随重建变化,但标签是稳定的语义身份。
坑一:from 里 namespaceSelector 和 podSelector 的「与/或」
跨命名空间放行时最容易踩的坑——下面这两种写法含义完全不同:
# 写法 A:一个 from 元素里同时有两个 selector = 「与」ingress:-from:-namespaceSelector:matchLabels:env:prodpodSelector:matchLabels:app:backend# 含义:必须是 env=prod 命名空间里、且 app=backend 的 Pod# 写法 B:两个 from 元素 = 「或」ingress:-from:-namespaceSelector:matchLabels:env:prod-podSelector:matchLabels:app:backend# 含义:env=prod 命名空间的任意 Pod,或 本命名空间里 app=backend 的 Pod写法 B 里第二个podSelector只作用于本命名空间,不会扩到 prod 命名空间。想「prod 命名空间里的 backend」必须用写法 A 那种同一元素内的组合。这个「短横线位置决定与/或」的坑,配错了要么放行过宽、要么该通的不通。
坑二:别忘了出站(Egress)和 DNS
上面只管了 Ingress。如果你还要加default-deny-egress锁死出站,会立刻发现一个惊喜:Pod 连 DNS 都解析不了了,因为 DNS 查询(到 kube-dns/CoreDNS 的 53 端口)也是出站流量,被一起拒了。
加 egress 默认拒绝时,记得放行 DNS:
apiVersion:networking.k8s.io/v1kind:NetworkPolicymetadata:name:allow-dns-egressnamespace:demospec:podSelector:{}policyTypes:-Egressegress:-to:-namespaceSelector:{}# 允许到 kube-system 里的 CoreDNSports:-protocol:UDPport:53-protocol:TCPport:53不放行 53 端口的话,应用里所有http://service-name这种域名访问都会失败,而且报错常常是「connection timeout」而非「DNS 错误」,排查起来很误导。
小结
- K8s 默认全通,同集群任意 Pod 互访;NetworkPolicy 是给 Pod 间流量加防火墙,但必须 CNI 支持(Calico/Cilium 可,默认 flannel 不可,且不生效还不报错)。
- 零信任套路:先
default-deny-ingress关死,再用podSelector按标签开白名单;基于标签身份放行,而非易变的 Pod IP。 - 记牢「from 里短横线决定与/或」:同一元素内多个 selector 是「与」,多个元素是「或」,配错直接导致放行范围错。
- 加 egress 默认拒绝时,第一件事是放行 53 端口的 DNS,否则所有域名访问静默超时。
- 一句话记忆:NetworkPolicy 是白名单模型——只要有一条策略选中了 Pod,没被明确允许的流量就一律拒绝。