Kubernetes NetworkPolicy 实战:默认全通的坑与用标签做零信任隔离
很多人第一次意识到 K8s 网络的默认行为时都会愣一下:同一个集群里,任意 Pod 都能访问任意其他 Pod。你的前端 Pod 能直连数据库 Pod,一个被攻破的边缘服务能横向扫遍整个命名空间。生产环境里这几乎等于没有网络隔离。
NetworkPolicy 就是给 Pod 之间的流量加防火墙规则的。这篇从「默认全通」的现场讲起,一步步收敛到「默认拒绝 + 白名单放行」的零信任模型。
前提:你的 CNI 得支持 NetworkPolicy
先泼一盆冷水:NetworkPolicy 是个「声明」,真正执行它的是 CNI 插件。如果你的集群用的是不支持 NetworkPolicy 的 CNI(比如默认配置的 flannel),你写的策略会被 API Server 乖乖收下,然后完全不生效------这是最坑的地方,你以为隔离了,其实没有。
支持的 CNI:Calico、Cilium、Weave Net 等。快速自查:
bash
# 看用的什么 CNI
kubectl get pods -n kube-system | grep -E 'calico|cilium|weave|flannel'
如果是 flannel,要么换 CNI,要么叠加 Calico 的 policy-only 模式。下面的例子假设你用的是 Calico/Cilium。
先看默认全通的现场
建两个 Pod,一个当「数据库」,一个当「攻击者」,验证默认能不能通:
bash
kubectl create ns demo
kubectl run db --image=nginx --labels="app=db" -n demo
kubectl run attacker --image=busybox -n demo --command -- sleep 3600
拿到 db 的 IP,从 attacker 去连:
bash
DB_IP=$(kubectl get pod db -n demo -o jsonpath='{.status.podIP}')
kubectl exec -n demo attacker -- wget -qO- --timeout=3 http://$DB_IP
会正常返回 nginx 首页 HTML。attacker 和 db 毫无关系,却能直连------这就是默认全通。
第一步:命名空间级默认拒绝
零信任的第一原则是「先关死,再开口子」。给整个命名空间加一条「拒绝所有入站流量」的兜底策略:
yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: demo
spec:
# podSelector 为空 = 选中命名空间内所有 Pod
podSelector: {}
policyTypes:
- Ingress # 只管入站;不写 Ingress 规则 = 拒绝所有入站
关键理解 NetworkPolicy 的语义:
- 一旦有任何策略选中了某个 Pod,该 Pod 就从「默认全通」切换到「默认拒绝,只放行被明确允许的」。
podSelector: {}匹配命名空间内所有 Pod。policyTypes: [Ingress]且没有ingress:规则 → 所有入站被拒。
应用后再连一次:
bash
kubectl apply -f default-deny.yaml
kubectl exec -n demo attacker -- wget -qO- --timeout=3 http://$DB_IP
# wget: download timed out ------ 通了,现在被拒了
第二步:只放行该放行的
现在 db 谁都连不上,包括合法的后端服务。我们只想让带 app=backend 标签的 Pod 访问 db 的 80 端口。用标签精确开口子:
yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-db
namespace: demo
spec:
podSelector:
matchLabels:
app: db # 这条策略作用在 db 上
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend # 只允许 app=backend 的 Pod
ports:
- protocol: TCP
port: 80
验证:attacker(没有 backend 标签)仍然连不上,而新建一个带 app=backend 的 Pod 就能连:
bash
kubectl run backend --image=busybox -n demo --labels="app=backend" \
--command -- sleep 3600
kubectl exec -n demo backend -- wget -qO- --timeout=3 http://$DB_IP
# 正常返回 nginx 首页 ------ 白名单放行成功
这就是零信任:基于身份(标签)而非 IP 放行。Pod IP 会随重建变化,但标签是稳定的语义身份。
坑一:from 里 namespaceSelector 和 podSelector 的「与/或」
跨命名空间放行时最容易踩的坑------下面这两种写法含义完全不同:
yaml
# 写法 A:一个 from 元素里同时有两个 selector = 「与」
ingress:
- from:
- namespaceSelector:
matchLabels:
env: prod
podSelector:
matchLabels:
app: backend
# 含义:必须是 env=prod 命名空间里、且 app=backend 的 Pod
# 写法 B:两个 from 元素 = 「或」
ingress:
- from:
- namespaceSelector:
matchLabels:
env: prod
- podSelector:
matchLabels:
app: backend
# 含义:env=prod 命名空间的任意 Pod,或 本命名空间里 app=backend 的 Pod
写法 B 里第二个 podSelector 只作用于本命名空间,不会扩到 prod 命名空间。想「prod 命名空间里的 backend」必须用写法 A 那种同一元素内的组合。这个「短横线位置决定与/或」的坑,配错了要么放行过宽、要么该通的不通。
坑二:别忘了出站(Egress)和 DNS
上面只管了 Ingress。如果你还要加 default-deny-egress 锁死出站,会立刻发现一个惊喜:Pod 连 DNS 都解析不了了,因为 DNS 查询(到 kube-dns/CoreDNS 的 53 端口)也是出站流量,被一起拒了。
加 egress 默认拒绝时,记得放行 DNS:
yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: demo
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {} # 允许到 kube-system 里的 CoreDNS
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
不放行 53 端口的话,应用里所有 http://service-name 这种域名访问都会失败,而且报错常常是「connection timeout」而非「DNS 错误」,排查起来很误导。
小结
- K8s 默认全通,同集群任意 Pod 互访;NetworkPolicy 是给 Pod 间流量加防火墙,但必须 CNI 支持(Calico/Cilium 可,默认 flannel 不可,且不生效还不报错)。
- 零信任套路:先
default-deny-ingress关死,再用podSelector按标签开白名单;基于标签身份放行,而非易变的 Pod IP。 - 记牢「from 里短横线决定与/或」:同一元素内多个 selector 是「与」,多个元素是「或」,配错直接导致放行范围错。
- 加 egress 默认拒绝时,第一件事是放行 53 端口的 DNS,否则所有域名访问静默超时。
- 一句话记忆:NetworkPolicy 是白名单模型------只要有一条策略选中了 Pod,没被明确允许的流量就一律拒绝。