8.4 K8s 生产运维:NetworkPolicy 网络策略

预计阅读时间:13 分钟

📖 目录

默认情况下,K8s 集群中的所有 Pod 可以互相通信(扁平网络)。NetworkPolicy 是一种防火墙规则,用于控制 Pod 之间以及 Pod 与外部服务的网络流量。

学习目标

  • 理解 NetworkPolicy 的作用、匹配逻辑与策略类型(Ingress/Egress)
  • 能够为生产环境编写默认拒绝 + 白名单放行的完整网络策略
  • 掌握多维度选择器(namespaceSelector、podSelector、ipBlock)的组合用法
  • 具备排查网络策略不生效的常见问题的能力

前置知识

  • K8s Pod、Namespace、Label/Selector 基本概念
  • 基础网络知识:TCP/UDP 端口、CIDR、子网划分
  • 已安装支持 NetworkPolicy 的 CNI 插件(Calico、Cilium 等)
  • 了解 iptables 基本原理(有助于理解底层实现)

前提条件

NetworkPolicy 需要 CNI 插件支持。以下 CNI 支持 NetworkPolicy:

  • Calico(最常用,功能最丰富)
  • Cilium(eBPF 驱动,性能最好)
  • Weave Net
  • Antrea
  • Flannel(不支持,需额外组件)
# 检查当前 CNI
kubectl get pods -n kube-system | grep -E "calico|cilium|weave|flannel|antrea"

# 如果 CNI 不支持 NetworkPolicy,策略会被创建但不生效

# 查看集群是否启用了 NetworkPolicy 支持
kubectl get crd | grep networkpolicies

1. NetworkPolicy 基础

策略结构

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
spec:
  podSelector: {}          # 空选择器匹配命名空间下所有 Pod
  policyTypes:
  - Ingress
  - Egress

默认拒绝所有入站流量

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  # 没有 ingress 规则 = 拒绝所有入站

默认拒绝所有出站流量

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
spec:
  podSelector: {}
  policyTypes:
  - Egress
  # 没有 egress 规则 = 拒绝所有出站

同时拒绝入站和出站

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
# 此策略创建后,production 命名空间中所有 Pod 的所有流量被阻断
# 必须配合显式白名单规则才能恢复通信

2. 常见场景

场景一:只允许前端访问后端 API

# 后端只允许前端 Pod 访问(端口 8080)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-allow-frontend
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

场景二:允许前端从公网访问

# 前端允许来自 Ingress Controller 的流量(端口 80/443)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-allow-ingress
spec:
  podSelector:
    matchLabels:
      app: frontend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
    ports:
    - protocol: TCP
      port: 80
    - protocol: TCP
      port: 443

场景三:数据库只允许后端访问

# MySQL 只允许后端 Pod 访问(端口 3306)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mysql-allow-backend
spec:
  podSelector:
    matchLabels:
      app: mysql
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: backend
  - from:
    - namespaceSelector:
        matchLabels:
          name: monitoring  # 允许监控系统访问

场景四:允许 Pod 访问特定外部地址

# 只允许访问 DNS 和 API
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-and-api
spec:
  podSelector:
    matchLabels:
      app: app
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector: {}
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
  - to:
    - ipBlock:
        cidr: 10.100.0.0/16
        except:
        - 10.100.0.0/24
    ports:
    - protocol: TCP
      port: 443

场景五:按 Pod 标签精细化控制出站

# 只允许 app=payroll 的 Pod 访问财务系统
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: payroll-egress-finance
spec:
  podSelector:
    matchLabels:
      app: payroll
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 10.200.1.0/24   # 财务系统网段
    ports:
    - protocol: TCP
      port: 8443
  - to:                        # 必须放行 DNS
    - namespaceSelector: {}
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53

场景六:同命名空间内所有 Pod 互访

# 允许同一命名空间内所有 Pod 互相通信
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-same-namespace
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector: {}    # 空 podSelector = 同命名空间内所有 Pod

3. 多维度选择器

# namespaceSelector + podSelector 组合
ingress:
- from:
  - namespaceSelector:
      matchLabels:
        environment: production
    podSelector:
      matchLabels:
        app: frontend
  # 上述规则意味着:来自 production 命名空间中 label app=frontend 的 Pod

# 多个 from 条目是 OR 关系
ingress:
- from:                      # 条件 1:允许监控命名空间所有 Pod
  - namespaceSelector:
      matchLabels:
        name: monitoring
- from:                      # 条件 2:允许本命名空间中 app=frontend 的 Pod
  - podSelector:
      matchLabels:
        app: frontend
# Pod 满足任意一个条件即可访问

# 同一 from 条目内的 namespaceSelector + podSelector 是 AND 关系
ingress:
- from:
  - namespaceSelector:
      matchLabels:
        env: prod
    podSelector:
      matchLabels:
        role: api-client
  # 必须同时满足:来自 env=prod 命名空间 AND label role=api-client 的 Pod

4. 三层防护模型

# 生产环境推荐的分层策略
# 1. 全局默认拒绝(在 namespace 级别创建)
#    - default-deny-ingress
#    - default-deny-egress

# 2. 基础设施规则(kube-system)
#    - 允许 DNS 出站
#    - 允许 kube-dns 入站

# 3. 应用层策略(逐命名空间细化)
#    每个命名空间定义自己的白名单规则

# 完整的三层防护 YAML 示例:
---
# Layer 1: 默认拒绝入站
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes:
  - Ingress
---
# Layer 2: 允许 DNS 出站
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
---
# Layer 3: 应用层白名单
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-allow-frontend
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

5. 调试 NetworkPolicy

# 安装网络策略调试工具
# Calico 自带的策略跟踪
calicoctl get networkpolicy -n default
calicoctl get profile -o wide

# 使用 netshoot 测试连通性
kubectl run -it --rm test-pod --image=nicolaka/netshoot -- /bin/bash
# 在容器内
curl -v http://backend-svc:8080
nc -zv backend-svc 8080

# 检查策略规则数
kubectl get networkpolicy -A
kubectl describe networkpolicy backend-allow-frontend

# Cilium 专用调试
kubectl -n kube-system exec -it cilium-xxxxx -- cilium endpoint list
kubectl -n kube-system exec -it cilium-xxxxx -- cilium policy trace \
  --src-k8s-pod default:frontend-pod \
  --dst-k8s-pod default:backend-pod \
  --dport 8080

# 查看策略生效顺序
kubectl get networkpolicy -n default -o custom-columns=\
'NAME:.metadata.name,POD-SELECTOR:.spec.podSelector.matchLabels'

# 对比策略前后差异(先删除策略再测试)
kubectl delete networkpolicy backend-allow-frontend
# 再测试连通性是否恢复

6. 多策略叠加语义详解

生产环境一个命名空间往往有 10+ 个 NetworkPolicy 同时存在,理解它们的合并逻辑是排障的前提。规则非常简单,但很多人记反:

组合方式语义类比
多个策略之间并集(OR):任一策略放行即放行防火墙白名单叠加
同一策略内多个 from/to 条目并集(OR):满足任一条目即可多个放行规则
同一 from 条目内多个选择器交集(AND):必须同时满足条件叠加
ports 与 from/to 的关系同一个 ingress 条目内 同时生效(AND):既匹配来源又匹配端口源 + 端口双条件
# 叠加示例:以下三个策略同时作用于 app=backend 的 Pod
# 策略 A:允许 frontend 访问 8080
# 策略 B:允许 monitoring 命名空间访问所有端口
# 策略 C:允许本命名空间任意 Pod 访问 3306
# 合并结果:来源为 frontend 或 monitoring 或本命名空间 Pod 的流量全部放行,
#           端口 8080/3306 的限制只在各自策略内生效
# 换句话说:只要命中任意一条完整规则,流量即被放行——"任何策略放行 = 放行"
最容易踩的坑 以为"再加一个拒绝策略就能堵上漏洞"是错误的想法——NetworkPolicy 只有 Allow 语义,没有 Deny 语义(Calico 的 GlobalNetworkPolicy 才有显式 Deny 动作)。想让某个来源被拒绝,唯一办法是不写放行它的规则。删除/收紧已有策略,而不是"补一条拒绝"。

默认拒绝策略的最佳写法

# 生产推荐:default-deny 与白名单分开管理,互不干扰
# 1. default-deny(每个命名空间一份,podSelector: {} 覆盖全部)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
---
# 2. 白名单(只写允许项,无需担心与 deny 冲突)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

7. Calico 实现原理

理解底层实现有助于解释"为什么策略不生效"这类问题。以最常用的 Calico 为例(iptables 数据面):

  • Felix:Calico 的核心 agent,监听 K8s API 中的 NetworkPolicy/EndpointSlice 变更,把它们翻译成宿主机 iptables 规则
  • Profile 与标签:Calico 把每个 Pod 的标签编码成 iptables 的 comment/子链(如 cali-pc-<hash>),规则匹配基于这些标识
  • 链结构:每个 Pod 的 veth 出口挂一条 cali-tw-<pod> 链,流量先经过 20+ 条 cali 前缀链的过滤(这是 iptables-save 里大量 cali- 规则的原因)
  • Egress 方向:Pod 出站流量同样经独立链检查,未命中的默认 DROP
# 查看 Calico 渲染出的 iptables 规则
iptables-save | grep -E "cali-pi|cali-po" | head -20

# 查看 Felix 日志中的策略处理
kubectl -n calico-system logs -l k8s-app=calico-node | grep -i felix | tail -20

# 数据面验证:直接在节点上模拟 Pod 流量路径
# Pod 的 veth 接口
ip link show | grep cali
# 跟踪链规则(需要 root)
iptables -t nat -L -n -v | grep -A 3 cali

当数据面与 API 对象不同步(例如 Calico 升级期间、Felix 重启)时,就会出现"策略存在但规则未渲染"的假象——排错第一步永远是检查 Felix 是否健康、策略是否被正确翻译。

8. 排错案例实战

案例一:数据库连接超时(端口协议不匹配)

现象:应用能连上 MySQL,但 30 秒后超时断开。

排查:MySQL 的 3306 只放行了 TCP,但 MySQL 客户端默认开启了连接复用探测(TCP keepalive),中间 Calico 的 conntrack 老化把空闲连接清掉了。

# 检查 conntrack 表
conntrack -L | grep 3306
# 解决:确认策略端口为 TCP,且 conntrack 超时配置合理
# 或者改用 TCP + 更短的客户端 keepalive 参数

案例二:Ingress 访问 404(策略拦的是健康检查)

现象:Ingress 转发正常,但后端 Pod 频繁摘流。

排查:只放行了来自 ingress-nginx 命名空间的 80 端口,但 kubelet 的探针流量(ReadinessProbe)来自节点 IP,被默认拒绝策略拦截——探针失败导致 Pod 被摘除。

# 修复:放行节点网段(探针与 metrics 采集都走这里)
ingress:
- from:
  - ipBlock:
      cidr: 10.0.0.0/8    # 节点网段,按实际调整
  - namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: ingress-nginx
  ports:
  - protocol: TCP
    port: 8080

案例三:策略灰度导致全量故障

现象:上线新策略 5 分钟后,支付服务大面积超时。

排查:default-deny-egress 应用到所有 Pod 后,支付服务需要访问的外部回调地址(异地数据中心 IP)没有被列入白名单,出站全被丢弃。生产环境应分阶段灰度策略(先小流量命名空间),且 Egress 白名单必须提前基于现有连接清单整理(ss -tnp 或 conntrack 导出活跃连接)。

策略灰度方法论 任何默认拒绝策略都不应该一步到位:先在 dev/staging 观察一周、收集出站流量清单(kubectl exec <pod> -- ss -tn),再按"监控命名空间 → 非核心业务 → 核心业务"的顺序推进。

常见错误

问题一:NetworkPolicy 创建后不生效

现象:创建了拒绝策略,但 Pod 间仍然可以通信。

原因:当前 CNI 插件不支持 NetworkPolicy(如 Flannel)。

解决:确认 CNI 支持情况,使用前文命令检查。Flannel 不支持 NetworkPolicy,需要迁移到 Calico 或 Cilium。

问题二:DNS 解析失败

现象:设置了 default-deny-egress 后,Pod 无法解析域名。

原因:出站策略未放行 DNS 流量(UDP/TCP 53)。

解决:为所有命名空间添加 DNS 出站放行规则。

# 必须为每个命名空间添加此规则
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

问题三:策略规则互相冲突

现象:多个 NetworkPolicy 同时匹配同一 Pod,行为不符合预期。

原因:多个策略的 ingress/egress 规则是并集关系(OR),任何一个策略放行就会生效。

解决:理解策略合并逻辑——所有匹配的规则取并集。避免在同一 Pod 上创建互相矛盾的策略。

问题四:跨命名空间访问被拒绝

现象:设置了 namespaceSelector 但跨命名空间通信仍被阻断。

原因:目标命名空间的默认拒绝策略阻止了入站。

解决:确保目标命名空间也有对应的放行规则,或检查是否有 default-deny-ingress 覆盖。

问题五:kube-dns Pod 无法访问

现象:设置了 DNS 出站规则,但 kube-dns 本身被隔离。

原因:kube-system 命名空间的默认拒绝策略阻止了外部 Pod 访问 kube-dns。

解决:为 kube-system 命名空间添加允许所有命名空间 Pod 访问 kube-dns 的策略。

最佳实践

  • 从默认拒绝开始——先创建 default-deny 策略,再逐步添加白名单规则
  • 使用命名空间级别的默认策略先兜底——避免遗漏任何 Pod
  • 标签命名要一致——建议用 org/app/tier 三层标签体系
  • 出站规则通常比入站更复杂——先从入站开始,再扩展出站
  • 不要忘记 DNS(UDP 53)出站规则——否则 Pod 无法解析域名
  • 定期审核 NetworkPolicy:kubectl get networkpolicy -A -o yaml
  • 使用 calicoctl 或 cilium policy trace 验证策略——不要仅凭直觉判断
  • 为每个环境维护独立的策略文件——通过 kustomize overlay 管理 dev/staging/prod 差异

练习题

练习一:三层防护实践

在 default 命名空间创建三层防护:(1) 默认拒绝所有入站和出站流量;(2) 放行 DNS 出站;(3) 允许 app=frontend 的 Pod 访问 app=backend 的 Pod 8080 端口。然后用 netshoot 测试验证。

练习二:多命名空间隔离

创建两个命名空间 team-a 和 team-b,各自部署前端和后端 Pod。要求:同命名空间内可互访,跨命名空间默认禁止。team-a 的后端允许 team-b 的前端访问 8080 端口。

练习三:出站白名单

为 app=payroll 的 Pod 创建出站策略:只允许访问 10.200.1.0/24 网段的 8443 端口和 DNS(53 端口),禁止其他所有出站流量。验证 Pod 只能访问目标服务。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释 NetworkPolicy 的 Pod 选择器和规则类型尝试向他人讲解
命令操作能不查文档完成 NetworkPolicy 的编写和应用在终端实际执行
原理掌握能说出 NetworkPolicy 的 CNI 实现和流量过滤原理画出流程图
故障排查能独立排查 NetworkPolicy 导致 Pod 无法通信的问题模拟故障并修复
最佳实践能说明为什么需要为每个 Namespace 配置默认拒绝策略对比不同方案

本章总结

默认全开的扁平网络是生产安全的最大缺口,NetworkPolicy 的核心思路是"默认拒绝 + 最小放行":先为每个命名空间创建 default-deny 兜底,再按 podSelector/namespaceSelector/ipBlock 组合逐层添加白名单(多个策略是并集语义),并牢记放行 DNS 出站。落地时采用三层防护模型——全局默认拒绝、基础设施规则、应用层白名单,并配合统一的标签体系与 calicoctl/cilium policy trace 验证。

延伸阅读

↑ 回到顶部