一、痛点开头:那晚我差点把整个支付链路干趴
先给你讲个我亲历的事故。去年双十一前一周,公司过等保三级复审,安全团队甩过来一份“加固清单”,其中一条是:所有生产命名空间必须显式配置 NetworkPolicy,实现工作负载之间的网络隔离。这个需求本身没毛病,问题是执行的人(没错就是我)为了“省事”,在一个叫 pay-core 的命名空间里直接贴了一条看起来人畜无害的策略:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: pay-core
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
我当时心想,全拒绝然后慢慢放行呗,合规又干净。结果你猜怎么着?贴上去了大概三十秒,告警群就炸了。
先是监控大盘上的 pay-core 所有 Pod 的 CPU、内存曲线全部变灰——Prometheus 抓取不到了。紧接着支付网关报 5xx 飙升,因为订单服务和账户服务互相调不通了。再然后更离谱,Pod 疯狂 ImagePullBackOff 重启,因为 egress 全断,连镜像仓库都拉不到。最后连 kube-dns 都解析不了,日志里全是一坨 dial tcp: lookup xxx.svc.cluster.local: no such host。
最要命的是 Ingress Controller(我们用的是 Nginx Ingress)开始把 pay-core 的后端标记为 unhealthy,因为 readiness 探针的端口被这条策略挡在了门外。整个支付链路在四分钟内基本瘫痪。我一个八年运维老哥,手抖着把那条 policy kubectl delete 掉才救回来。那一晚我彻底记住了一件事:NetworkPolicy 是白名单,不是黑名单,一旦你“选中”了某个 Pod,它的世界就只剩你显式放行的那几条路。
二、一句话点透原理
NetworkPolicy 的语义就一句话:它是白名单机制。当某个 Pod 被“任意一条” NetworkPolicy 的 podSelector 选中时,这个 Pod 的入向(Ingress)或出向(Egress)流量,凡是没有被策略显式 allow 的,一律 deny。
这里有三个你可能忽略的要点:
第一,命名空间级别 + Pod 选择器是双向作用的。策略写在哪个 namespace,默认只管那个 namespace 里的 Pod;但 from / to 里可以用 namespaceSelector 去引用别的命名空间。选谁、放谁,是两个维度的组合拳。
第二,只要被选中,连 DNS、健康检查、监控抓取都算“流量”,统统受控。很多人以为“我只是限制了业务端口”,结果 kube-dns 的 53 端口没放,Pod 连域名都解析不了,业务自然全挂。
第三,多条策略之间是“或”关系叠加,没有优先级概念。Kubernetes 不会因为你后写的策略就更优先,所有匹配到的策略的放行项会合并。所以一个 Pod 被两条策略选中,只要其中任意一条放行了某条流,它就通。反过来,如果没有任何一条策略放行,它就死。
还有个隐藏大坑:NetworkPolicy 必须 CNI 插件支持才生效。Calico、Cilium 支持;但你如果用的是原生 Flannel(没开 kube-router 或者没配 Calico 做网络策略),你贴一百条 policy 也是空气,根本不拦。我见过有人排障三小时,最后发现是集群压根没装支持网络策略的 CNI,纯纯的乌龙。
三、实战步骤:怎么判断“是不是 NetworkPolicy 干的”
真出事了,别慌,按下面这套流程走,十分钟内基本能定位。
步骤 1:先确认集群里到底挂了多少条策略
kubectl get netpol -A
看输出,重点找出事命名空间下有没有策略。如果 pay-core 下面挂着策略,而半小时前还没,那嫌疑直接拉满。
步骤 2:对比“有策略”和“没策略”的连通性
在同一个命名空间起两个临时 Pod 互相测:
kubectl run test-a --image=nicolaka/netshoot -n pay-core -- sleep 3600
kubectl run test-b --image=nicolaka/netshoot -n pay-core -- sleep 3600
kubectl exec -it test-a -n pay-core -- nc -vz test-b.pay-core.svc.cluster.local 8080
如果 nc 卡住或 Connection refused / timed out,而你知道目标 Pod 的 8080 是开着的,那大概率是策略拦了。
步骤 3:临时删掉策略验证
这是最有效的“回滚验证法”。出事后第一时间把怀疑的策略删了看业务是否恢复:
kubectl delete netpol default-deny-all -n pay-core
kubectl get pods -n pay-core -w
如果删完 Pod 陆续 Ready、监控曲线回来,那基本实锤了。验证完记得把正确的策略再补回去,别留着口子。
步骤 4:用 netshoot 抓包 + DNS 测试
# 进 netshoot 测 DNS 能不能解析
kubectl exec -it test-a -n pay-core -- nslookup kubernetes.default.svc.cluster.local
# 看 DNS 端口通不通(DNS 是 UDP 53)
kubectl exec -it test-a -n pay-core -- nc -uvz kube-dns.kube-system.svc.cluster.local 53
# curl 测业务
kubectl exec -it test-a -n pay-core -- curl -sS -m 5 http://order-svc.pay-core.svc.cluster.local:8080/health
步骤 5:看 CNI 日志
如果是 Calico,可以看它的日志确认策略有没有下发:
kubectl logs -n kube-system -l k8s-app=calico-node --tail=100 | grep -i "policy\|deny\|drop"
Cilium 可以用 cilium monitor 看丢弃的流量,那是另一个维度的爽文,本文不展开。
四、生产环境注意事项
-
永远先放行 DNS 再谈隔离。kube-system 的 53 端口(UDP/TCP)必须第一批放行,否则 Pod 连域名都解析不了,等于直接断网。
-
默认拒绝(default-deny)要渐进式做,别一把梭。先 deny 所有,再一条条 allow 关键流,每放行一条观察一条。推荐“先全放行核心依赖,再逐步收紧”的节奏。
-
端口一定写明 protocol。DNS 是 UDP 53(同时建议放 TCP 53 做大包兜底),别只写端口不写协议,部分 CNI 默认按 TCP 处理会出偏差。
-
egress 别忘了放镜像仓库、外部 API、日志/监控上报端点。否则就会出现 ImagePullBackOff 和监控全瞎。
-
多条策略叠加无优先级,写之前先用 kubectl get netpol -n xxx 看清楚已经有哪些,避免互相打架。
-
确认 CNI 支持。上线前用 kubectl get netpol 加一条测试策略,验证是否真能拦住流量。
-
Ingress Controller 的健康检查端口必须放行。否则后端会被摘流量,比断网还隐蔽。
五、踩坑实录:6 个真实案例
案例①:podSelector: {} 选中全部,却忘了放行 kube-system 的 DNS
这是我最经典的翻车。写了全拒绝策略,结果 Pod 启动后全在 CrashLoopBackOff,日志里全是 DNS 解析失败。根因就是 egress 把 53 端口也拒了。正确做法是同时放行 kube-system:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: pay-core
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
案例②:egress 白名单没放行镜像仓库,导致 ImagePullBackOff
有个同事给 ci 命名空间配了 egress 只允许访问内网业务网段,结果新 Pod 调度上去拉镜像全失败。镜像仓库在集群外,没放进 ipBlock:
egress:
- to:
- ipBlock:
cidr: 10.20.30.0/24 # 镜像仓库所在网段
ports:
- protocol: TCP
port: 443
记住:出网白名单里,镜像仓库、外部 API、日志收集端点,一个都不能少。
案例③:Ingress Controller 的健康检查被拒
Nginx Ingress 的 readiness 探针从 ingress-nginx 命名空间来,但你的策略只允许了业务命名空间的 Pod,结果后端被全部标记 unhealthy,流量进不来。解决是放行 Ingress Controller 命名空间:
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 8080
案例④:namespaceSelector 标签写错,策略完全不生效
想把 pay-core 暴露给 gateway 命名空间,结果写了 matchLabels: {app: gateway},但命名空间本身根本没有 app=gateway 这个标签,只有 kubernetes.io/metadata.name: gateway。结果策略一条流都没放行,等于形同虚设,业务照样互相串。正确写法用命名空间的内置标签:
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: gateway
教训:命名空间的标签用 kubectl get ns --show-labels 确认,别凭记忆写。
案例⑤:Prometheus 抓取被拒,监控全瞎
Prometheus 通常跑在 monitoring 命名空间,它去 pay-core 抓 metrics 端口。你只放行了业务 Pod 之间的互相访问,没放行 monitoring,结果监控曲线全灰,告警失灵。补一条:
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- protocol: TCP
port: 9090
案例⑥:policyTypes 只写了 Ingress,却期待 egress 也受限
有人以为 policyTypes: [Ingress] 是“既管入也管出”的简写,结果 egress 完全没限制,Pod 该出网还出网,安全审计直接不通过。记住:policyTypes 显式声明了哪些方向受控,没写的那个方向就不受这条策略约束(但可能被别的策略管)。想要双向限制就写 [Ingress, Egress]。
六、两个可直接抄的模板
模板 A:正确的最小可用 Policy(先放行 DNS + 监控,再收紧)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: pay-core-baseline
namespace: pay-core
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- protocol: TCP
port: 8080
- protocol: TCP
port: 9090
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 443
模板 B:ipBlock 放行集群外访问的写法
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-api
namespace: pay-core
spec:
podSelector:
matchLabels:
app: order-svc
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 203.0.113.0/24 # 第三方支付网关网段
except:
- 203.0.113.99/32 # 黑名单某个具体 IP
ports:
- protocol: TCP
port: 443
模板 C:渐进式“先放行再收紧”示例输出
# 第一步:先全放行,确认业务正常
$ kubectl apply -f allow-all.yaml
netpol/allow-all created
$ kubectl exec -it test-a -n pay-core -- curl -sS -m5 http://order-svc:8080/health
{"status":"ok"} # 业务通了
# 第二步:收紧为 default-deny,再逐条放行业务依赖
$ kubectl delete netpol allow-all -n pay-core
$ kubectl apply -f pay-core-baseline.yaml
netpol/pay-core-baseline created
$ kubectl exec -it test-a -n pay-core -- curl -sS -m5 http://order-svc:8080/health
curl: (28) Connection timed out # DNS 没放?检查 egress 53
根据上面的输出,如果你看到 Connection timed out 而不是 {"status":"ok"},先回头把 kube-system 的 53 端口补进 egress,这是 90% 渐进式收紧翻车的第一个坑。
七、延伸场景:多租户隔离与跨集群访问
场景一:多租户共用一个集群,A 租户不能访问 B 租户
很多公司把测试、预发、生产塞在一个集群不同命名空间,担心互相串。最稳的做法是每个租户命名空间先 default-deny,再用 namespaceSelector 只放行自己人:
# 给命名空间打租户标签
kubectl label ns team-a tenant=team-a
kubectl label ns team-b tenant=team-b
# 验证标签
kubectl get ns --show-labels | grep tenant
然后 team-a 只放行来自 tenant=team-a 的入向流量,team-b 的 Pod 即使知道 Service 名也连不上,从网络层就隔开了。注意这里 namespaceSelector 用的是你自己打的标签,不是内置的 kubernetes.io/metadata.name,两种方式都行,但自建标签更灵活,适合一个租户占多个命名空间的情况。
场景二:Pod 需要访问集群外的数据库
数据库在 IDC 机房,不在 K8s 网段。这时候不能用 podSelector / namespaceSelector,得用 ipBlock 把对端网段放出来,同时记得 except 掉不该去的网段,避免策略变成“出网全开”。上面模板 B 已经给过写法,这里提醒一句:ipBlock.cidr 写的是目标 Pod 看到的远端地址,不是 Node IP,别搞混。
场景三:滚动发布时新 Pod 短暂不通
有人发现新版本 Pod 起来后的前十几秒访问 5xx,怀疑是策略问题。其实多半是策略匹配的 podSelector 用的是版本号标签(比如 version: v2),而新 Pod 还没完全带齐标签、或者旧策略只放行了 version: v1。建议 podSelector 用稳定的 app 标签而非易变的 version 标签,版本间互通用 Service 名解耦,别让网络策略跟发布版本强绑定。
八、FAQ:被问得最多的几个问题
Q1:我写了 NetworkPolicy,但用 curl 测还是通,策略没生效?
九成是 CNI 不支持。原生 Flannel 不实现网络策略,策略会被 API Server 接受但不拦截任何流量。先确认 kubectl get pods -n kube-system 里有没有 calico-node 或 cilium 相关的 Pod。没有就别白忙活,先换 CNI 或叠加网络策略插件。
Q2:两条策略,一条 allow 一条 deny,谁赢?
没有“谁赢”这一说。多条策略对同一个 Pod 是并集(或关系),只要任意一条 allow 了某条流,它就通;反过来所有策略都没 allow 的流才被拒。所以不存在“后写覆盖前写”,也别指望用一条 deny 去抵消另一条 allow,那行不通。
Q3:policyTypes 不写会怎样?
如果你只写了 ingress 字段没写 policyTypes,K8s 会自动把 Ingress 加进 policyTypes,语义没问题;但如果你的 spec 里只有 egress 规则却没写 policyTypes,系统会自动补 Egress。坑在于:如果你 spec 里 ingress 和 egress 都写了,却只写了 policyTypes: [Ingress],那 egress 规则虽然写了也不生效。所以最稳妥就是显式写全 [Ingress, Egress]。
Q4:为什么 kube-dns 解析时报 no such host 而不是连接超时?
这其实是 DNS 请求被 egress 拒了,客户端拿不到响应,表现就是解析失败。并不是 DNS 服务挂了,而是你的 Pod 出不去 53 端口。把 kube-system 的 53 放进 egress 立刻就好,这是本文案例①的核心。
Q5:NetworkPolicy 能限制 HostNetwork 的 Pod 吗?
不能。用了 hostNetwork: true 的 Pod 走的是宿主网络栈,NetworkPolicy 管不到它。这种 Pod 的隔离得靠节点层面的防火墙或 Calico 的 HostEndpoint 来解决,别指望一条普通策略。
Q6:怎么在测试环境安全地练手?
单独建一个 netpol-lab 命名空间,先 default-deny 它,再用本文模板逐步放行,配合 netshoot 做对照测试。把每次 nc / curl 的结果记下来,形成你自己的“放行对照表”,上线生产时直接照抄,心里有底。
九、总结 + 排查 Checklist
NetworkPolicy 是个好东西,但它是白名单,下手之前先想清楚:这个 Pod 要解析谁、要调用谁、谁要调用它、谁要抓它的指标、它的健康检查从哪来。把这五条列出来,再写策略,基本不会翻车。我那晚的事故,归根结底就是“先 deny 再想”反了,应该是“先想清楚再 deny”。
最后送你一份出事时可以照着勾的清单:
- [ ] 确认 CNI 是否支持 NetworkPolicy(Calico/Cilium,而非原生 Flannel)
- [ ] 用
kubectl get netpol -A 列出所有策略,定位出事命名空间
- [ ] 用
kubectl describe netpol <name> -n <ns> 看 podSelector 和 from/to 细节
- [ ] 临时
kubectl delete netpol 验证,业务是否恢复
- [ ] 用 netshoot 起测试 Pod,nc/nslookup/curl 逐端口测试
- [ ] 检查 egress 是否放行 kube-system 的 DNS(UDP/TCP 53)
- [ ] 检查 egress 是否放行镜像仓库、外部 API 网段(ipBlock)
- [ ] 检查 ingress 是否放行 Ingress Controller 命名空间的健康检查
- [ ] 检查 ingress 是否放行 monitoring 命名空间的抓取端口
- [ ] 确认 namespaceSelector 用的是真实标签(
kubectl get ns --show-labels)
- [ ] 确认 policyTypes 同时包含 Ingress 和 Egress(如需双向限制)
- [ ] 端口都显式写了 protocol(TCP/UDP)
- [ ] 多条策略叠加后,用“或”逻辑复核放行是否充足
- [ ] 上线后用
cilium monitor / Calico 日志确认策略已下发并生效
踩坑不可怕,可怕的是踩完不记。把这份清单贴在工位上,下次动 NetworkPolicy 之前先过一遍,能救你半条命。如果你也有类似的 K8s 排障经验,欢迎来云栈社区分享交流。