Kubernetes 集群中的 Pod 通常可以彼此通信。部署了 Frontend、Backend 和 Database,并不意味着三层之间天然存在网络边界。要实现零信任,需要先把命名空间切换为默认拒绝,再基于标签、端口和方向逐条开放业务真正需要的链路。
NetworkPolicy 是标准 Kubernetes API,但 API 对象本身不会拦截数据包。真正执行规则的是 CNI 插件。下面的实验假设集群使用 Calico,或者使用另一种明确支持 Ingress 与 Egress NetworkPolicy 的 CNI。
策略生效前,先确认两个前提
第一,确认 CNI 具有策略执行能力。可以先查看集群中的网络组件:
kubectl get pods -A | grep -Ei 'calico|cilium|antrea|weave'
如果使用的是不支持 NetworkPolicy 的网络插件,YAML 可能成功创建,但流量不会被阻断。
第二,给工作负载设计稳定且受控的标签。本文使用:
tier=frontendtier=backendtier=database
NetworkPolicy 把标签当作网络身份。如果攻击者或普通开发账号可以任意修改 Pod、Deployment 的标签,就可能把自己伪装成 Backend。因此,网络策略必须和 RBAC、准入控制一起使用。
搭建一个可测试的三层环境
下面的命令创建独立命名空间、三个工作负载以及对应的 ClusterIP Service。示例中的数据库密码仅适用于临时实验,生产环境应改用 Secret。
kubectl create namespace production-app
kubectl run frontend \
--image=nicolaka/netshoot \
--labels=tier=frontend \
-n production-app \
-- sleep 3600
kubectl run backend \
--image=nginx \
--labels=tier=backend \
-n production-app
kubectl run database \
--image=postgres:18 \
--labels=tier=database \
-n production-app \
--env=POSTGRES_DB=myapp \
--env=POSTGRES_USER=appuser \
--env=POSTGRES_PASSWORD=securepass123
kubectl expose pod frontend --port=80 --target-port=80 -n production-app
kubectl expose pod backend --port=80 --target-port=80 -n production-app
kubectl expose pod database --port=5432 --target-port=5432 -n production-app
kubectl get pods -n production-app --show-labels
kubectl get services -n production-app
等待三个 Pod 进入 Running 状态。应用策略前,可以从 Frontend 验证默认放行行为:
kubectl exec -n production-app frontend -- curl --max-time 3 http://backend
正常情况下会返回 Nginx 页面。
先全部拒绝,再精确开孔
完整零信任基线应同时覆盖 Ingress 和 Egress。只有 Ingress 默认拒绝时,Pod 仍然可以主动访问其他服务或互联网;只有 Egress 默认拒绝时,其他 Pod 仍可能主动连接它。
下面是一组可直接应用的策略:
cat > network-policies.yaml <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production-app
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
namespace: production-app
spec:
podSelector:
matchLabels:
tier: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: frontend
ports:
- protocol: TCP
port: 80
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: database-allow-backend
namespace: production-app
spec:
podSelector:
matchLabels:
tier: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: backend
ports:
- protocol: TCP
port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-allow-egress
namespace: production-app
spec:
podSelector:
matchLabels:
tier: frontend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
tier: backend
ports:
- protocol: TCP
port: 80
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-egress
namespace: production-app
spec:
podSelector:
matchLabels:
tier: backend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
tier: database
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
YAML
kubectl apply -f network-policies.yaml
kubectl get networkpolicy -n production-app
这里有三个容易混淆的语义:
podSelector: {}选择当前命名空间中的全部 Pod。from或to中只有podSelector时,它选择的是策略所在命名空间中的 Pod。- 多个策略不会按顺序覆盖。Kubernetes 会合并它们允许的流量,效果相当于 OR。新增策略通常是在扩大许可范围,而不是创建一条优先级更高的拒绝规则。
Egress 最容易漏掉 DNS
Frontend 访问 http://backend 时,需要先通过 CoreDNS 把 Service 名解析为 ClusterIP。启用默认拒绝 Egress 后,如果没有显式允许 53/UDP 和 53/TCP,常见现象不是连接被拒绝,而是域名解析超时。
先确认 kube-system 具有示例中使用的标准命名空间标签:
kubectl get namespace kube-system --show-labels
上面的规则允许访问 kube-system 中所有使用 53 端口的工作负载,兼容性较好,但范围稍宽。生产环境可以查看 CoreDNS 的实际标签,再组合 namespaceSelector 和 podSelector,只选中 DNS Pod。例如,若集群使用 k8s-app=kube-dns:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
同一个 peer 中的 namespaceSelector 与 podSelector 是 AND 关系:既要位于目标命名空间,又要具有指定 Pod 标签。
用正向和反向用例验证边界
策略创建成功不等于策略符合预期。验证时至少要覆盖允许路径、跨层绕行、错误端口和跨命名空间访问。
Frontend 到 Backend 应成功:
kubectl exec -n production-app frontend -- \
curl --max-time 3 http://backend
Frontend 使用 Backend 的未授权端口应失败:
kubectl exec -n production-app frontend -- \
nc -zvw 3 backend 8080
Frontend 直接访问 Database 应失败,因为 Frontend Egress 不允许这条链路,同时 Database Ingress 只信任 tier=backend:
kubectl exec -n production-app frontend -- \
nc -zvw 3 database 5432
为了验证 Backend 到 Database,可以创建一个带 Backend 标签的诊断 Pod。这个测试也直观说明了为什么标签修改权限必须受到保护:
kubectl run backend-probe \
--image=nicolaka/netshoot \
--labels=tier=backend \
-n production-app \
-- sleep 3600
kubectl exec -n production-app backend-probe -- \
nc -zvw 3 database 5432
无标签 Pod 访问 Backend 应被拒绝:
kubectl run unlabeled-probe \
--image=nicolaka/netshoot \
-n production-app \
-- sleep 3600
kubectl exec -n production-app unlabeled-probe -- \
curl --max-time 3 http://backend
还应从另一个命名空间测试:
kubectl run external-probe \
--image=nicolaka/netshoot \
-n default \
-- sleep 3600
kubectl exec -n default external-probe -- \
curl --max-time 3 http://backend.production-app.svc.cluster.local
未授权流量通常表现为超时,而不是立即返回 connection refused。超时意味着数据包可能被策略丢弃;connection refused 往往意味着流量已到达目标地址,但目标端口没有进程监听。
外部 API 与策略叠加的边界
如果 Frontend 需要调用外部 HTTPS API,可以使用 ipBlock 限制目标网段,并排除已知地址:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-allow-external-api
namespace: production-app
spec:
podSelector:
matchLabels:
tier: frontend
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 203.0.113.0/24
except:
- 203.0.113.1/32
ports:
- protocol: TCP
port: 443
203.0.113.0/24 是文档示例网段,使用前必须替换。还要注意,ipBlock 观察到的是 NAT 前还是 NAT 后的地址,可能受 CNI、云网络和 Service 实现影响。NetworkPolicy 也不是域名防火墙;如果需求是按域名、HTTP 路径或用户身份授权,应考虑支持相应能力的出口网关、服务网格或 CNI 扩展策略。
此外,这条策略会和已有的 frontend-allow-egress 合并,而不会替换它。删除旧策略之前,不要假设新策略能够收紧旧策略已经允许的流量。
上线检查清单
将 NetworkPolicy 推向生产环境时,可以按以下顺序降低误封风险:
- 确认 CNI 同时执行 Ingress 和 Egress 策略。
- 盘点真实通信矩阵,包括 DNS、监控、日志、时间同步和外部 API。
- 为每条允许链路同时检查源端 Egress 和目标端 Ingress。
- 使用稳定标签,并通过 RBAC 或准入策略限制标签修改权限。
- 从默认拒绝开始,以最小端口和最小身份范围开放流量。
- 同时编写允许与拒绝测试,不能只验证正常路径。
- 观察超时、DNS 错误和 CNI 日志,再逐步扩大部署范围。
NetworkPolicy 的关键不在于写出一份复杂 YAML,而在于把应用依赖变成一张可以测试的通信矩阵:Frontend 只能到 Backend,Backend 只能到 Database,其余路径默认不存在。这样即使某一层被攻陷,横向移动和数据外泄的空间也会明显缩小。