收紧 NetworkPolicy:一次误配导致集群内服务全隔离的排查与回退

命名空间 default-deny 一开、白名单漏写,集群内 HTTP 全断;靠同源测试与事件日志定位,5 分钟回退止血。

问题背景

公司跑着一套自建 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 -sconntrack -C 都在正常水位,排除了 conntrack 打满、本机端口耗尽这类老问题。容器侧 kubectl top pod 也正常,不像资源压垮。

第二步:把故障面从「应用挂了」收敛到「东西向网络」。
在入口 Nginx 上抓一条失败请求的 request id,对到订单服务日志,发现失败点都在「调库存 gRPC」。在订单 Pod 里:

1
2
3
4
5
6
7
# 同命名空间直连
curl -v --connect-timeout 3 http://inventory-svc:8080/healthz
# 结果:挂起直到超时

# 看 Endpoints 是否存在
kubectl get endpoints inventory-svc -n prod
# 后端 Pod IP 都在,说明 kube-proxy 映射正常

Endpoints 有、Pod Ready,但业务层连不上,基本指向 网络路径被策略拦,而不是 Service 空后端。

第三步:核对近期变更与 NetworkPolicy 现状。
kubectl get networkpolicy -n prod -o wide 一下蹦出三条新策略,其中一条名字叫 default-deny-all

1
2
3
4
5
6
spec:
  podSelector: {}          # 选中本 ns 全部 Pod
  policyTypes:
  - Ingress
  - Egress
  # 没有任何 from / to 白名单

Kubernetes 语义很明确:只要某个方向(Ingress 或 Egress)上存在任意一条 NetworkPolicy 选中了该 Pod,该方向就切到「只允许策略允许的流量」,其余一律丢弃。默认拒绝本身没错,问题出在配套白名单。

第四步:对比「该放行」与「实际放行」。
设计文档里白名单应包含:

  1. 同命名空间内微服务互访(按 app label)
  2. Ingress Controller 所在命名空间的入站(通常带 ingress-nginx 标签)
  3. 出站到 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-allpolicyTypes 里 Egress 注释掉再 apply(仅保留 Ingress 拒绝做小范围验证),订单 → 库存立刻恢复;再把整条 default-deny 删掉,全站 30 秒内 error rate 回落。根因锁定:策略语义理解偏差 + 白名单覆盖不全,不是 CNI 插件本身坏了。

附带排查了一眼 CNI:集群用 Calico,calicoctl get gp 无全局异常,Felix 日志有大量 Drop 计数飙升,与时间线完全吻合,进一步印证是策略丢包而非底层路由。

解决方案

止血(变更窗口内 5 分钟完成):

1
2
# 先整体回退有问题的策略,恢复默认「无策略=全放行」语义
kubectl delete networkpolicy default-deny-all allow-ingress-to-gateway allow-gateway-to-order -n prod

删除后 Calico 立刻收敛,东西向恢复,客服侧超时告警在两分钟内熄火。同步在变更单标记「紧急回退」,并通知安全侧:默认拒绝暂缓,改走「预发验证后再切」。

根治(隔离环境验证后灰度):

  1. 拆策略,禁止一条 YAML 包打天下。 按「DNS / 监控 / 东西向业务 / 南北向 Ingress」四类拆开,每类独立 NetworkPolicy,方便回滚单条。
  2. 白名单用标签选择器,不用拍脑袋写 IP。 例如:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 允许同 ns 内 app 互访(东西向)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-intra-ns
  namespace: prod
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]
  ingress:
  - from:
    - podSelector: {}
  egress:
  - to:
    - podSelector: {}
  - to:   # CoreDNS
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
  1. 灰度顺序:先监控命名空间 → 预发 → 单个业务 ns → 全量。 每个阶段用脚本做「同 ns 互访 / 跨 ns 依赖 / DNS / Ingress / 指标刮取」五元组连通性检查,全绿再进下一 ns。
  2. 变更配套:网络策略变更必须附回滚命令与「删除即恢复」预案;Calico Felix Drop 计数纳入告警,骤升即页。

按上述清单在预发演练两轮后,再于下一个变更窗分命名空间重新落地,未再出现全站隔离。

根因分析

直接原因是:在命名空间内下发了 空白名单的 default-deny(同时覆盖 Ingress 与 Egress),把 Kubernetes「有策略即默认拒绝」的语义触发到了极致,而配套放行策略只覆盖了南北向入口链路,漏掉东西向服务依赖与 DNS

深层原因有三条:

  1. 把「主机防火墙思维」直接套到 K8s。 主机上 default-drop 常先放 established;NetworkPolicy 没有状态防火墙那套隐性放行,必须显式写清每一跳。
  2. 验证方法只测了 Ingress 入口和 Pod 内 loopback,没有测「服务 A → 服务 B」和「Pod → CoreDNS」,用错误探针给了错误信心。
  3. 变更颗粒度过大。 三条策略一次 apply 到生产全部业务 ns,缺少金丝雀与自动连通性门禁,出问题就是爆炸半径拉满。

Calico 工作正常,丢包计数上升是策略按设计在 ren_der——锅在策略内容,不在 CNI。

预防措施

  1. NetworkPolicy 变更门禁:CI 里用 kubectl apply --dry-run=server + 自定义连通性 Job(curl 矩阵)做 PR 检查;无矩阵结果禁止合入。
  2. 策略基线模板化:平台提供「default-deny + DNS + 同 ns + Ingress + metrics」五件套 Helm chart,业务只填 label,禁止手写空 deny。
  3. 可观测性:Felix iptables_denied / eBPF drop 指标接入告警;业务 SLI 出现依赖超时时自动附带「近 15 分钟 NetworkPolicy 变更」注解,缩短定位路径。
  4. 演练:每季度在预发做一次「误删白名单 / 误加 deny」故障注入,验证回退脚本与值班手册。
  5. 权限收敛:生产 networkpolicies 的 create/update/delete 走变更单 + 双人复核,SA 默认只读。
  6. 文档化依赖图:服务依赖从代码/网格侧自动生成,生成 NetworkPolicy 时按图补边,减少漏写。

总结

这是一起典型的「安全加固动机正确、落地验证残缺」事故:default-deny 本身是最佳实践,但在 Kubernetes 里它是硬默认拒绝,白名单少写一跳就是整片东西向网络静默黑洞。排查上不要被「Pod 都 Running」迷惑,Endpoints 在而 TCP 不通时,优先 kubectl get networkpolicy 和 CNI drop 计数;止血优先删策略恢复默认放行,再在预发用连通性矩阵把白名单补全后灰度。

对日常运维的提醒就一句:NetworkPolicy 的爆炸半径不亚于改路由,测入口不等于测全图,先矩阵、后全量,随时能 delete 回滚。

使用 Hugo 构建
主题 StackJimmy 设计