问题背景
公司跑着一套自建 Kubernetes 集群,承载内部订单、库存、积分三套核心微服务,流量都在集群内走 Service 互通,对外只经 Ingress 入口。安全审计后要求「默认拒绝、按需放行」,落地动作是给业务命名空间补上 NetworkPolicy:先加一条 default-deny-all,再按服务依赖补 Ingress / Egress 白名单。
变更窗口选在业务低峰,运维按表格把三条网段策略 YAML 推上去,kubectl apply 显示成功,当时用 curl 测了 Ingress 入口和同 Pod 内 loopback,都正常,就认为落地完成。半小时后客服开始报「下单页加载中、接口超时」,监控面板上一片服务 5xx 与依赖超时。表面看「网络好像还在」,实际是集群内东西向通信被策略一把掐断,入口探测掩盖了真正的故障面。
故障现象
高峰前的半小时内现象逐步放大:
- 前端网关日志大量
upstream timed out,调用订单服务 30 秒无响应。 - 订单 Pod 日志频繁
connection refused/i/o timeout,目标正是库存与积分的 ClusterIP。 kubectl get pods全部 Running、Ready,无 Crash、无重启,节点 NotReady 也没有。- 从跳板机访问 Ingress 公网域名偶发能通(命中本地缓存或未依赖下游的静态接口),容易误判为「业务代码问题」。
- 监控上 error rate 从 <0.1% 抬到 40%+,延迟 P99 从 80ms 飙到 20s+,告警刷屏但没有节点级 CPU / 内存尖刺。
更迷惑的是:kubectl exec 进业务 Pod 里 ping 同命名空间其他 Pod IP 超时,curl ClusterIP 直接 hang;同一节点上 curl 本机 kube-proxy 监听端口却能通。看起来「节点还活着,只是容器网络策略把东西向流量拦了」。
排查过程
第一步:先排除常规宿主机与节点问题。
kubectl get nodes 全 Ready,dmesg 无 OOM / 网卡 down。ss -s 与 conntrack -C 都在正常水位,排除了 conntrack 打满、本机端口耗尽这类老问题。容器侧 kubectl top pod 也正常,不像资源压垮。
第二步:把故障面从「应用挂了」收敛到「东西向网络」。
在入口 Nginx 上抓一条失败请求的 request id,对到订单服务日志,发现失败点都在「调库存 gRPC」。在订单 Pod 里:
|
|
Endpoints 有、Pod Ready,但业务层连不上,基本指向 网络路径被策略拦,而不是 Service 空后端。
第三步:核对近期变更与 NetworkPolicy 现状。
kubectl get networkpolicy -n prod -o wide 一下蹦出三条新策略,其中一条名字叫 default-deny-all:
|
|
Kubernetes 语义很明确:只要某个方向(Ingress 或 Egress)上存在任意一条 NetworkPolicy 选中了该 Pod,该方向就切到「只允许策略允许的流量」,其余一律丢弃。默认拒绝本身没错,问题出在配套白名单。
第四步:对比「该放行」与「实际放行」。
设计文档里白名单应包含:
- 同命名空间内微服务互访(按 app label)
- Ingress Controller 所在命名空间的入站(通常带
ingress-nginx标签) - 出站到 CoreDNS(
kube-system的 53/UDP+TCP)以及必要的外部依赖
实际 apply 的另外两条策略只写了:
- 允许 Ingress Controller → 前端 gateway Pod(Ingress)
- 允许 gateway → 订单(Egress / Ingress 一对)
漏了: 订单 → 库存 / 积分、全部服务 → CoreDNS、监控探针(Prometheus)从 monitoring ns 的刮取。结果是:公网经 Ingress 打到 gateway 偶发能通,gateway 再往下调用全死;Pod 内解析域名也开始间歇失败(UDP 53 被 Egress 默认拒绝)。
第五步:用对照实验一秒定性。
临时把 default-deny-all 的 policyTypes 里 Egress 注释掉再 apply(仅保留 Ingress 拒绝做小范围验证),订单 → 库存立刻恢复;再把整条 default-deny 删掉,全站 30 秒内 error rate 回落。根因锁定:策略语义理解偏差 + 白名单覆盖不全,不是 CNI 插件本身坏了。
附带排查了一眼 CNI:集群用 Calico,calicoctl get gp 无全局异常,Felix 日志有大量 Drop 计数飙升,与时间线完全吻合,进一步印证是策略丢包而非底层路由。
解决方案
止血(变更窗口内 5 分钟完成):
|
|
删除后 Calico 立刻收敛,东西向恢复,客服侧超时告警在两分钟内熄火。同步在变更单标记「紧急回退」,并通知安全侧:默认拒绝暂缓,改走「预发验证后再切」。
根治(隔离环境验证后灰度):
- 拆策略,禁止一条 YAML 包打天下。 按「DNS / 监控 / 东西向业务 / 南北向 Ingress」四类拆开,每类独立 NetworkPolicy,方便回滚单条。
- 白名单用标签选择器,不用拍脑袋写 IP。 例如:
|
|
- 灰度顺序:先监控命名空间 → 预发 → 单个业务 ns → 全量。 每个阶段用脚本做「同 ns 互访 / 跨 ns 依赖 / DNS / Ingress / 指标刮取」五元组连通性检查,全绿再进下一 ns。
- 变更配套:网络策略变更必须附回滚命令与「删除即恢复」预案;Calico Felix Drop 计数纳入告警,骤升即页。
按上述清单在预发演练两轮后,再于下一个变更窗分命名空间重新落地,未再出现全站隔离。
根因分析
直接原因是:在命名空间内下发了 空白名单的 default-deny(同时覆盖 Ingress 与 Egress),把 Kubernetes「有策略即默认拒绝」的语义触发到了极致,而配套放行策略只覆盖了南北向入口链路,漏掉东西向服务依赖与 DNS。
深层原因有三条:
- 把「主机防火墙思维」直接套到 K8s。 主机上 default-drop 常先放 established;NetworkPolicy 没有状态防火墙那套隐性放行,必须显式写清每一跳。
- 验证方法只测了 Ingress 入口和 Pod 内 loopback,没有测「服务 A → 服务 B」和「Pod → CoreDNS」,用错误探针给了错误信心。
- 变更颗粒度过大。 三条策略一次 apply 到生产全部业务 ns,缺少金丝雀与自动连通性门禁,出问题就是爆炸半径拉满。
Calico 工作正常,丢包计数上升是策略按设计在 ren_der——锅在策略内容,不在 CNI。
预防措施
- NetworkPolicy 变更门禁:CI 里用
kubectl apply --dry-run=server+ 自定义连通性 Job(curl 矩阵)做 PR 检查;无矩阵结果禁止合入。 - 策略基线模板化:平台提供「default-deny + DNS + 同 ns + Ingress + metrics」五件套 Helm chart,业务只填 label,禁止手写空 deny。
- 可观测性:Felix
iptables_denied/ eBPF drop 指标接入告警;业务 SLI 出现依赖超时时自动附带「近 15 分钟 NetworkPolicy 变更」注解,缩短定位路径。 - 演练:每季度在预发做一次「误删白名单 / 误加 deny」故障注入,验证回退脚本与值班手册。
- 权限收敛:生产
networkpolicies的 create/update/delete 走变更单 + 双人复核,SA 默认只读。 - 文档化依赖图:服务依赖从代码/网格侧自动生成,生成 NetworkPolicy 时按图补边,减少漏写。
总结
这是一起典型的「安全加固动机正确、落地验证残缺」事故:default-deny 本身是最佳实践,但在 Kubernetes 里它是硬默认拒绝,白名单少写一跳就是整片东西向网络静默黑洞。排查上不要被「Pod 都 Running」迷惑,Endpoints 在而 TCP 不通时,优先 kubectl get networkpolicy 和 CNI drop 计数;止血优先删策略恢复默认放行,再在预发用连通性矩阵把白名单补全后灰度。
对日常运维的提醒就一句:NetworkPolicy 的爆炸半径不亚于改路由,测入口不等于测全图,先矩阵、后全量,随时能 delete 回滚。