Kubernetes 基础体系 · 第 31/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes NetworkPolicy:Selector、Ingress、Egress、默认拒绝和验证

NetworkPolicy 是 Kubernetes 用来声明 Pod 网络访问边界的 API。它描述的是:

  • 哪些 Pod 被策略选中;
  • 被选中的 Pod 允许哪些入站连接(Ingress);
  • 被选中的 Pod 允许哪些出站连接(Egress);
  • 连接的来源或目标如何通过 Selector、IP 网段和端口表达。

它不是防火墙规则的直接执行器。Kubernetes API Server 只保存并分发策略,真正拦截流量的是 CNI 网络插件或其 NetworkPolicy 实现。因此,创建成功的 NetworkPolicy 不等于网络访问已经被限制。

本文使用稳定的 networking.k8s.io/v1 API,示例适用于支持 Kubernetes NetworkPolicy 的 CNI 实现。不同 CNI 在主机网络、Service IP、外部流量、日志和扩展能力上的行为可能不同。


一、先建立正确的模型:策略保护的是 Pod,而不是 Service

一个 NetworkPolicy 是命名空间级资源:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-from-frontend
  namespace: app
spec:
  podSelector:
    matchLabels:
      app: api

这里的 podSelector 选中了 app 命名空间中标签为 app: api 的 Pod。它没有直接选择:

  • Service;
  • Deployment;
  • Namespace 资源本身;
  • 节点;
  • 用户身份;
  • HTTP URL;
  • DNS 名称。

Service 只是将访问者转发到后端 Pod。NetworkPolicy 最终通常作用于 Pod 的网络接口,因此访问 api.app.svc 后,真正需要被允许的是到后端 Pod 的连接。

可以把一条连接抽象为:

C=(S,D,P,T)C = (S, D, P, T)

其中:

  • SS:源端点,通常是源 Pod;
  • DD:目标端点,通常是目标 Pod;
  • PP:传输层协议,例如 TCP 或 UDP;
  • TT:目标端口。

NetworkPolicy 主要回答两个问题:

  1. 目标 Pod 是否允许来自 SS 的 Ingress;
  2. 源 Pod 是否允许发往 DD 的 Egress。

对于 Pod 到 Pod 的连接,通常必须同时满足两端的限制:

Allow(C)=AllowIngressD(C)AllowEgressS(C)Allow(C) = AllowIngress_D(C) \land AllowEgress_S(C)

这意味着:

  • 目标 Pod 的 Ingress 允许,但源 Pod 的 Egress 被拒绝:连接失败;
  • 源 Pod 的 Egress 允许,但目标 Pod 的 Ingress 被拒绝:连接失败;
  • 一端没有对该方向启用隔离时,该方向不会因为“没有规则”自动拒绝;
  • 两端都允许时,连接才具备通过 NetworkPolicy 的条件。

这不是对所有网络路径和所有 CNI 实现的完整描述,尤其是主机网络、外部地址和 NAT 会改变可观察到的源地址,但它是理解普通 Pod-to-Pod 流量的核心模型。


二、NetworkPolicy 的执行前提:API 成功不代表策略生效

Kubernetes 定义了 NetworkPolicy API 的语义,但不负责强制执行。集群必须使用支持 NetworkPolicy 的网络插件,例如某些 CNI 的策略实现。

如果插件不支持 NetworkPolicy,可能出现以下情况:

kubectl apply -f policy.yaml
networkpolicy.networking.k8s.io/deny-all created

资源创建成功,但 Pod 之间仍然可以互相访问。因为:

  1. API Server 接受了合法的对象;
  2. 控制面把对象暴露给集群;
  3. CNI 没有将对象转换为数据面规则;
  4. 数据包没有被丢弃。

因此验证必须包含真实连接测试,不能只执行:

kubectl get networkpolicy -A

常见 CNI 还可能提供额外能力,例如:

  • NetworkPolicy 事件或拒绝日志;
  • 更细粒度的身份策略;
  • FQDN、HTTP、DNS-aware 规则;
  • 节点级或主机端策略。

这些属于插件扩展,不是 Kubernetes networking.k8s.io/v1 基础 API 的统一保证。


三、Selector:策略选择谁,以及规则允许谁

3.1 spec.podSelector:选择被保护的 Pod

策略的顶层 spec.podSelector 决定该策略作用于哪些 Pod。它总是在策略所在的 Namespace 中解析。

spec:
  podSelector:
    matchLabels:
      app: api

含义是:

该 Namespace 中,标签 app=api 的 Pod 受到此策略影响。

空选择器表示当前 Namespace 中的所有 Pod:

spec:
  podSelector: {}

它不是“没有选择对象”,而是“匹配全部对象”。

例如:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-api-ingress
  namespace: app
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress: []

只有 app=api 的 Pod 被拒绝入站,app=web 的 Pod 不受这条策略影响。


3.2 podSelectornamespaceSelectoripBlock

ingress.fromegress.to 中,常见的来源或目标条件有三类:

from:
  - podSelector: ...
  - namespaceSelector: ...
  - ipBlock: ...

仅使用 podSelector

ingress:
  - from:
      - podSelector:
          matchLabels:
            app: frontend

这里的 podSelector 选择的是策略所在 Namespace 中的 Pod。

如果策略位于 app Namespace:

app/frontend Pod  ->  app/api Pod

可以匹配;frontend Namespace 中的同名 Pod 不匹配。

这是常见误解:podSelector 不会跨 Namespace 搜索。

仅使用 namespaceSelector

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            team: frontend

它表示允许来自满足 team=frontend 的所有 Namespace 的 Pod。Namespace 必须具有对应标签,例如:

kubectl label namespace frontend team=frontend

同一个列表项中的两个 Selector 是 AND

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            team: frontend
        podSelector:
          matchLabels:
            app: web

这表示:

Source=Namespace(team=frontend)Pod(app=web)Source = Namespace(team=frontend) \land Pod(app=web)

也就是只允许:

frontend Namespace 中 app=web 的 Pod

不是“允许所有 team=frontend Namespace,加上当前 Namespace 中所有 app=web Pod”。

两个列表项通常是 OR

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            team: frontend
      - podSelector:
          matchLabels:
            app: monitoring

逻辑上是:

Source=Namespace(team=frontend)Pod(app=monitoring)Source = Namespace(team=frontend) \lor Pod(app=monitoring)

前一个列表项允许匹配 Namespace 中的 Pod,后一个列表项允许策略所在 Namespace 中匹配 app=monitoring 的 Pod。

为了避免 YAML 缩进造成错误,建议明确写成:

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            team: frontend
      - podSelector:
          matchLabels:
            app: monitoring

而不是把两个选择器嵌套在同一个 - 项中。


3.3 空的 namespaceSelector 和空的 podSelector

以下两个写法含义不同:

- namespaceSelector: {}

表示所有 Namespace 中的 Pod。

- podSelector: {}

表示策略所在 Namespace 中的所有 Pod。

如果二者放在同一个 from 项中:

- namespaceSelector: {}
  podSelector: {}

它表示所有 Namespace 中的所有 Pod,因为两个条件都匹配全部对象。


3.4 ipBlock:按 CIDR 匹配地址

ipBlock 用于按 IP 网段匹配:

ingress:
  - from:
      - ipBlock:
          cidr: 203.0.113.0/24
          except:
            - 203.0.113.10/32

含义是允许:

203.0.113.0/24203.0.113.10/32203.0.113.0/24 - 203.0.113.10/32

except 必须是 cidr 的子网范围,否则对象不符合 API 约束或无法表达预期逻辑。

ipBlock 常用于:

  • 外部客户端网段;
  • 管理网络;
  • 固定的办公出口地址;
  • 指定的节点或基础设施网段。

但需要注意几个边界:

  1. 经过 Service、NodePort、LoadBalancer 或 NAT 后,策略看到的源地址可能不是客户端原始地址;
  2. Pod IP 是否适合用 ipBlock 匹配,取决于实现;Kubernetes 对 Pod IP 与 ipBlock 的交互存在实现差异;
  3. ipBlock 不等于“按域名匹配”;
  4. 动态变化的云出口 IP 不适合静态写入 CIDR;
  5. 外部流量的实际地址可能因 kube-proxy、云负载均衡器或 CNI 路径而不同。

当通信对象是集群内 Pod,优先使用 namespaceSelectorpodSelector,而不是硬编码 Pod CIDR。


四、Ingress 和 Egress:两个独立的隔离方向

4.1 Ingress 是进入被选中 Pod 的流量

Ingress 描述目标 Pod 接收哪些流量:

spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

这条策略的条件是:

  • 目标 Pod 位于策略 Namespace;
  • 目标 Pod 有 app=api
  • 源 Pod 位于同一 Namespace;
  • 源 Pod 有 app=frontend
  • 使用 TCP;
  • 目标端口是 8080。

它允许的是“进入 API Pod 的 TCP/8080”,不是“允许 frontend Pod 访问 API 服务的所有端口”。

端口规则可以省略:

ingress:
  - from:
      - podSelector:
          matchLabels:
            app: frontend

此时表示来自该来源的所有端口,而不是拒绝所有端口。

from 可以省略:

ingress:
  - ports:
      - protocol: TCP
        port: 8080

这表示所有来源都可以访问 TCP/8080。

ingress: [] 则相反,表示没有允许的 Ingress 规则:

spec:
  policyTypes:
    - Ingress
  ingress: []

它常用于默认拒绝。


4.2 Egress 是被选中 Pod 发出的流量

Egress 描述源 Pod 可以发往哪里的流量:

spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: database
      ports:
        - protocol: TCP
          port: 5432

它允许 app=api 的 Pod 发往同一 Namespace 中 app=database Pod 的 TCP/5432。

Egress 规则中的端口是目标端口。例如:

ports:
  - protocol: TCP
    port: 443

允许的是发往目标 TCP/443,不是源 Pod 的临时端口。

如果 Egress 规则中没有 to

egress:
  - ports:
      - protocol: UDP
        port: 53

表示允许发往任何目标的 UDP/53。

如果没有 ports

egress:
  - to:
      - namespaceSelector:
          matchLabels:
            team: observability

表示允许发往目标 Namespace 中匹配的 Pod 的所有端口。


4.3 Ingress 与 Egress 必须分别分析

考虑三个 Pod:

frontend  --TCP/8080-->  api  --TCP/5432-->  database

要让第一条连接成立,至少需要:

api 的 Ingress 允许 frontend -> api
frontend 的 Egress 允许 frontend -> api(如果 frontend 启用了 Egress 隔离)

要让第二条连接成立,至少需要:

database 的 Ingress 允许 api -> database
api 的 Egress 允许 api -> database

因此,只给 api 写一条 Egress 规则并不能自动让 frontend 访问 api。同样,只给 database 写 Ingress 规则,也不能绕过 api 的 Egress 限制。


五、策略的组合规则:允许规则是并集,不是覆盖替换

同一 Pod 可以被多个 NetworkPolicy 选中:

policy-a:
  允许 app=frontend 访问 TCP/8080

policy-b:
  允许 app=monitoring 访问 TCP/9090

最终允许集合是并集:

Allowed(Pod)=Allowed(policy-a)Allowed(policy-b)Allowed(Pod) = Allowed(policy\text{-}a) \cup Allowed(policy\text{-}b)

因此:

  • 添加一条策略通常会扩大允许集合,不能用后一条策略“覆盖”前一条;
  • 删除其中一条策略只会撤销该策略贡献的允许项;
  • 只有至少一条策略为某方向选中 Pod 后,该方向才进入隔离状态;
  • 在隔离方向中,未被任何策略允许的连接被拒绝。

可以将某个 Pod 的 Ingress 状态表示为:

未被任何 Ingress 策略选中
    => Ingress 不隔离

至少被一条 Ingress 策略选中
    => 只有所有匹配策略允许的来源/端口可以进入

同理适用于 Egress。

一个典型反例是:

# 策略 A
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend

# 策略 B
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: monitoring

结果不是“只允许 monitoring”,而是同时允许:

frontend -> api
monitoring -> api

因为两个策略的允许集合求并集。


六、默认拒绝:必须理解方向和空规则的区别

6.1 默认拒绝所有 Ingress

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: app
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress: []

逐项解释:

  • podSelector: {}:选择 app Namespace 中所有 Pod;
  • policyTypes: [Ingress]:声明只启用入站隔离;
  • ingress: []:没有任何允许的入站规则;
  • 结果:这些 Pod 的 Ingress 默认拒绝。

它不会限制这些 Pod 发出的流量。


6.2 默认拒绝所有 Egress

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: app
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress: []

它只隔离出站流量。Pod 仍可能接收入站连接,前提是没有其他 Ingress 策略限制它。


6.3 默认拒绝 Ingress 和 Egress

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: app
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress: []
  egress: []

这会对 Namespace 中所有 Pod 启用两个方向的隔离,并且两个方向都没有允许项。

实际部署时,必须先规划必要的例外,否则可能同时阻断:

  • Service 访问;
  • DNS 解析;
  • 数据库连接;
  • 监控抓取;
  • 日志或指标上报;
  • Admission Webhook 或控制器需要的回调。

6.4 为什么 Egress 默认拒绝经常导致 DNS 失败

应用通常先解析域名:

api Pod -> kube-dns/CoreDNS Service -> DNS Pod

DNS 通常使用:

  • UDP/53;
  • TCP/53,某些响应过大或特定场景需要 TCP。

因此,启用 Egress 默认拒绝后,通常要额外允许 DNS。一个通用写法是按 Namespace 和标签选择 DNS Pod:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: app
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

这里使用了两个条件的 AND:

Namespace 名称为 kube-system
AND
Pod 标签 k8s-app=kube-dns

但 DNS 标签和部署位置可能因发行版、集群安装方式或 CNI 方案不同而变化。应先检查实际对象:

kubectl -n kube-system get pods --show-labels
kubectl -n kube-system get svc kube-dns -o yaml

仅允许访问 DNS Pod 还存在一个实现边界:应用可能访问的是 DNS Service IP,数据包在不同阶段可能经过 kube-proxy、eBPF Service 转换或其他 NAT。多数支持良好的 CNI 能正确处理这一场景,但生产环境仍应通过实际解析测试验证。


七、policyTypes、字段省略与空列表

7.1 显式声明 policyTypes 更容易审查

建议在生产策略中显式写出:

policyTypes:
  - Ingress
  - Egress

这样读者可以直接知道策略隔离哪些方向。

Kubernetes API 对省略字段有默认推导行为:

  • 如果存在 ingress 规则,通常会包含 Ingress
  • 如果存在 egress 规则,通常会包含 Egress
  • 只写 policyTypes: [Egress] 而省略 egress,表达的是启用 Egress 隔离但没有允许项;
  • 只写 policyTypes: [Ingress] 而省略 ingress,表达的是启用 Ingress 隔离但没有允许项。

为了避免代码审查时误判,默认拒绝策略应明确写空数组:

ingress: []
egress: []

以下写法的语义不同:

# 没有 ingress 字段,不能仅凭视觉判断规则意图
spec:
  podSelector: {}
  policyTypes:
    - Ingress

它依赖 API 语义来表达“没有允许规则”。虽然可以成立,但不如显式的 ingress: [] 清晰。


7.2 空列表与缺失列表不是工程上可互换的表达

下面两种规则分别表示:

ingress: []

没有任何允许的 Ingress。

ingress:
  - {}

允许所有来源、所有端口的 Ingress,因为该规则没有 fromports 限制。

这是一个危险反例:

spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - {}

它不是默认拒绝,而是对所有被选 Pod 放行所有入站流量。

同样:

egress:
  - {}

表示允许所有目的地、所有端口的 Egress。


八、完整算例:构造一个最小的三层访问模型

下面创建三个 Namespace:

frontend:前端 Pod
app:API Pod
data:数据库 Pod

先给 Namespace 打标签:

kubectl create namespace frontend
kubectl create namespace app
kubectl create namespace data

kubectl label namespace frontend tier=frontend
kubectl label namespace app tier=app
kubectl label namespace data tier=data

部署示例 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: frontend
  namespace: frontend
  labels:
    app: frontend
spec:
  containers:
    - name: main
      image: nginx:1.27
      ports:
        - containerPort: 80
---
apiVersion: v1
kind: Pod
metadata:
  name: api
  namespace: app
  labels:
    app: api
spec:
  containers:
    - name: main
      image: nginx:1.27
      ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Pod
metadata:
  name: database
  namespace: data
  labels:
    app: database
spec:
  containers:
    - name: main
      image: postgres:16
      ports:
        - containerPort: 5432

这里仅用于演示标签和连接路径。nginx 默认并不监听 8080,PostgreSQL 也可能需要初始化配置;因此真实测试应使用实际监听端口的镜像或测试服务器。NetworkPolicy 只控制连接,不会让应用自动开始监听某个端口。

8.1 给 API 设置默认拒绝,再允许前端访问

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-ingress
  namespace: app
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              tier: frontend
          podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

规则要求:

SourceNamespace.tier=frontendSourcePod.app=frontendProtocol=TCPDestinationPort=8080SourceNamespace.tier = frontend \land SourcePod.app = frontend \land Protocol = TCP \land DestinationPort = 8080

因此:

frontend/frontend Pod -> app/api Pod:8080

可以通过 API 的 Ingress 条件;但如果 frontend Pod 自己启用了 Egress 隔离,则它还需要拥有相应 Egress 允许规则。

8.2 给 API 设置出站数据库访问

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-egress
  namespace: app
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              tier: data
          podSelector:
            matchLabels:
              app: database
      ports:
        - protocol: TCP
          port: 5432

这条规则只允许 API 发往:

data Namespace 中 app=database 的 Pod 的 TCP/5432

它不会允许:

  • API 访问 data Namespace 中其他 Pod;
  • API 访问数据库的其他端口;
  • API 访问互联网;
  • 数据库主动访问 API。

最后一种方向需要分析数据库的 Egress 和 API 的 Ingress。


九、把策略翻译成数据流,而不是只看 YAML

对一条连接:

frontend Pod -> api Pod:8080

应按以下顺序检查:

第一步:确认目标 Pod 是否被策略选中

kubectl -n app get pod api --show-labels
kubectl -n app get networkpolicy api-ingress -o yaml

确认:

api Pod 是否满足 app=api

如果不满足,这条策略完全不影响它。

第二步:确认来源是否满足 from

检查来源 Namespace:

kubectl get namespace frontend --show-labels

检查来源 Pod:

kubectl -n frontend get pod frontend --show-labels

必须同时满足:

frontend Namespace: tier=frontend
frontend Pod: app=frontend

第三步:确认协议和目标端口

NetworkPolicy 的 port: 8080 是目标端口。若应用实际监听 80,访问 80 不会因为规则写了 8080 而成功。

第四步:检查源 Pod 的 Egress

如果源 Pod 被任何 Egress 类型策略选中,则它的 Egress 也必须允许目标:

kubectl -n frontend get networkpolicy
kubectl -n frontend describe networkpolicy

第五步:检查实际路径

如果请求通过 Service、Ingress、NodePort 或外部负载均衡器,源地址和目标地址可能经过转换。应确认:

kubectl get svc -A
kubectl get endpointslice -A

Service 的 selector、EndpointSlice 和 NetworkPolicy 是不同层次的对象:Service 决定转发目标,NetworkPolicy 决定连接是否被允许。


十、验证:从对象检查到真实连接测试

10.1 检查策略对象是否被正确解析

kubectl -n app get networkpolicy
kubectl -n app describe networkpolicy api-ingress
kubectl -n app get networkpolicy api-ingress -o yaml

describe 适合人工检查:

  • Policy Types;
  • Pod Selector;
  • Ingress/Egress 规则;
  • 端口;
  • 来源或目标选择器。

get -o yaml 适合核对 API Server 实际保存的字段和默认值。

检查选择器能选中哪些 Pod:

kubectl -n app get pod -l app=api
kubectl -n frontend get pod -l app=frontend
kubectl get namespace -l tier=frontend

如果选择结果为空,策略可能是正确创建的,但没有作用于预期对象。

10.2 使用临时测试 Pod

可以在测试环境创建带网络工具的 Pod。镜像名称应根据组织镜像仓库和安全要求选择,例如:

kubectl -n frontend run net-test \
  --image=nicolaka/netshoot \
  --restart=Never \
  --command -- sleep 3600

进入测试 Pod:

kubectl -n frontend exec -it net-test -- sh

测试 TCP:

nc -vz -w 3 api.app.svc.cluster.local 8080

可能结果:

succeeded

表示 TCP 连接建立;或者:

timed out

通常表示数据包被丢弃、路径不可达或目标没有响应。connection refused 的含义不同:它通常表示已经到达目标网络栈,但目标端口没有监听或主动拒绝,不能简单归因于 NetworkPolicy。

测试 DNS:

nslookup api.app.svc.cluster.local

或者:

dig +short api.app.svc.cluster.local

如果 DNS 失败而直接访问 IP 成功,优先检查 Egress 到 DNS 的 UDP/TCP 53,而不是先修改 API Ingress。

10.3 用正向和反向测试验证边界

验证“允许”不能证明“拒绝”有效。至少需要构造:

允许来源 -> 允许端口       应成功
允许来源 -> 未允许端口     应失败
未允许来源 -> 允许端口     应失败
允许来源 -> 错误 Namespace  应失败

例如 API Ingress 允许 tier=frontendapp=frontend 访问 TCP/8080,则应另外创建:

apiVersion: v1
kind: Pod
metadata:
  name: other-client
  namespace: app
  labels:
    app: other-client
spec:
  containers:
    - name: main
      image: nicolaka/netshoot
      command: ["sleep", "3600"]

从该 Pod 访问 API:

kubectl -n app exec -it other-client -- \
  nc -vz -w 3 api.app.svc.cluster.local 8080

它不满足:

Namespace tier=frontend
AND Pod app=frontend

因此,在目标应用确实监听 8080、CNI 支持策略且没有其他放行策略时,该连接应失败。

10.4 观察拒绝原因

NetworkPolicy 本身通常不规定统一的拒绝日志格式。诊断来源可能包括:

  • CNI 插件的流量日志;
  • 节点上的 eBPF 或 iptables 规则;
  • Pod 内应用日志;
  • CoreDNS 日志;
  • Service 和 EndpointSlice 状态;
  • 云负载均衡器或安全组日志。

不能只依赖 kubectl describe networkpolicy 找“被拒绝的连接记录”,因为 Kubernetes 基础 API 没有统一的 NetworkPolicy 流量审计接口。


十一、默认拒绝的部署顺序和故障恢复

默认拒绝的危险之处在于它可能同时影响多个依赖。一个更可控的变化过程是:

  1. 先盘点应用依赖;
  2. 先部署明确的允许规则;
  3. 在非生产或单个 Namespace 验证;
  4. 再启用对应方向的默认拒绝;
  5. 观察 DNS、服务调用、监控和控制器行为;
  6. 失败时按 Namespace 和策略对象回滚。

删除策略可以快速恢复该策略贡献的隔离状态:

kubectl -n app delete networkpolicy default-deny-all

但删除一条策略不一定恢复所有流量,因为:

  • 其他策略仍可能启用同方向隔离;
  • CNI 可能有额外的扩展策略;
  • Service、路由、DNS 或应用本身也可能存在故障;
  • Egress 和 Ingress 是独立方向,删除 Ingress 策略不会撤销 Egress 限制。

更安全的恢复动作通常是先确认当前对象:

kubectl -n app get networkpolicy -o wide
kubectl -n app get networkpolicy -o yaml

再针对造成问题的 Namespace 或策略处理,而不是直接删除整个集群中的所有策略。


十二、常见误解和失败表现

12.1 “写了允许规则,就自动允许返回流量”

NetworkPolicy 的规则按 Ingress 和 Egress 分别判断。一次由客户端发起的 TCP 连接可能需要:

  • 客户端 Egress;
  • 服务端 Ingress。

实现通常会对已经允许的连接处理返回方向,以使 TCP 连接正常工作,但不能把这理解为“任意反向新连接都会被允许”。一个新的、方向相反的连接仍需按其自己的源、目标和策略判断。

12.2 “NetworkPolicy 可以按 URL、域名或 HTTP 方法控制”

基础 NetworkPolicy 主要提供:

  • Pod/Namespace Selector;
  • IP CIDR;
  • TCP、UDP、SCTP 端口。

它不能表达:

只允许 GET /v1/users
只允许 Host: api.example.com
只允许某个 DNS 名称

这些属于 L7 代理、Service Mesh、API Gateway、Ingress Controller 或 CNI 扩展能力。即使使用域名解析结果写入 IP,也会遇到地址变化、缓存和 NAT 问题。

12.3 “Service 名称可以直接写到 NetworkPolicy”

不能把以下内容写进基础 NetworkPolicy:

to:
  - serviceName: database

应选择后端 Pod 的 Namespace 和标签。Service 的 selector 必须与后端 Pod 标签、NetworkPolicy 的目标选择器分别核对。

12.4 “默认拒绝会保护整个 Namespace 的所有网络资源”

podSelector: {} 只选择该 Namespace 中的 Pod。它不会自动限制:

  • 其他 Namespace 的 Pod;
  • 节点进程;
  • 主机网络 Pod;
  • Kubernetes API Server;
  • 云平台安全组;
  • 外部负载均衡器;
  • 未被 CNI 策略实现覆盖的特殊路径。

多租户隔离因此不是单靠 NetworkPolicy 完成的。Namespace、RBAC、资源配额、节点隔离、云网络控制和审计分别解决不同边界。

12.5 “策略 YAML 合法,所以网络一定按预期工作”

合法性只说明 API 对象通过了 schema 和基本校验。仍需检查:

  • CNI 是否启用 NetworkPolicy;
  • 目标 Pod 是否被选择;
  • 来源 Namespace 是否有正确标签;
  • 应用是否监听规则端口;
  • Service 是否有 EndpointSlice;
  • DNS 是否被 Egress 放行;
  • NAT 或入口路径改变了源地址;
  • 是否存在其他策略形成允许并集。

十三、特殊路径和实现差异

13.1 hostNetwork Pod

使用 hostNetwork: true 的 Pod 共享节点网络命名空间,不等同于普通 Pod 网络接口。其 NetworkPolicy 行为与普通 Pod 可能不同,具体取决于 CNI 和实现。

如果安全边界依赖 NetworkPolicy,不能默认认为它覆盖了所有主机网络流量。应单独检查:

  • CNI 对 host-network Pod 的支持;
  • 节点防火墙;
  • 主机级策略;
  • 云安全组;
  • 该工作负载是否真的需要 hostNetwork。

13.2 外部地址、NodePort 和 LoadBalancer

当客户端通过 NodePort 或云负载均衡器访问服务时,目标 Pod 看到的源地址可能是:

  • 客户端原始地址;
  • 节点地址;
  • 负载均衡器地址;
  • 经过 SNAT 后的地址。

因此用 ipBlock 允许办公网段时,必须从实际数据包路径和 CNI 文档确认可见地址。不能仅根据客户端浏览器显示的公网 IP 推断 Pod 侧一定看到相同地址。

13.3 Pod IP、Overlay 和 Underlay 的关系

NetworkPolicy 依赖 CNI 管理的 Pod 网络。Pod IP 可能来自:

  • Overlay 网络中的虚拟地址;
  • Underlay 网络中的可路由地址;
  • 云厂商 VPC 子网;
  • CNI 自己维护的地址池。

这影响路由、NAT、可见源地址和策略实现,但不改变 Selector 的基本语义:Selector 匹配的是 Kubernetes 对象标签,而不是根据 IP 反查 Pod 身份。

直接使用 Pod CIDR 作为长期安全身份通常不稳定,因为 Pod 重建后 IP 可能变化。对于集群内工作负载,标签和 Namespace 身份通常更适合表达意图;对于固定外部网络,CIDR 才更自然。


十四、生产设计中的边界:身份、网络和权限是三个不同问题

NetworkPolicy 解决的是网络连接边界,不解决 Kubernetes API 权限。

例如:

  • RBAC 决定用户是否可以读取 Secret、创建 Pod 或修改 NetworkPolicy;
  • NetworkPolicy 决定 Pod 网络是否可以连接另一个端点;
  • ResourceQuota 限制 Namespace 的资源使用;
  • Pod Security 或准入策略限制工作负载配置;
  • 云安全组和节点防火墙控制更外层的网络边界。

一个用户不能因为 Pod 无法访问 API Server 就自动失去 Kubernetes API 权限;同样,拥有 kubectl 权限也不代表某个 Pod 网络一定能访问数据库。

在多租户环境中,需要分别回答:

  1. 租户能否创建或修改 NetworkPolicy;
  2. 租户能否给 Namespace 打标签;
  3. 租户能否创建带任意标签的 Pod;
  4. CNI 是否能隔离节点、主机网络和外部入口;
  5. 默认拒绝策略是否覆盖所有租户 Namespace;
  6. 是否存在管理员或系统组件需要的例外路径。

如果租户可以自由修改策略,NetworkPolicy 不能作为管理员强制的单一隔离边界;还需要 RBAC、准入控制和平台级策略防止租户撤销保护。


十五、一份可审查的最小模板

下面模板同时表达:

  • 保护当前 Namespace 的所有 Pod;
  • 默认拒绝 Ingress;
  • 默认拒绝 Egress;
  • 允许 DNS;
  • 允许来自 frontend 团队前端 Pod 的 API 入站;
  • 允许 API 访问 data 团队数据库;
  • 具体标签需按集群实际对象调整。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: app-default-deny
  namespace: app
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress: []
  egress: []
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-from-frontend
  namespace: app
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              tier: frontend
          podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-database
  namespace: app
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              tier: data
          podSelector:
            matchLabels:
              app: database
      ports:
        - protocol: TCP
          port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-from-app
  namespace: app
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

这个模板仍然不是完整生产策略,因为 frontend Namespace 中的 Pod 如果也启用了 Egress 默认拒绝,还需要允许它们访问 API;data Namespace 中的数据库 Pod 如果启用了 Ingress 默认拒绝,则还需要在 data Namespace 编写允许 API 来源的 Ingress 策略。

因此,NetworkPolicy 的正确审查方式不是逐条读 YAML,而是对每一条业务连接建立完整条件:

业务连接成功=源 Pod Egress 允许目标 Pod Ingress 允许路由可达目标端口有监听DNS、Service、NAT 路径正确\text{业务连接成功} = \text{源 Pod Egress 允许} \land \text{目标 Pod Ingress 允许} \land \text{路由可达} \land \text{目标端口有监听} \land \text{DNS、Service、NAT 路径正确}

其中前两项属于 NetworkPolicy 主要负责的边界,后三项属于 CNI、Service、应用和底层网络共同决定的运行条件。只有把策略语义与真实数据流、选择器结果和连接测试结合起来,才能判断一个 NetworkPolicy 是否真正实现了预期的隔离。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。