说实话,我最初接触K8S网络策略(NetworkPolicy)的时候有这么一个感受:Kubernetes文档里讲得清清楚楚,每个字段都能看懂,可真到了生产环境要落地、要排障的时候,才发现文档里没写出来的坑一个接一个。尤其是你满怀信心地给某个服务加了一条默认拒绝策略,结果一上线,业务直接断连,线上告警响成一片,那种感觉太熟悉了。
我写这篇东西的目的很简单:把NetworkPolicy从原理到实战、从配置到踩坑,完完整整捋一遍。适合刚接触K8S网络隔离、被安全要求逼着上策略的人,也适合已经配了一堆策略但遇到"流量不通"不知道从哪查起的人。文章里不会只有YAML片段,还会解释每个设计为什么是合理的,以及我在真实项目里踩过的坑和总结出的排查链路。
1. 为什么默认全放通的集群必须补上网络策略这道闸
1.1 K8S网络默认值与生产环境的矛盾
Kubernetes默认的网络模型是:所有Pod之间可以直接通信,不管它们在哪个节点、哪个命名空间。这个设计初衷是为了让服务发现和调用足够简单——你不需要为每个服务手工配路由,只要通过Service名或者ClusterIP就能访问。但"简单"的另一面就是"无防护":一旦某个Pod被攻破,攻击者可以横向扫整个集群,访问所有能被网络触达的端口。
我经历过一次安全评审,对方上来就问:"你们的Pod之间能不能互相访问?"我当时还觉得这是理所当然的——能访问不是很正常吗?但评审人员接着问:"如果某个应用有漏洞被利用,攻击者能不能从那个Pod直接访问你们的数据库Pod?"这一下就让人意识到,默认全放通在生产环境里是不可接受的。数据库、配置中心、内部API这些高价值服务,不应该对所有Pod开放。这就是网络策略存在的根本原因:在K8S自己的网络层上面,再搭一层基于身份和标签的访问控制。
1.2 NetworkPolicy在东西向流量安全里的位置
在传统机房时代,安全边界是靠防火墙、VLAN、访问控制列表来划的,南北向流量(外部到内部)是主要防御对象。但在K8S集群里,更危险的是东西向流量——也就是Pod与Pod之间的横向流量。因为一个集群里通常同时跑着几百上千个服务,开发、测试、第三方组件全混在一起,边界很难靠物理网络划分。
NetworkPolicy就是专门管这种东西向流量的。它跟传统防火墙思路不太一样:不依赖IP和端口白名单做静态规则,而是利用K8S自身的标签机制,用podSelector、namespaceSelector选出"哪些Pod是被保护的",再声明"哪些来源可以访问它们"。这种基于标签而不是基于固定IP的模型,天然适合Pod IP随时变化的动态环境。
也可以把它理解成每个命名空间或每组Pod内部的一道小闸门:Pod可以照常启动、调度,但流量能不能进来、能不能出去,由策略说了算。相比服务网格那一套,NetworkPolicy是K8S原生的、控制面开销更小的方案,适合解决"最基础、最必须"的东西向隔离问题。
1.3 先确认你的CNI支不支持
这是最容易忽略、也最容易导致"策略配了却没效果"的前提条件。NetworkPolicy是K8S API层面的对象,但真正拦截数据包的是底层的CNI网络插件。如果CNI插件本身不支持NetworkPolicy,或者没有启用相关特性,那YAML写得再对也没用。
我建议任何想上网络策略的人,动手之前先做一件事:查清楚你的CNI用的什么网络方案,以及它如何实现策略。不同的CNI实现差异其实挺大:有些基于iptables或nftables做规则转换,有些基于eBPF在内核态直接过滤,还有些老牌的纯Overlay方案默认甚至不支持NetworkPolicy。除了插件类型,还要看部署参数——有些插件默认关闭策略强制执行,需要修改配置或重新部署才能启用。
验证方法很简单:在集群里随便挑一个命名空间,创建一个"拒绝所有入口流量"的最小策略,然后从这个命名空间的Pod访问另一个Pod,看看流量是否真的被拦。如果策略写到YAML里之后完全没反应,那八成就是CNI这一层没开。
2. 网络策略的工作机制:选择器、方向与规则叠加
2.1 一份策略到底在描述什么
NetworkPolicy的核心概念就三个:谁、往哪个方向、被允许访问谁。它没有"拒绝"动作,规则里只白名单式地列举允许条件,其余流量默认被拦。这是理解网络策略最重要的一点:它跟防火墙ACL的"先允许后拒绝"模式不同,更像一套叠加的入网许可。
一个NetworkPolicy对象写在某个命名空间下面,里面用podSelector选中要被这个策略影响的Pod。然后定义两个可选块:ingress(入口)和egress(出口)。ingress段落里列出的源(from)就是"允许谁访问被选中的Pod";egress段落里列出的目的(to)就是"被选中的Pod允许访问谁"。
举个例子,如果我要保护某个核心服务,只允许网关服务的Pod访问它,那么策略就是:选择核心服务的Pod,ingress里写一个from,源是"带有网关服务标签的Pod"。语义上很好理解,但真正复杂的是选择器本身的组合方式。
2.2 三个选择器的边界:podSelector、namespaceSelector、ipBlock
NetworkPolicy里的规则可以从三种来源里选:podSelector、namespaceSelector、ipBlock。各自有明确的适用范围。
- podSelector:选中"同一命名空间内"带有指定标签的Pod。注意,它没法跨命名空间选Pod,想跨命名空间必须配合namespaceSelector用。
- namespaceSelector:选中"命名空间级别"的标签,配合podSelector时可以表达"某个命名空间里的某类Pod"。如果只写namespaceSelector不写podSelector,表示选中那个命名空间里的所有Pod。
- ipBlock:直接按CIDR网段匹配,例如10.0.0.0/16。它能匹配Pod,也能匹配节点、外部负载均衡器之类的IP地址,因为它不关心K8S对象身份,只看IP包头的目标或源地址。
我实际用下来最常见的组合是:namespaceSelector + podSelector一起出现在ingress的from里,表示"来自某命名空间中具有某标签的Pod"。很多刚上手的人会疑惑为什么from里可以同时出现这两个字段——它们其实是"并且"的关系,必须先满足命名空间条件,再满足Pod标签条件。而from列表中的多个独立条目之间是"或者"的关系,只要命中其中一个条目就算匹配。
2.3 policyTypes:最容易踩空的隐式行为
这个字段值得单独拿出来讲。NetworkPolicy里有个可选字段policyTypes,它声明这份策略管的是入口、出口、还是两者都管。如果省略它,K8S会根据规则内容做推导:规则里写了ingress,就把Ingress加进来;写了egress,就把Egress加进来;如果两个块都写了,那policyTypes就是Ingress和Egress。
这套隐式逻辑大部分时候没毛病,但如果你的目标是想创建一份"只拒绝、不显式允许"的策略,就很容易翻车。比如我当初想实现命名空间入口全拒,这样写:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-ingress namespace: myapp spec: podSelector: {} policyTypes: - Ingress ingress: - {}这里的ingress: - {}表示"我们列出允许条件,但条件是空的",所以没有任何流量被允许,实现默认全拒。如果少了policyTypes字段,光凭ingress里有内容,系统也能推导出是Ingress,问题还不大。但如果你想同时默认拒绝入口和出口,而且规则正文里某个方向是空数组,那就必须显式把两个方向都写进policyTypes,否则另一个方向的限制根本没生效。我自己就见过一个同事写的策略本来想双向全拒,结果只约束了入口,出口照样畅通,数据一样被外盗。
2.4 多条策略的叠加逻辑
这个点很多人理解成"多条策略要么取交集,要么后写的覆盖先写的",其实都不对。作用在同一批Pod上的多条NetworkPolicy,是并集逻辑:每条策略只需要在它自己的范围内"允许"流量,只要有任何一条策略的白名单命中,这个流量就放行;同时每条策略的相对方向里没被允许的流量会被拒绝。
这样设计的好处是,你可以把策略拆分成多个小文件分别管理——例如一个团队维护自己的服务入方向策略,另一个团队维护自己的出方向策略,不会互相覆盖。缺点是排障时容易"只见树木不见森林":你以为这条策略已经把流量拦了,但另一条策略里恰好放行了同样的来源,流量照样通过。这也是我后来建议团队把策略汇总到一个目录统一审查的原因。
3. 一套可以照着抄的三层隔离方案
3.1 目标拓扑:网关、核心服务与数据服务
为了把上面的原理串起来,我拿一个模拟项目举个例子。假设有一个典型的内部业务系统,三个关键服务:
- 网关服务(gateway):对外暴露入口,需要访问核心服务的接口。
- 核心服务(core):承载主要业务逻辑,需要访问数据服务读取数据,也需要访问DNS做解析。
- 数据服务(db):存储业务数据,只允许核心服务访问,禁止其他任何Pod访问。
三个服务都跑在同一个命名空间app-prod里,我们想让它们之间的网络关系变成:gateway能访问core的8080端口,core能访问db的5432端口和集群DNS的53端口,其他访问一律禁掉。这是一个非常经典的"默认拒绝 + 白名单放行"场景,也基本覆盖了网络策略的绝大多数用法。
3.2 第一步:三个默认拒绝策略
先别急着写放行规则,第一步一定是给每个命名空间/每组Pod建立"基础防线"。在我们的架构里,最稳妥的方案是对app-prod命名空间里的所有Pod,同时默认拒绝入口和出口,然后再用额外的策略逐层放行。
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: app-prod spec: podSelector: {} policyTypes: - Ingress - Egress ingress: [] egress: []注意ingress: []和egress: []是空数组,配合policyTypes使用,就能实现全部拒绝。这段配置是后面所有放行策略的"地基",没有它,其他策略就只是一层可穿透的纱窗。我测试过,只写podSelector: {}而不加两个空规则数组,是无法实现全拒绝的,必须把方向和空数组都写清楚。
3.3 第二步:入口方向精确放行
接下来是入口放行。我们希望网关Pod能访问核心服务的8080端口,于是创建一条针对core服务的Ingress策略:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-gateway-to-core namespace: app-prod spec: podSelector: matchLabels: app: core policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: gateway ports: - protocol: TCP port: 8080因为默认策略已经把入口全部拒了,这条策略只需要声明"来自gateway的TCP 8080允许进来"就够。同一时间,db服务的入口没有策略放行,所以只有默认拒绝挡在那。数据库服务我们打算单独再写一条只有core能访问的策略,下面会讲。
3.4 第三步:出口方向的流量清单化
出口方向复杂一些,因为服务不仅要访问集群内其他服务,还要访问DNS。在K8S里,大多数服务依赖集群内置DNS做服务发现,如果出口全拒了,DNS请求也会被拦。
核心服务要能访问数据库的5432端口,于是这样写:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-core-to-db namespace: app-prod spec: podSelector: matchLabels: app: core policyTypes: - Egress egress: - to: - podSelector: matchLabels: app: db ports: - protocol: TCP port: 5432 - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: TCP port: 53 - protocol: UDP port: 53这段配置包含两个to条目,它们是"或"的关系:要么访问db的5432,要么访问kube-system命名空间里kube-dns的53。这里有两个细节需要注意。
第一,访问DNS时最好TCP和UDP 53都放行,否则某些场景下解析会超时——有些实现走UDP,有些健康检查或大响应走TCP。
第二,namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}}这种写法,是在用命名空间自身名字作为标签来精确选择kube-system。K8S会给每个命名空间自动打一个kubernetes.io/metadata.name标签,值就是命名空间名字,这样选择最稳妥。
3.5 完整YAML与验证方法
同样的思路,给db服务写上"仅允许core访问5432"的入口策略;给gateway服务写上"仅允许访问core的8080端口"的出口策略。一个完整的最小闭环就成型了:gateway -> core -> db,其他方向全部静默。
写完策略之后,不要急着说"配好了",要验证。我习惯的流程是:先看策略是否真的作用于目标Pod——kubectl get networkpolicy -n app-prod能看到策略列表;然后找一个临时调试Pod,手动访问目标服务的IP和端口,观察通断。如果是允许方向,应该通;如果不该通的方向通了,说明某条策略的选择器写错了,回去查标签。
验证入口可以这样起一个临时Pod:
kubectl run nettest --image=nicolaka/netshoot -n app-prod --rm -it -- bash进去之后用nc -zv <目标IP> <端口>或curl -v测连通性。netshoot镜像里预装了大量网络排查工具,是我见过最省事的调试方式。
4. 落地过程踩过的坑,按"惨烈程度"排序
4.1 写了策略但流量畅通无阻:CNI开关没开
这是我见过最隐蔽、也最打击人的问题。你明明按文档写好了一条默认拒绝策略,想验证一下防护效果,结果两个Pod之间还能正常通信,仿佛策略不存在。
排查下来原因通常是:CNI插件根本不强制执行NetworkPolicy,或者组件部署时关闭了相关特性。有些老牌网络方案默认就不支持网络策略,有些需要额外的组件或配置才能开启。我的建议是,正式评审之前先做一个"最小网络策略冒烟测试":随便挑两个Pod,下一份全拒绝策略,观察连通性是否被切断。切断说明策略链路生效;没切断就别继续往复杂场景里堆策略了,先解决数据面问题。
另外,有些云厂商托管的K8S服务默认集成了支持策略的CNI组件,但你得在集群创建时或者组件配置里开启安全策略开关,这个开关藏得比较深,文档也不醒目,非常容易漏。
4.2 DNS流量被拦,服务"间歇性"不可达
默认拒绝出口之后,最先断的往往是DNS。一开始你会看到服务报错都是"连接被拒绝""域名解析失败",但不是每次请求都失败,而是时好时坏。这个现象很容易让人去查网络波动、查Service配置,完全想不到是NetworkPolicy。
原因是DNS查询到了缓存,某些记录短时间内不用发DNS请求,所以服务还能正常访问一部分;一旦缓存过期或者解析新域名,请求发不出去,故障就出现了。抓包观察最清楚:源Pod朝kube-dns的53端口发UDP包,但始终得不到响应——数据包被出口方向的策略默默丢弃了。
解决办法前面已经提到:在每个需要域名解析的服务的egress规则里,显式放行到kube-system命名空间中kube-dns的TCP/UDP 53端口。如果你的集群里DNS部署在其他命名空间,或者用了自建DNS,那选择器要相应调整。
4.3 egress方向拦住了节点健康检查和日志采集
集群里除了业务流量,还有很多"基础设施流量",比如节点上的kubelet检查Pod存活,监控Agent采集Metrics,日志Agent往日志服务发数据。这些都从节点网段发起,目标或源头仍是Pod。如果你在某个命名空间上加了一条很严的出口拦截,很可能会把这些基础设施的流量一起拦住。
这时候ipBlock就派上用场了。我们可以在策略里额外放行节点网段或监控基础设施所在网段。例如在命名空间的egress里加上:
- to: - ipBlock: cidr: 10.0.0.0/8注意,这条放行是针对整个保留网段的,粒度比较粗,但如果你的节点网段是独立的,那放在这里问题不大。production环境最好单独查清楚监控Agent跑在哪、用的什么源IP,然后精确定位到网段,别偷懒放一个大段,否则等于直接把出口策略拆了一半。
4.4 namespaceSelector为空的两种语义
namespaceSelector是一个特别容易被误解的字段。它的空值有两种完全不同的含义:
- 如果
namespaceSelector: {},选中"所有命名空间"。 - 如果字段根本不存在,那这一项就不会对命名空间做过滤,相当于没有这个条件。
在from里尤其容易混淆。假设我想写"允许所有命名空间里所有Pod访问我",namespaceSelector: {}就能做到。但如果我漏写了namespaceSelector,只写了一个空podSelector,那语义就退化成"只允许同命名空间里所有Pod访问我"。这两种结果差别很大,尤其是当你的服务被跨命名空间调用时,一定会踩坑。
我建议在写跨命名空间策略时,明确写出namespaceSelector,并且加上kubernetes.io/metadata.name标签,让语义一目了然,不要依赖裸的matchLabels。
4.5 端口协议匹配过于严格导致的失败
NetworkPolicy的ports字段要求协议和端口精确匹配。如果你只写了protocol: TCP, port: 8080,那么UDP流量哪怕发到同一端口也不会被放行。有些服务实际上是UDP协议(比如DNS、某些视频流、日志传输),但策略写成TCP,结果流量被拦得很彻底。
另一个容易忽略的点是:NetworkPolicy里的podSelector选中Pod后,ingress规则只影响目标端口。如果客户端访问的是ClusterIP通过Service转发,最终落地的还是Pod的IP+端口,所以你得确认Service的目标端口和Pod监听端口一致,否则策略写的是8080,Pod实际监听9090,流量一样到不了。
5. 断连之后的排查链路:从怀疑到定位
5.1 先别改代码,先把策略白板画出来
我见过太多人一看到"连不上"就先去改应用配置、重启服务,其实网络策略导致的断连,特征非常明显。断连出现后第一步应该做的不是动业务,而是把当前命名空间内的策略全部拉出来,画一张白板:每个服务的Pod被哪几条策略选中,入口和出口各放行了哪些源和目标。
白板往往几秒钟就能暴露问题。比如策略里的podSelector标签写的是app=core-service,但实际Pod的标签是app=core,那策略根本没生效。这种标签错位在改动Pod模板时特别容易发生——有人改了Deployment的labels,忘了同步改NetworkPolicy。
画白板的同时,用kubectl describe networkpolicy看一眼每条策略的details,重点检查选择器到底命中了哪些Pod。别忘了还有一点:NetworkPolicy是命名空间级的,如果你的客户端和目标服务不在同一命名空间,跨命名空间的规则必须写对namespaceSelector,否则策略作用域压根覆盖不到。
5.2 用临时调试Pod验证连通性
理论分析再完美,最终还是要靠探针说话。我的标准做法是,用一个带全套网络工具的临时Pod,部署在与目标服务相同的命名空间里,然后分方向测:
# 测试入口方向 nc -zv <目标服务ClusterIP> <端口> curl -v http://<目标Pod IP>:<端口> # 同时观察自己的出口能否访问DNS nslookup <服务名>如果直接部署在目标命名空间里测不通,先不要怀疑NetworkPolicy,也可能是Service没选到Pod或者Pod端口异常。所以我一般会先拿到目标Pod的真实IP,绕过Service直连Pod测一遍。这一步能滤掉很多干扰项:如果直连Pod通,问题大概率不在应用而在Service转发链路上;如果直连也不通,那就回到网络策略排查上。
5.3 通过数据面日志和计数器归零嫌疑
当策略数量多的时候,光靠推理很难快速定位是哪条拦了。这时候就要借助CNI数据面的观测能力。有些实现了网络策略的组件会在日志里输出"该流量因策略被拒绝"的记录,或者提供策略命中次数的计数器。你可以通过监控面板看某个方向一段时间内的策略命中量。
如果没有观测能力,也可以用"隔离变量"的方式:临时创建一个完全不带网络策略干扰的新命名空间,把客户端和目标服务的最小副本跑进去,看流量是否恢复正常。如果恢复,那基本确认是策略问题;如果在纯净环境里也不通,那就要看应用本身、网络插件、甚至节点路由了。这个二分法做下来,往往比满屏日志更高效。
5.4 二分放行法与最终定位
最后分享一个我自己很喜欢的排查技巧——二分放行法。当不能精确判断是哪条策略导致断连时,不要一条条去改,那样影响面太大。做法是:先复制一份和当前一模一样的策略集,然后只对目标Pod应用一个"全放行"策略(允许所有入口所有出口),看流量是否恢复。
如果恢复了,说明问题肯定在策略集合里;接下来把全放行策略从"允许所有"改成"允许大部分、拒绝一小部分",逐步缩小范围,直到复现问题。这个过程类似二分查找,每做一轮就能排除一半嫌疑。
要注意的是,全放行策略本身也是一条NetworkPolicy,它和普通策略一样叠加生效。因为策略是并集逻辑,一条全放行策略会把其他策略的拒绝效果"冲掉",所以它适合做排查用的"临时开关",但绝对不能留到生产里。
6. 维护网络策略的几点心得
6.1 把策略当代码管理
网络策略很容易变成一团没人敢动的历史包袱,尤其是当集群里跑了几十个命名空间之后,每一份策略是谁加的、为什么加、现在还生不生效,基本没人说得清。我的习惯是把所有NetworkPolicy像应用代码一样纳入版本管理,每个策略文件带上注释,写明适用的服务、放行的方向、关联的需求工单号。这样半年后回头看,至少能知道当初为什么开这个口子。
另外,尽量把策略拆细而不是堆大。一个大策略里塞十几个from/to,虽然看着省事,但排障时根本不知道是哪一条在起作用。按服务维度拆分之后,每一条的作用域很清楚,出问题时定位到对应服务即可。
6.2 先白名单后放行的落地节奏
给生产环境上网络策略,最怕的就是步子太大。我建议按三个阶段推进:先在测试或预发环境里启用默认拒绝策略,跑一轮完整的业务回归,把断掉的不合规调用找出来;再逐步加放行策略,每次加完都要验证对应的链路是通的;最后才在production逐步推行,并且最好先在业务低峰期操作,准备好一键回滚方案。
还有一个经验是:不要把"默认拒绝"和"逐步放行"放在同一次变更里。先只加放行策略(这时因为还没有默认拒绝,放行策略形同虚设,但可以检查选择器是否正确);确认无误后,第二次变更才加上默认拒绝策略。这样即使出了问题,回滚也很干净。
6.3 监控与告警
最后说一个经常被忽略的部分:网络策略本身也需要监控。至少要把策略数量和策略变更记录纳入审计,最好能对关键服务的网络策略命中量做展示。因为策略改变导致的断连,往往不是马上爆发,而是在某个流量路径变化后突然出现。有了监控,断连出现时你至少能确认"之前策略一直没变,问题不在新增策略上",排障方向就能更快收敛。
我现在每次在群里看到"某服务突然连不上"的告警,第一反应已经习惯性先问一句:最近有没有人动过NetworkPolicy。你也会慢慢养成这个习惯的。