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

Kubernetes 出站流量治理:Egress、NAT、代理、DNS、白名单和审计

Kubernetes 中的出站流量(egress),是指 Pod 发起、离开自身网络命名空间并访问其他目标的流量。目标可以是:

  • 集群内其他 Pod;
  • Kubernetes Service;
  • 集群外部的数据库、对象存储、支付接口或 SaaS API;
  • DNS 服务;
  • HTTP/HTTPS 代理;
  • Service Mesh 的出站网关。

出站治理不是单一功能,而是一条由多个组件共同决定的路径:

flowchart LR
    A[应用容器] --> B[Pod 网络命名空间]
    B --> C[Sidecar 或本地代理]
    C --> D[NetworkPolicy 执行点]
    D --> E[Node 路由与 NAT]
    E --> F[云 NAT / 防火墙 / 出站网关]
    F --> G[外部服务]

    B --> H[集群 DNS]
    H --> B

    C --> I[HTTP 代理]
    I --> G

    D --> J[CNI 流量观测]
    C --> K[代理访问日志]
    H --> L[DNS 查询日志]
    F --> M[VPC Flow Logs]
    G --> N[外部服务审计]

治理时必须分别回答六个问题:

  1. 可以发起流量:哪个 Namespace、Pod、ServiceAccount 或工作负载?
  2. 发往哪里:Pod、Service、IP、CIDR、域名还是代理?
  3. 使用什么协议:TCP、UDP、HTTP、HTTPS、DNS 还是其他协议?
  4. 源地址是什么:Pod IP、Node IP、云 NAT IP 还是固定 Egress IP?
  5. 在哪里阻断或放行:NetworkPolicy、代理、防火墙、云 NAT 还是目标系统?
  6. 如何证明发生过什么:NetworkPolicy 命中、代理日志、DNS 日志、VPC 流日志还是应用审计?

如果只配置其中一层,例如只写了 NetworkPolicy,却没有理解 DNS 和 NAT,通常会出现“策略看似正确但应用仍然无法访问”或“外部系统无法识别真实工作负载”的问题。


一、先区分 Kubernetes 原生能力与实现能力

Kubernetes API 标准化了 NetworkPolicy 等声明式对象,但并没有为所有集群提供一个统一的“出站网关”或“固定出站 IP”实现。

可以先做如下区分:

能力 Kubernetes API 是否直接保证 常见实现
限制 Pod 的 TCP/UDP 出站 NetworkPolicy API CNI 插件
Pod 访问集群 DNS Pod 与 DNS Service 的网络路径 CoreDNS、kube-dns、CNI
Pod 出站源地址转换 通常不是 Kubernetes API 行为 CNI、Linux netfilter、云网络
HTTP/HTTPS 显式代理 Kubernetes 不自动提供 Squid、Envoy、企业代理
固定 Egress IP Kubernetes 核心 API 不直接保证 CNI Egress Gateway、云 NAT、云厂商能力
按域名限制访问 标准 NetworkPolicy 不支持域名选择器 支持 FQDN 的 CNI、代理
网络流量审计 Kubernetes Audit 不记录数据包 CNI、代理、DNS、云网络日志

因此,“使用 Kubernetes 做出站治理”通常意味着把多个控制面组合起来,而不是寻找一个单独的 YAML 字段。


二、Pod 出站流量的真实路径

2.1 Pod IP 不等于外部可见 IP

假设一个 Pod 的 IP 是 10.244.2.17,它访问公网地址 198.51.100.20:443

应用
  └─ 源地址 10.244.2.17:随机源端口
       └─ Pod veth
            └─ Node
                 └─ NAT 后源地址 203.0.113.25:随机端口
                      └─ Internet

外部服务通常看到的是:

203.0.113.25:随机端口 -> 198.51.100.20:443

而不是:

10.244.2.17:随机端口 -> 198.51.100.20:443

其中:

  • 10.244.2.17 是 Pod 网络中的源地址;
  • 203.0.113.25 可能是 Node 公网地址、云 NAT 网关地址或专用 Egress IP;
  • 198.51.100.20 是外部目标;
  • 443 是目标端口;
  • 随机源端口 通常由连接跟踪和 NAT 规则分配。

2.2 NAT 的作用

NAT(Network Address Translation,网络地址转换)修改 IP 包中的地址,最常见的是 SNAT(Source NAT,源地址转换)。

设原始连接为:

Cbefore=(spod,ps,d,pd,proto)C_{\text{before}} = (s_{\text{pod}}, p_s, d, p_d, proto)

其中:

  • spods_{\text{pod}}:Pod 源 IP;
  • psp_s:源端口;
  • dd:目标 IP;
  • pdp_d:目标端口;
  • protoproto:协议,例如 TCP。

经过 SNAT 后:

Cafter=(snat,ps,d,pd,proto)C_{\text{after}} = (s_{\text{nat}}, p'_s, d, p_d, proto)

NAT 设备需要保存一个连接映射:

(spod,ps,d,pd,proto)(snat,ps,d,pd,proto)(s_{\text{pod}}, p_s, d, p_d, proto) \leftrightarrow (s_{\text{nat}}, p'_s, d, p_d, proto)

返回流量到达 s_nat:p'_s 后,NAT 根据连接跟踪表还原为 s_pod:p_s,再转发给 Pod。

这说明 NAT 解决的是地址可达性和返回路径问题,不等价于访问控制:

  • NAT 可以让私有 Pod 访问公网;
  • NAT 不决定 Pod 是否应该访问某个目标;
  • 目标系统看到的是 NAT 地址,而不是原始 Pod 身份;
  • 多个 Pod 可能共享同一个 NAT 地址。

2.3 Kubernetes 对 NAT 没有统一语义

Kubernetes 网络模型要求 Pod 能够与其他 Pod 通信,但具体的路由和 NAT 由网络实现负责。常见情况包括:

  1. Pod CIDR 在节点和云网络中可路由,不需要对集群内地址做 NAT;
  2. Pod 访问集群外部网络时,在 Node 上执行 SNAT;
  3. 云 CNI 直接给 Pod 分配 VPC 地址;
  4. 云 NAT 网关把多个节点或 Pod 的源地址转换成固定公网地址;
  5. Egress Gateway 把某些 Pod 的流量集中到专用节点或网关再出站。

因此,不能仅凭 Pod IP 推断外部白名单应该配置什么地址。正确做法是从外部服务或网络设备的日志中确认实际源地址。

可以在 Pod 内查看本地地址:

kubectl exec -n payments deploy/payment-api -- ip addr
kubectl exec -n payments deploy/payment-api -- ip route

但这只能说明 Pod 内部视角。要确认公网出口地址,需要访问由组织控制的回显服务,或查看云 NAT/VPC 流日志:

kubectl exec -n payments deploy/payment-api -- \
  curl -sS https://ifconfig.example.internal

这里的 ifconfig.example.internal 应替换为组织内部可信的地址回显服务。不要把任意公网“查 IP”服务作为生产审计的唯一依据。


三、NetworkPolicy:用身份和网络属性限制出站

3.1 NetworkPolicy 的语义

NetworkPolicy 是 Kubernetes 的声明式网络策略对象。它描述哪些 Pod 的入站或出站流量允许通过。

一个策略可以通过以下条件匹配目标:

  • podSelector:目标 Pod;
  • namespaceSelector:目标 Namespace;
  • ipBlock:目标 CIDR;
  • ports:目标端口和协议。

NetworkPolicy 本身不负责实现数据包过滤。CNI 插件必须支持并执行它。如果 CNI 不支持 NetworkPolicy,创建对象可能成功,但流量并不会按预期被限制。

NetworkPolicy 的重要语义是:

对某个方向而言,只要 Pod 被至少一个该方向的策略选中,它就在该方向进入隔离状态;允许规则取所有匹配策略的并集。

设 Pod pp 被出站策略集合 PeP_e 选中:

Pe(p)P_e(p) \neq \varnothing

则它进入出站隔离。对于一次出站请求 rr,允许条件是:

allow(r)=policyPe(p)match(policy,r)allow(r) = \bigvee_{policy \in P_e(p)} match(policy, r)

也就是“任意一个匹配策略允许”即可,而不是多个策略逐层相交。

这带来一个常见反例:

  • 策略 A 只允许访问 DNS;
  • 策略 B 只允许访问公网 API;
  • 最终效果是两者的并集:DNS 和公网 API 都允许。

添加一个更严格的策略,并不会自动覆盖或否定已有的宽松策略。要实现默认拒绝,必须确保不存在其他策略继续允许额外流量。

3.2 默认拒绝出站

下面的策略选择 payments Namespace 中的所有 Pod,并拒绝所有出站流量:

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

podSelector: {} 表示选择当前 Namespace 中所有 Pod。policyTypes: [Egress] 表示该策略只影响出站方向。

应用:

kubectl apply -f default-deny-egress.yaml

检查:

kubectl get networkpolicy -n payments
kubectl describe networkpolicy default-deny-egress -n payments

此时,具体表现取决于 CNI,但通常会出现:

kubectl exec -n payments deploy/payment-api -- \
  curl -m 3 -v https://api.example.com

连接超时或被拒绝。不能简单把“超时”解释为 NetworkPolicy,因为路由、DNS、云防火墙也可能造成相同现象;需要继续验证 DNS 和连接路径。

3.3 允许集群 DNS

默认拒绝后,应用通常连域名都解析不了。必须允许 Pod 访问集群 DNS。

一种常见写法是按 kube-system Namespace 中带有 k8s-app=kube-dns 标签的 Pod 放行 UDP/TCP 53:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-cluster-dns
  namespace: payments
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

这里的两个选择器在同一个 to 项中,表示:

目标 Namespace 是 kube-system
并且目标 Pod 具有 k8s-app=kube-dns

而不是“匹配任意一个条件”。

同时放行 UDP 和 TCP 是有原因的:

  • 普通 DNS 查询通常使用 UDP;
  • 响应过大时可能使用 TCP;
  • DNS over TCP、重试或特定解析器行为也可能使用 TCP。

但标签并非所有集群都完全一致。必须先检查实际标签:

kubectl -n kube-system get pods --show-labels
kubectl -n kube-system get svc kube-dns -o wide
kubectl -n kube-system get endpointslice \
  -l kubernetes.io/service-name=kube-dns

如果集群使用 NodeLocal DNSCache,Pod 可能访问节点上的本地 DNS 地址,而不是直接访问 kube-dns Service 后端。此时,上述按 DNS Pod 放行的策略可能不够,需要根据实际 DNS 配置允许对应的节点本地地址和端口。NodeLocal DNSCache 的部署方式属于集群实现差异,不能假设所有集群都走同一条路径。

验证 DNS:

kubectl exec -n payments deploy/payment-api -- \
  cat /etc/resolv.conf

kubectl exec -n payments deploy/payment-api -- \
  nslookup api.example.com

预期至少应看到:

  • nameserver 指向集群配置的 DNS 地址;
  • nslookup 能获得响应;
  • 解析失败时要区分 SERVFAILNXDOMAIN、超时和连接拒绝。

3.4 允许固定 CIDR 和端口

假设外部支付服务公布的目标地址是 198.51.100.20/32,应用只需要 HTTPS:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-payment-provider
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: payment-api
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 198.51.100.20/32
      ports:
        - protocol: TCP
          port: 443

将默认拒绝、DNS 放行和该策略一起应用:

kubectl apply -f default-deny-egress.yaml
kubectl apply -f allow-cluster-dns.yaml
kubectl apply -f allow-payment-provider.yaml

此时理论上的允许条件为:

destinationDNS_endpointsport=53destination \in DNS\_endpoints \land port=53

或:

destination=198.51.100.20protocol=TCPport=443destination = 198.51.100.20 \land protocol=TCP \land port=443

例如:

请求 结果
DNS 查询 kube-dns:53/UDP 允许
DNS 查询 kube-dns:53/TCP 允许
198.51.100.20:443/TCP 允许
198.51.100.20:80/TCP 拒绝
198.51.100.21:443/TCP 拒绝
任意公网 IP 的 443/TCP 拒绝

但这里有一个关键边界:应用访问的是域名,NetworkPolicy 实际看到的通常是解析后的 IP 和端口。若 api.example.com 解析出多个地址,只有被 ipBlock 覆盖的地址可以访问。

3.5 NetworkPolicy 不等于域名白名单

标准 NetworkPolicy API 没有 domainNamefqdn 字段。因此下面这种写法不是合法的标准 NetworkPolicy:

# 不存在这样的标准字段
egress:
  - to:
      - domainName: api.example.com

域名白名单与 IP 白名单存在本质差异:

  1. DNS 解析结果可能变化;
  2. 一个域名可能返回多个 A 或 AAAA 记录;
  3. CDN 可能返回不同区域的地址;
  4. 应用可能直接连接 IP,绕过域名;
  5. DNS 响应可能包含短 TTL;
  6. IPv4 与 IPv6 可能走不同出口;
  7. 域名可能通过 CNAME 链指向多个服务。

如果使用支持 FQDN 策略的 CNI,通常由该 CNI 观察 DNS 响应并动态维护 IP 集合,但这属于 CNI 扩展,不是 Kubernetes NetworkPolicy 标准保证。生产中必须确认:

  • 具体 CNI 版本;
  • FQDN 策略的语法;
  • 是否只对特定 DNS 路径生效;
  • TTL 过期和缓存更新方式;
  • 是否支持 CNAME;
  • IPv6 是否覆盖;
  • Pod 直接访问 IP 时是否仍然阻断;
  • DNS 被代理或加密后是否还能识别域名。

如果目标是严格的 HTTP 域名白名单,显式代理通常比在三层网络策略中模拟域名更清晰。


四、代理:把出站访问从网络连接变成可审计的应用请求

4.1 显式 HTTP 代理的工作方式

显式代理要求应用主动把请求发给代理,而不是直接连接最终目标。

对于普通 HTTP:

应用 -> 代理:8080
代理 -> api.example.com:80

应用请求行通常包含完整 URL:

GET http://api.example.com/data HTTP/1.1
Host: api.example.com

对于 HTTPS,应用通常向代理发送 CONNECT

CONNECT api.example.com:443 HTTP/1.1
Host: api.example.com:443

代理建立到目标的 TCP 连接后,应用与目标之间的 TLS 流量通过该隧道传输。

代理可以因此获得:

  • 目标域名;
  • 目标端口;
  • 请求时间;
  • 客户端身份或认证信息;
  • 响应状态;
  • 字节数;
  • 允许或拒绝结果。

但普通 HTTPS CONNECT 模式下,代理通常不能看到 HTTP 路径和请求体,除非部署 TLS 解密,这会引入证书签发、私钥保护、合规和应用兼容性问题。

4.2 通过环境变量配置代理

一个常见的 Pod 配置如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-api
  namespace: payments
spec:
  replicas: 2
  selector:
    matchLabels:
      app: payment-api
  template:
    metadata:
      labels:
        app: payment-api
    spec:
      containers:
        - name: app
          image: registry.example.com/payment-api:1.4.0
          env:
            - name: HTTPS_PROXY
              value: http://egress-proxy.networking.svc.cluster.local:8080
            - name: HTTP_PROXY
              value: http://egress-proxy.networking.svc.cluster.local:8080
            - name: NO_PROXY
              value: .svc,.cluster.local,10.0.0.0/8,127.0.0.1,localhost

每个变量都有实际边界:

  • HTTP_PROXY:通常用于 HTTP 请求;
  • HTTPS_PROXY:名称容易误解,它通常表示“访问 HTTPS 目标时使用的代理地址”,代理本身可以是普通 HTTP 代理;
  • NO_PROXY:列出不应经过代理的目标;
  • .svc.cluster.local:避免访问集群内部 Service 时绕到外部代理;
  • CIDR 支持程度取决于应用使用的 HTTP 客户端;
  • 大小写变量的优先级因语言库而异。

NO_PROXY 配错会导致两类故障:

  1. 集群内 Service 被发送给不能解析集群域名的外部代理;
  2. 本来必须经过代理的外部域名被绕过,直接出站。

因此不能假设所有应用都遵循这些环境变量。必须查看实际运行时,例如 Java、Go、Python、Node.js 或命令行工具的代理支持方式可能不同。

4.3 代理不是万能的出站控制

显式 HTTP 代理主要适合 HTTP/HTTPS。以下流量可能不会自动经过它:

  • 任意 TCP 协议;
  • UDP;
  • 数据库原生协议;
  • DNS;
  • 应用自己实现的网络客户端;
  • 忽略环境变量的程序;
  • 使用固定 IP 或自定义协议的程序。

所以常见的分层设计是:

NetworkPolicy:
    只允许访问代理 Service,以及 DNS

代理:
    只允许访问指定域名和端口

云防火墙 / NAT:
    只允许固定网络出口访问必要 CIDR

目标服务:
    只接受固定 Egress IP,并执行应用级认证

如果只允许 Pod 访问代理,必须保证 Pod 无法同时直接访问公网。否则应用只要忽略 HTTPS_PROXY,就能绕过代理。


五、Egress Gateway:固定出口与身份集中控制

5.1 Egress Gateway 解决什么问题

Egress Gateway 是一种架构模式:将一组工作负载的出站流量先导向专用网关,再由网关访问外部系统。

业务 Pod
  └─> Egress Gateway
        └─> NAT / 防火墙
              └─> 外部服务

它通常解决两个问题:

  1. 固定源地址:外部系统只需白名单网关的地址;
  2. 集中治理:在一个位置执行路由、TLS、策略、限速和审计。

但是 Egress Gateway 不是 Kubernetes 核心 API 中统一定义的对象。不同实现可能来自:

  • Service Mesh;
  • CNI 插件;
  • 云厂商网络产品;
  • 专用代理或防火墙。

不能因为某个产品存在 EgressGateway CRD,就认为所有 Kubernetes 集群都有同样的 API 和语义。

5.2 典型数据流

以 Service Mesh 的出站网关为例,可能存在如下路径:

应用容器
  -> 本地 Sidecar
  -> 节点或网格重定向规则
  -> Egress Gateway Sidecar
  -> 外部地址

需要确认的状态包括:

  1. 应用是否真的把流量交给 Sidecar;
  2. Sidecar 是否识别目标协议;
  3. 出站规则是否把目标路由到 Egress Gateway;
  4. Gateway 是否允许该目标;
  5. Gateway 是否拥有正确的 DNS、路由和证书;
  6. Gateway 出口是否经过预期 NAT;
  7. 返回包是否能沿连接跟踪状态返回。

任意一层失败都可能表现为应用超时。

5.3 不要混淆 NetworkPolicy 与 Service Mesh

两者控制层次不同:

能力 NetworkPolicy Service Mesh
主要层次 L3/L4 L4/L7,取决于实现
识别内容 IP、端口、协议、Pod/Namespace 服务身份、HTTP 路由、TLS、请求属性
是否能稳定识别域名 标准 API 不能 代理通常能识别 CONNECT/SNI/HTTP Host
是否记录每条 HTTP 请求 不提供统一标准日志 通常可提供访问日志和遥测
是否自动代理所有协议 取决于 Sidecar 和协议识别
是否提供固定 Egress IP 不直接提供 通过 Egress Gateway 等组件实现

Service Mesh 的 mTLS 主要解决服务间身份认证和传输加密,不自动等价于“允许访问公网”。访问外部服务时,外部系统未必理解网格内部身份,仍然需要出口 IP、代理认证或应用级凭据。


六、DNS:解析机制、策略边界与故障定位

6.1 一次域名访问至少包含两个动作

应用访问 https://api.example.com,通常先执行:

api.example.com{IP1,IP2,,IPn}api.example.com \rightarrow \{IP_1, IP_2, \ldots, IP_n\}

然后选择一个 IP 建立连接:

PodIPi:443Pod \rightarrow IP_i:443

因此,DNS 可达不代表业务可达,业务 IP 可达也不代表 DNS 正常。

一个完整的失败路径可能是:

应用
  -> DNS 查询超时
  -> 没有 IP
  -> 根本不会产生 TCP 443 连接

另一个失败路径是:

应用
  -> DNS 成功得到 198.51.100.20
  -> TCP 443 被 NetworkPolicy 拒绝
  -> 应用报告 connection timeout

第三种失败路径是:

应用
  -> DNS 成功
  -> TCP 443 成功
  -> TLS SNI 或证书不匹配
  -> 应用报告 TLS handshake error

必须分别测试:

kubectl exec -n payments deploy/payment-api -- \
  nslookup api.example.com

kubectl exec -n payments deploy/payment-api -- \
  sh -c 'nc -vz -w 3 198.51.100.20 443'

kubectl exec -n payments deploy/payment-api -- \
  curl -v --connect-timeout 3 https://api.example.com/health

这三个命令分别验证:

  1. DNS;
  2. TCP 建连;
  3. TCP、TLS、HTTP 的完整链路。

6.2 DNS 不是安全授权

DNS 只回答“这个名称当前解析到哪些地址”,不能证明:

  • 返回结果一定属于可信服务;
  • 访问者具有业务权限;
  • 目标 IP 不会变化;
  • 目标服务不会通过重定向引导到其他域名;
  • 应用没有直接访问其他 IP。

DNS 也可能受到缓存、污染、配置错误或域名劫持影响。因此,域名白名单不能替代:

  • TLS 证书校验;
  • HTTP Host/SNI 校验;
  • 目标服务认证;
  • IP 或网络层白名单;
  • 代理侧的策略。

七、白名单设计:从“域名列表”转化为可验证条件

7.1 白名单至少包含五个维度

一个可执行的出站白名单应尽量写成:

W=(workload, protocol, destination, port, path)W = (workload,\ protocol,\ destination,\ port,\ path)

其中:

  • workload:哪个工作负载;
  • protocol:TCP、UDP、HTTP 等;
  • destination:Pod、Service、IP、CIDR 或域名;
  • port:目标端口;
  • path:是否必须通过代理、Egress Gateway 或特定 NAT。

例如:

payment-api
  只能通过 egress-proxy:8080
  由代理访问 api.payment.example:443
  不能直接访问公网
  DNS 只能访问集群 DNS

这比“允许访问支付域名”更精确,因为它同时限定了路径和绕过方式。

7.2 目标地址变化时的更新问题

假设域名当前解析结果为:

api.payment.example -> 198.51.100.20

策略允许 198.51.100.20/32。几小时后 DNS 改为:

api.payment.example -> 198.51.100.30

若策略未更新,业务会失败。反过来,如果旧 IP 被其他租户重新使用,而策略仍然保留,可能产生错误放行。

因此,固定 IP 白名单适合:

  • 对方提供稳定 IP;
  • 对方提供官方 CIDR;
  • 有自动化同步机制;
  • 有变更通知和回滚方式。

对于 CDN、动态 SaaS 和多区域服务,代理或支持 FQDN 的网络实现通常更合适,但也要审计其 DNS 观察机制。

7.3 IPv4 与 IPv6

如果 Pod 具有 IPv6 出站能力,只限制 IPv4 CIDR 可能并不能达到预期:

应用解析出 AAAA
  -> 通过 IPv6 连接
  -> 绕过只针对 IPv4 设计的出口规则

生产策略需要明确:

  • 是否启用双栈;
  • 是否允许 IPv6;
  • IPv6 是否经过同一代理;
  • IPv6 是否有对应防火墙和审计;
  • DNS 是否返回 AAAA 记录。

不能用“当前测试环境没有 IPv6”推断生产一定没有 IPv6。

7.4 不能把 ServiceAccount 直接写进标准 NetworkPolicy

标准 NetworkPolicy 选择器主要针对 Pod、Namespace 和 IP 块,不提供按 ServiceAccount 直接选择的字段。

如果需求是:

只允许 payment-api 这个 ServiceAccount 出站

常见做法是:

  1. 用 Admission 控制器或工作负载模板把 ServiceAccount 映射成 Pod 标签;
  2. 用 NetworkPolicy 按该标签选择 Pod;
  3. 在更高层用 Service Mesh 或代理身份做 L7 授权;
  4. 用 RBAC 控制谁可以修改 Deployment、ServiceAccount 和 NetworkPolicy。

网络策略与 Kubernetes 授权不是同一层:

  • NetworkPolicy 控制数据包;
  • RBAC 控制谁可以操作 Kubernetes API;
  • ServiceAccount 是工作负载调用 API 或被其他系统识别的身份;
  • 它不会自动变成 NetworkPolicy 的网络源身份。

八、一个可验证的最小实验

下面的实验展示“默认拒绝 + DNS 放行 + 固定目标放行”。它假设:

  • Namespace payments 已存在;
  • CNI 支持 NetworkPolicy;
  • payment-api Deployment 已存在;
  • kube-system 中 DNS Pod 使用 k8s-app=kube-dns
  • 198.51.100.20 仅作为文档示例地址,不能在真实网络中访问。

创建 Namespace:

kubectl create namespace payments

创建测试 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: net-test
  namespace: payments
  labels:
    app: net-test
spec:
  containers:
    - name: curl
      image: curlimages/curl:8.10.1
      command: ["sleep", "3600"]
kubectl apply -f net-test.yaml
kubectl wait -n payments --for=condition=Ready pod/net-test --timeout=60s

应用三类策略:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: payments
spec:
  podSelector: {}
  policyTypes:
    - Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: payments
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
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: net-test
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 198.51.100.20/32
      ports:
        - protocol: TCP
          port: 443

应用并测试:

kubectl apply -f policies.yaml

kubectl exec -n payments net-test -- \
  nslookup kubernetes.default.svc.cluster.local

如果 DNS 正常,应该得到集群内 Service 的解析结果。

测试一个不在白名单中的地址:

kubectl exec -n payments net-test -- \
  curl -m 3 -v https://example.com

预期是超时或连接被拒绝,但具体错误取决于 CNI 的实现。

测试允许的地址:

kubectl exec -n payments net-test -- \
  curl -m 3 -v https://198.51.100.20

由于 198.51.100.20 属于文档保留网段,这条命令不会在真实公网中成功。这里验证的是策略匹配逻辑,而不是外部服务可用性。生产测试必须换成组织控制、且确实监听 TCP 443 的测试端点。

若应用通过域名访问:

kubectl exec -n payments net-test -- \
  curl -m 3 -v https://api.payment.example

即使该域名当前解析到 198.51.100.20,也只有在解析结果确实落在允许 CIDR 中时才可能成功。解析到其他地址时,策略会阻止连接。


九、故障诊断:按层验证而不是反复修改 YAML

9.1 先确认策略是否实际生效

kubectl get networkpolicy -A
kubectl describe networkpolicy -n payments
kubectl get pods -n payments -o wide

检查:

  • Pod 是否被策略的 podSelector 选中;
  • 策略是否在正确的 Namespace;
  • CNI 是否声明支持 NetworkPolicy;
  • 是否存在其他策略提供了额外放行;
  • Pod 是否使用 hostNetwork: true

hostNetwork: true 的 Pod 使用节点网络命名空间,其流量路径和普通 Pod 不同,不能直接套用普通 Pod 的判断。

9.2 再区分 DNS、TCP、TLS 和 HTTP

推荐按以下顺序:

# 1. 查看解析器配置
kubectl exec -n payments net-test -- cat /etc/resolv.conf

# 2. 验证 DNS
kubectl exec -n payments net-test -- nslookup api.payment.example

# 3. 验证目标 TCP 端口
kubectl exec -n payments net-test -- \
  sh -c 'nc -vz -w 3 198.51.100.20 443'

# 4. 验证 TLS 与 HTTP
kubectl exec -n payments net-test -- \
  curl -v --connect-timeout 3 https://api.payment.example/health

典型错误的含义不同:

表现 可能层次
Could not resolve host DNS、DNS Policy、DNS Service、搜索域配置
Connection timed out NetworkPolicy、路由、云防火墙、目标丢包
Connection refused 已到达目标,但目标端口无监听或主动拒绝
TLS handshake error 证书、SNI、代理隧道、协议版本
HTTP 403 已联网,通常是应用认证或目标授权
HTTP 407 代理要求认证
访问内部 Service 却出现代理错误 NO_PROXY 配置错误

9.3 检查代理是否被绕过

进入容器检查环境变量:

kubectl exec -n payments deploy/payment-api -- \
  printenv | grep -i proxy

还要检查应用自身的配置,因为一些客户端库:

  • 不读取环境变量;
  • 只读取大写或只读取小写;
  • NO_PROXY 的 CIDR 支持不一致;
  • 对域名后缀匹配方式不同;
  • 对 HTTPS 代理使用方式不同。

代理侧应同时查看:

  • 是否收到该 Pod 的请求;
  • 请求目标是什么;
  • 是否命中拒绝规则;
  • 是否能解析目标;
  • 是否能建立上游连接;
  • 上游返回了什么状态。

如果代理没有任何请求日志,问题很可能发生在应用到代理之间,或者应用根本没有使用代理。

9.4 检查 NAT 和出口地址

如果外部服务只允许固定 IP,应检查:

  1. Pod 到 Node 的路由;
  2. Node 是否执行 SNAT;
  3. 云 NAT 是否存在并绑定正确子网;
  4. 多可用区是否存在不同出口;
  5. Egress Gateway 是否有独立地址;
  6. 返回路径是否被网络 ACL 或防火墙阻断。

云环境中,节点可能没有公网 IP,但私有子网通过 NAT Gateway 出公网。此时外部服务应该白名单 NAT Gateway 的公网地址,而不是 Node 私有地址或 Pod CIDR。

9.5 需要可观测性时,不要指望 NetworkPolicy 自动产生日志

标准 NetworkPolicy API 没有统一的“每条规则命中日志”字段。要知道“为什么被拒绝”,通常需要:

  • CNI 的 flow visibility 或 drop event;
  • 节点上的 eBPF、iptables 或 conntrack 观测;
  • DNS 查询日志;
  • 代理访问日志;
  • Service Mesh telemetry;
  • 云 VPC Flow Logs;
  • 外部服务访问日志。

抓包时要注意位置。Pod 内部、节点网桥、NAT 前后看到的源地址可能不同:

Pod 内抓包:看到 Pod IP
Node NAT 前:可能仍看到 Pod IP
Node NAT 后:看到 Node 或 NAT 地址
外部服务:看到最终出口地址

如果只在错误的位置抓包,很容易得出“没有流量”的错误结论。


十、审计:Kubernetes Audit 与网络流量审计不是一回事

10.1 Kubernetes Audit 记录 API 请求

Kubernetes Audit 用于记录对 Kubernetes API Server 的请求,例如:

  • 谁创建、修改或删除了 NetworkPolicy;
  • 谁修改了 Deployment 的代理环境变量;
  • 谁改变了 ServiceAccount;
  • 谁读取了 Secret;
  • 谁修改了 Egress Gateway 相关对象。

它记录的是控制面操作,不是 Pod 发出的 TCP 数据包。

审计事件通常包含:

  • 用户或 ServiceAccount 身份;
  • 请求动词;
  • 资源、Namespace、名称;
  • 请求时间;
  • 响应状态;
  • 请求阶段;
  • 必要时的请求对象内容。

因此,一个完整的出站审计链通常需要至少三种证据:

Kubernetes Audit:
    谁改变了策略

网络/代理/DNS日志:
    哪个工作负载访问了什么

外部服务日志:
    哪个出口地址成功或失败地调用了目标

10.2 审计策略示例

下面是一个审计策略片段,用于记录 NetworkPolicy 的元数据操作:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: Metadata
    resources:
      - group: networking.k8s.io
        resources:
          - networkpolicies
  - level: Metadata
    resources:
      - group: apps
        resources:
          - deployments
      - resources:
          - serviceaccounts

它表达的是“记录这些资源的 API 操作元数据”。具体如何加载审计策略、将日志写入文件还是 Webhook,由 API Server 的启动参数和集群运维方式决定,不是通过 kubectl apply 创建一个普通 Kubernetes 资源来完成。

审计级别也有取舍:

  • Metadata:记录请求身份和资源信息,不记录完整对象;
  • Request:额外记录请求体;
  • RequestResponse:还记录响应体,数据量和敏感信息风险更高。

如果记录包含 Secret 或敏感配置的完整对象,审计日志本身就成为高价值敏感数据,必须限制访问并设置保留周期。

10.3 网络审计字段

出站流量审计最好至少包含:

时间
Namespace
Pod 名称或工作负载
Pod IP
节点
协议
目标 IP
目标端口
最终出口 IP
是否经过代理或 Egress Gateway
允许/拒绝
字节数
连接持续时间

L7 代理还可以增加:

HTTP 方法
Host
路径
响应状态
TLS SNI
代理用户或工作负载身份

但不能把日志字段直接当作真实身份。Pod 名称会因重建变化,Pod IP 会复用,NAT 后地址会被多个工作负载共享。更可靠的关联方式是同时记录:

  • Pod UID;
  • 工作负载控制器信息;
  • Namespace;
  • ServiceAccount;
  • 节点;
  • 连接五元组;
  • 时间窗口;
  • NAT 映射或 Egress Gateway 记录。

十一、与 RBAC 的关系:谁能改变出站边界

出站治理不仅是运行时流量问题,也是配置变更权限问题。

例如,攻击者如果能够:

  1. 修改 Deployment,清除 HTTPS_PROXY
  2. 创建一个宽松的 NetworkPolicy;
  3. 修改工作负载标签,使其脱离原有选择器;
  4. 修改 ServiceAccount 或挂载的凭据;
  5. 部署一个 hostNetwork 或高权限 Pod;

那么原有出站限制可能失效。

RBAC 应至少保护以下对象的写权限:

  • networkpolicies.networking.k8s.io
  • deployments.apps
  • daemonsets.apps
  • pods
  • serviceaccounts
  • Service Mesh 或 CNI 的自定义资源;
  • 代理配置和 Secret。

一个最小化的查看权限示例:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: network-observer
  namespace: payments
rules:
  - apiGroups: ["networking.k8s.io"]
    resources: ["networkpolicies"]
    verbs: ["get", "list", "watch"]

这只允许读取 payments Namespace 的 NetworkPolicy。它不授予修改权限,也不代表调用者可以读取任意 Namespace 的策略。

需要特别注意:RBAC 保护的是 API 操作,不会阻止一个已经运行的进程直接发起网络连接。网络访问仍需由 NetworkPolicy、代理、防火墙等运行时控制。


十二、常见错误设计与反例

12.1 只允许 TCP 443,却忘记 DNS

错误假设:

只要允许外部服务的 443,域名访问就会成功。

实际流程是先 DNS,再 TCP:

DNS 失败
  -> 没有目标 IP
  -> 443 规则根本不会被使用

修复方式是显式允许 DNS,并确认 DNS 的真实目标地址。

12.2 用 0.0.0.0/0 伪装成域名白名单

ipBlock:
  cidr: 0.0.0.0/0
ports:
  - protocol: TCP
    port: 443

这表示允许所有 IPv4 目标的 TCP 443,并不表示“允许所有 HTTPS 域名”。应用可以直接访问任意 IP、绕过域名和代理,也可能访问云元数据地址或不应访问的内部网络。

12.3 允许代理,但代理 Service 自己无法出站

网络策略可能正确限制了业务 Pod:

业务 Pod -> 代理 Service

但代理 Pod 也可能被另一个默认拒绝策略限制:

代理 Pod -X-> 外部目标

此时业务侧看到的只是代理连接失败或 502 Bad Gateway。代理是独立的工作负载,必须为它设计独立的出站策略、DNS 权限和审计。

12.4 只白名单 Node IP,却实际经过云 NAT

外部服务看到的不是 Node 私有地址,而是 NAT Gateway 公网地址。把 Node IP 加入外部白名单不会产生效果。

正确做法是通过实际流日志或目标日志确认:

Pod IP -> Node IP -> NAT Gateway IP -> 外部服务

然后将真正的出口地址纳入变更管理。

12.5 以为 NetworkPolicy 能阻止节点级绕过

NetworkPolicy 通常针对 Pod 网络路径。以下对象可能绕过普通 Pod 策略边界,或需要额外防护:

  • hostNetwork: true
  • 特权容器;
  • 能访问节点网络命名空间的高权限工作负载;
  • 节点本身发起的流量;
  • CNI 或宿主机配置错误;
  • 外部负载均衡器和云网络路径。

这不是说 NetworkPolicy 没有价值,而是它不能替代节点安全、Pod Security、RBAC、云防火墙和宿主机加固。


十三、生产取舍:按目标选择控制层

场景一:只需要限制网络可达性

使用:

NetworkPolicy + 默认拒绝 + DNS 放行 + IP/端口白名单

适合固定 IP、内部服务和简单 TCP 依赖。优点是路径短、性能开销较低;缺点是无法稳定表达动态域名和 HTTP 级规则。

场景二:需要按域名审计和授权

使用:

NetworkPolicy 只允许代理
+ HTTP/HTTPS 显式代理
+ 代理域名白名单和访问日志

适合 SaaS API、动态 CDN 和需要集中审计的 HTTP 流量。必须防止应用绕过代理,并处理 NO_PROXY、代理认证、代理高可用和证书配置。

场景三:外部系统要求固定来源地址

使用:

Egress Gateway 或云 NAT
+ 外部系统白名单
+ 网络流量审计

适合支付、数据库和合作方 API。需要处理网关容量、故障切换、跨可用区路径、连接跟踪表和地址变更。

场景四:需要服务身份、mTLS 和 HTTP 流量治理

使用:

Service Mesh Sidecar
+ Egress Gateway
+ NetworkPolicy
+ 外部出口防火墙或代理

这种组合能力强,但组件更多,故障路径更长。必须明确 Sidecar、网格控制面、Egress Gateway、DNS、NAT 和外部防火墙分别负责什么,不能只依赖网格配置而忽略底层网络策略。


十四、上线前的验证闭环

一个出站策略不能只验证“允许的请求成功”,还要验证“不允许的请求确实失败”,并确认失败原因可追踪。

建议至少验证以下矩阵:

测试项 预期
允许目标的 DNS 查询 成功
不允许目标的 DNS 查询 根据 DNS 设计决定,通常 DNS 本身可解析,但后续连接必须失败
允许目标 TCP 端口 成功
允许目标的非授权端口 失败
不允许 IP 的同一端口 失败
直接访问公网 IP 失败或被代理策略拒绝
经过代理访问允许域名 成功
经过代理访问禁止域名 被代理拒绝
应用重建后 策略仍按标签和身份生效
NAT 或节点切换后 外部仍看到预期出口地址
DNS IP 变化后 策略按设计更新或明确失败
IPv6 目标 按策略明确允许或拒绝
NetworkPolicy 删除尝试 被 RBAC 拒绝或产生审计事件

验证命令应覆盖多个视角:

kubectl get pod -n payments -o wide
kubectl get networkpolicy -n payments
kubectl describe networkpolicy -n payments
kubectl logs -n payments deploy/payment-api
kubectl get events -n payments --sort-by=.lastTimestamp

然后结合:

  • CNI 流量观测;
  • CoreDNS 或 NodeLocal DNSCache 日志;
  • 代理访问日志;
  • NAT/VPC 流日志;
  • 外部服务日志;
  • Kubernetes Audit 日志。

只有这些证据能够在同一时间窗口内关联起来,才能回答“哪个工作负载通过哪个出口访问了哪个目标,以及为什么被允许或拒绝”。

Kubernetes 出站治理的核心不是写出更多规则,而是建立清晰的责任边界:NetworkPolicy 负责网络层可达性,DNS 负责名称解析,代理或 Egress Gateway 负责集中出口,NAT 负责地址转换,外部防火墙负责网络边界,RBAC 负责保护这些配置,审计系统负责证明配置和流量发生过什么。只有把这些层次连接起来,出站白名单才会从一份静态 YAML 变成可验证、可审计、可恢复的安全控制。


系列导航与关联阅读

官方资料

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