阶段二 · CNI 与数据平面

网络策略与零信任

一句话总结

Kubernetes 默认所有 Pod 互相全通--这对开发友好, 对安全是灾难: 一个被攻破的 Pod 能横向访问集群里的一切.
NetworkPolicy 是 K8s 的"分布式防火墙", 让我们从"默认全通"转向"默认拒绝, 按需放行"的零信任东西向隔离.

前置回顾

第 01 篇我们讲了 K8s 的扁平网络: 所有 Pod 像在一个大局域网, IP 直连, 无 NAT. 这对应用开发极友好--但有没有想过它的安全含义? "人人都能直连人人", 意味着任何一个 Pod 被攻破, 攻击者就能畅通无阻地访问集群里所有其他 Pod.

第 03 篇也提到: Flannel 不支持策略, Calico/Cilium 支持. 本篇就来解决这个隔离问题, 这是网络阶段的安全收尾.

危险的默认: 集群是一个"不设防的内网"

先认清这个让很多人后背发凉的事实:

默认情况下, K8s 集群里:
前端 Pod ←→ 订单 Pod ←→ 数据库 Pod ←→ 支付 Pod
└────────────全部可以互相直连, 任意端口────────────┘

现在, 前端 Pod 因为一个漏洞被攻破了:
攻击者拿下前端 Pod → 直接连数据库 Pod 的 5432 端口(扁平网络, 通!)
→ 直接连支付 Pod 的内部接口(通!)
→ 横向移动(Lateral Movement), 逐个沦陷

问题: 前端 Pod 本来只需要访问订单服务, 凭什么能直连数据库?
默认全通让一个小漏洞, 变成了整个集群的失守.

这就是东西向(East-West)流量失控的危险. 传统安全把重心放在南北向(外部进入集群的边界, 如防火墙, Ingress), 却往往忽略了集群内部服务之间的流量. 而现代攻击的核心手法正是横向移动--突破一个点后在内网里横着扩散.

默认全通的集群像一栋所有房门都不上锁的大楼: 小偷只要从一扇窗户进来(攻破一个 Pod), 就能大摇大摆走遍每个房间.
NetworkPolicy 就是给每个房间装上门禁: 就算小偷进了前台, 没有权限也进不了机房(数据库).

先分清两个方向: 南北向 vs 东西向

南北向(North-South) 东西向(East-West)
指什么流量 集群外部 ↔ 内部(用户访问服务) 集群内部服务之间(Pod ↔ Pod)
谁来管 Ingress, Gateway, LB, 防火墙 NetworkPolicy(本篇)
传统重视程度 高(大家都知道要设边界) 低(常被忽略, 是横向移动的温床)

名字来源: 画集群架构图时, 外部流量通常从上往下进来(南北纵向), 服务间流量在同一层横向流动(东西横向). NetworkPolicy 专治东西向--给集群内部服务之间的流量上锁.

NetworkPolicy: 用标签声明"谁能访问谁"

NetworkPolicy 是 K8s 的标准资源, 用声明式方式描述"哪些 Pod 可以接收/发出哪些流量". 它的核心机制和 Service 一样--靠标签选择器(label selector)选中目标 Pod, 而非靠易变的 IP(呼应第 04 篇"身份比地址可靠").

看一个例子: 只允许带 app=order 标签的 Pod 访问数据库的 5432 端口, 其他一概拒绝:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow-order-only
spec:
podSelector: # ← 这条策略保护谁: 带 app=db 的 Pod
matchLabels:
app: db
policyTypes:
- Ingress # ← 管入站流量
ingress:
- from:
- podSelector: # ← 只允许谁进来: 带 app=order 的 Pod
matchLabels:
app: order
ports:
- protocol: TCP
port: 5432 # ← 只允许访问 5432 端口
# 效果: 除了 order Pod 访问 db 的 5432, 其他到 db 的入站流量全部被拒

关键规则: 一旦被策略选中, 就从'默认全通'变成'默认拒绝'

这是 NetworkPolicy 最反直觉, 也最容易踩坑的一点: 一个 Pod 只要被任何一条 NetworkPolicy 的 podSelector 选中, 它就立刻从"默认全通"切换成"白名单模式"--只有策略明确放行的流量能通, 其余全部拒绝.

没被任何策略选中的 Pod 则仍然全通. 所以"加一条放行 order 的策略", 副作用是"同时拒绝了所有其他人"--这正是我们想要的, 但初学者常因此搞懵("我只是想放行 A, 怎么 B 也连不上了?").

最佳实践: 先建一道"默认拒绝"的墙

零信任的标准姿势是先全部拒绝, 再按需放行. 先给命名空间下一条"默认拒绝所有入站"的兜底策略:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {} # 空选择器 = 选中本命名空间所有 Pod
policyTypes:
- Ingress
# 没有写任何 ingress 放行规则 = 拒绝所有入站
# 效果: 本命名空间所有 Pod 的入站流量默认全拒, 再用别的策略逐个开口子

有了这道墙, 再为每个服务补上"谁确实需要访问我"的放行策略. 这就把集群从"默认全通的内网"改造成了"默认隔离, 显式授权"的零信任网络.

零信任: 从不默认信任, 每次都验证

零信任(Zero Trust) 的核心信条是"从不信任, 始终验证(Never trust, always verify)": 不因为"也在集群内网里"就信任, 每一次访问都要有明确授权.

这和 CI/CD 系列里的安全思路一脉相承--不靠"在边界内就放心"(城堡-护城河模型), 而是假设内部也可能被攻破, 处处设防. "默认拒绝 + 按需放行"就是零信任在网络层的落地. 一个 Pod 被攻破, 攻击者也寸步难行--爆炸半径被锁死在那一个 Pod.

L3/4 策略的天花板, 与 L7 的进阶

标准 NetworkPolicy 工作在 L3/L4: 它能管到"哪个 Pod, 哪个 IP 段, 哪个端口", 但管不到应用层的内容. 比如它能放行"访问订单服务的 8080 端口", 却无法表达"只允许调 GET /api/orders, 禁止 DELETE /api/orders".

要做到应用层(L7)的精细管控, 就需要第 04 篇的 Cilium(eBPF 能理解包内容)或服务网格(如 Istio)提供的增强策略:

层级 能管到 谁提供
L3/L4 IP, 端口, 协议(标准能力) 标准 NetworkPolicy(需 Calico/Cilium 等支持的 CNI)
L7 HTTP 方法, 路径, gRPC 方法等应用语义 Cilium(eBPF), 服务网格 Istio 等

别忘了: NetworkPolicy 要 CNI 支持才生效

呼应第 03 篇的坑: NetworkPolicy 是 K8s 定义的标准资源, 但真正执行它的是 CNI 插件. 如果 CNI 是 Flannel(不支持策略), 那么 kubectl apply 一条 NetworkPolicy, K8s 会欣然接受, 不报错, 但它完全不生效--流量照样全通.

这是个隐蔽又危险的坑: 看起来上了锁, 其实门根本没装. 要用 NetworkPolicy, 先确认 CNI 是 Calico, Cilium 等支持策略的插件.

快速回顾

  • 危险的默认: K8s 扁平网络下所有 Pod 默认全通, 一个 Pod 被攻破即可横向移动拿下全集群
  • 南北向 vs 东西向: 南北向(外部↔集群)靠 Ingress/防火墙, 东西向(Pod↔Pod)靠 NetworkPolicy, 后者常被忽视, 是横向移动温床
  • NetworkPolicy: 用标签选择器(非 IP)声明"谁能访问谁"; 一旦 Pod 被某策略选中, 即从全通变为白名单(默认拒绝)
  • 最佳实践: 先下一条 default-deny 兜底墙, 再按需放行--零信任的"默认拒绝 + 显式授权"
  • 零信任: 从不信任, 始终验证, 不因"在内网"就放行, 把爆炸半径锁死在单个 Pod
  • L3/4 vs L7: 标准 NetworkPolicy 到端口级, L7(HTTP 方法/路径)需 Cilium/Istio; 且 NetworkPolicy 必须 CNI 支持(Flannel 不支持)才生效

NetworkPolicy 和服务网格(Istio)的 mTLS, 授权策略有什么区别? 该用哪个?

两者都做东西向安全, 但层次不同, 可叠加. NetworkPolicy 在 L3/4 网络层做"谁能连谁", 轻量, 由 CNI 执行. 服务网格在更上层做身份认证(mTLS 双向加密) 和 L7 授权--它不仅控制"能不能连", 还保证"连接是加密的, 对端身份是经过密码学验证的".

简单场景 NetworkPolicy 足够; 需要服务间加密, 强身份, L7 细粒度授权时, 上服务网格. 二者常配合: NetworkPolicy 做粗粒度网络隔离, 网格做细粒度身份与加密.

给每个服务都写 NetworkPolicy, 会不会维护爆炸?

会有成本, 所以要讲策略. 实践建议:

  1. 按命名空间分层--先用 default-deny 把命名空间之间隔离开, 这一步收益最大, 规则最少
  2. 优先保护高价值目标(数据库, 支付等), 不必一上来给每个无状态服务都写
  3. 用标签和模板减少重复

和可观测性的告警设计同理--不追求面面俱到, 而是把有限的策略用在风险最高的地方. Cilium 等还提供基于身份的策略, 比逐条 IP/Pod 维护轻松得多.

动手练习

  1. 看到"危险的默认": 在一个支持策略的 CNI(Calico/Cilium)集群里, 部署三个 Pod(前端, 订单, 数据库), 确认默认情况下前端能直接连数据库--亲眼看到"危险的默认"
  2. 体会"默认拒绝": 下一条 default-deny-ingress 策略, 观察三个 Pod 瞬间互相连不通了--体会"被选中即默认拒绝"
  3. 按需放行切断横向移动: 逐条放行: 只允许订单访问数据库, 只允许前端访问订单, 验证前端无法直连数据库--横向移动被切断
  4. 踩一次 Flannel 的坑: 故意在 Flannel 集群上重复第 2 步, 观察 default-deny 策略不生效, 流量照样全通--亲身踩一次"CNI 不支持策略"的坑
  5. 写 L7 策略(进阶): 在 Cilium 上写一条 L7 策略, 只允许对某服务的特定 HTTP 路径访问, 验证其他路径被拒--对比标准 NetworkPolicy 做不到这一点