NetworkPolicy 是网络层的访问控制声明:以标签选择器圈定一组 Pod,为其声明允许的入向(ingress)与出向(egress)流量;一旦某 Pod 被任何策略选中,未被允许的流量即被丢弃。策略由 CNI 网络插件执行,Kubernetes 本体只负责存储与分发声明。
南北向入口(Service 与 Ingress)解决了"怎么进来",这一节解决"谁根本不许进来"。集群的默认网络姿态是全互通:任何 Pod 可以访问任何 Pod 与任何外部地址,这在单团队试验环境无伤大雅,在多团队生产集群里意味着一次凭据泄露就能横向摸到数据库。NetworkPolicy 把网络边界也变成声明。
NetworkPolicy 的语义有三个要点,先立起来再写规则:
这三条合起来构成惯用的"先关门、再开窗"模式。给生产空间先关上默认门:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-ingress namespace: production # 作用域:本空间内所有 Pod spec: podSelector: {} # 空选择器:选中全部 Pod policyTypes: ["Ingress"] # 只收紧入向;出向暂不动
一条规则、四行 spec,整个空间的入向流量即刻回到默认拒绝——包括 DNS 解析也会被拦(DNS 查询也是入向 UDP),所以关门之后必须马上开窗。给 orders-api 开两扇窗:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: orders-api-allow namespace: production spec: podSelector: matchLabels: app: orders-api # 围墙建在订单服务的副本上 policyTypes: ["Ingress"] ingress: - from: - namespaceSelector: # 第一扇窗:整个网关空间的流量 matchLabels: kubernetes.io/metadata.name: ingress-nginx ports: - protocol: TCP port: 8080 - from: - podSelector: # 第二扇窗:本空间的支付服务 matchLabels: app: payment-api ports: - protocol: TCP port: 8080
规则的坐标系统只有两类选择器:podSelector 按 Pod 标签圈人,namespaceSelector 按空间标签圈地,两者还能嵌套取交集(某空间里的某类 Pod)。配合 ipBlock 可以放行外部网段。出向(egress)规则语法对称,值得注意的仍是 DNS:收紧出向后要显式放行到 DNS 服务(kube-dns)的 53 端口,否则应用连服务名都解析不了。
策略生效与否必须实测,声明本身不会告诉你真相:
# 查看已生效的策略 kubectl get networkpolicies -n production # NAME POD-SELECTOR AGE # default-deny-ingress <none> 5m # orders-api-allow app=orders-api 4m # 实测一:网关空间访问订单服务(应通) kubectl run tmp --rm -it -n ingress-nginx --image=busybox:1.36 -- \ wget -qO- --timeout=3 http://orders-api.production:80/healthz # ok # 实测二:无关空间访问订单服务(应被拒,超时而非拒绝报文) kubectl run tmp --rm -it -n default --image=busybox:1.36 -- \ wget -qO- --timeout=3 http://orders-api.production:80/healthz # wget: download timed out # 丢包即证据:围墙在工作
超时(而非 connection refused)是被丢弃的签名,这与端口没人监听的报错不同,排障时可用来区分"被策略拦"与"服务没起"。
⚠️ 常见坑:声明被 API Server 正常受理、get 也能查到,但流量依旧全通——CNI 插件不支持 NetworkPolicy。策略的执行者是 CNI(Calico、Cilium 等支持,部分简易插件不支持),集群建错了插件,策略就是安静的一纸空文。验收新集群时,务必做一次上面这样的实测。
背景:orders-db(4.3 节的 StatefulSet)此前可以被全集群访问,安全审计要求收紧为"仅订单服务可访问 5432 端口,且数据库出向只允许 DNS"。
操作:一条双向策略加一条 DNS 放行——
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: orders-db-lockdown namespace: production spec: podSelector: matchLabels: app: orders-db # 围墙建在数据库副本上 policyTypes: ["Ingress", "Egress"] ingress: - from: - podSelector: # 只放行订单服务 matchLabels: app: orders-api ports: - protocol: TCP port: 5432 egress: - to: # 出向只放行 DNS(否则连服务名都解析不了) - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system ports: - protocol: UDP port: 53 - protocol: TCP port: 53
提交后双向实测:orders-api 的副本连接数据库成功;换 debug-tools Pod 连接则超时;数据库内发起的任何非 DNS 出站(比如尝试连外部地址)全部超时。
结果:审计项整改完成,数据库的网络暴露面从"全集群"收缩到"一个标签"。
解读:这套策略的关键是理解围墙建在数据库侧——ingress 规则声明"谁能来",egress 规则声明"我能去哪"。两侧都收紧后,即使数据库被注入恶意代码,它也连不出去拿外传通道;即使攻击者拿到集群网络权限,也摸不到 5432。这就是纵深防御在网络层的落点。
变式:多团队共管集群常按空间整体设防(default-deny 每个空间,再按 namespaceSelector 互开);对外服务则要在 Ingress 控制器所在的空间放行相应端口;若集群用 Service Mesh(第7章延伸),还可用更细粒度的应用层策略,但网络层围墙始终是地基,两者不互斥。
💡 关键直觉:NetworkPolicy 的选择器和 Service 的是同一套语法,但用途相反——Service 用标签聚流量,NetworkPolicy 用标签设关卡。一个聚合、一个过滤,合起来才是完整的访问面。
至此,一份声明的"访问面"完整成型:负载、副本、入口、围墙。下一章补上最后两块拼图——运行时配置(ConfigMap 与 Secret)和持久存储(从 Volume 到 PVC 的申领制度)。