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[外部服务审计]
治理时必须分别回答六个问题:
- 谁可以发起流量:哪个 Namespace、Pod、ServiceAccount 或工作负载?
- 发往哪里:Pod、Service、IP、CIDR、域名还是代理?
- 使用什么协议:TCP、UDP、HTTP、HTTPS、DNS 还是其他协议?
- 源地址是什么:Pod IP、Node IP、云 NAT IP 还是固定 Egress IP?
- 在哪里阻断或放行:NetworkPolicy、代理、防火墙、云 NAT 还是目标系统?
- 如何证明发生过什么: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,源地址转换)。
设原始连接为:
其中:
- :Pod 源 IP;
- :源端口;
- :目标 IP;
- :目标端口;
- :协议,例如 TCP。
经过 SNAT 后:
NAT 设备需要保存一个连接映射:
返回流量到达 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 由网络实现负责。常见情况包括:
- Pod CIDR 在节点和云网络中可路由,不需要对集群内地址做 NAT;
- Pod 访问集群外部网络时,在 Node 上执行 SNAT;
- 云 CNI 直接给 Pod 分配 VPC 地址;
- 云 NAT 网关把多个节点或 Pod 的源地址转换成固定公网地址;
- 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 被出站策略集合 选中:
则它进入出站隔离。对于一次出站请求 ,允许条件是:
也就是“任意一个匹配策略允许”即可,而不是多个策略逐层相交。
这带来一个常见反例:
- 策略 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能获得响应;- 解析失败时要区分
SERVFAIL、NXDOMAIN、超时和连接拒绝。
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
此时理论上的允许条件为:
或:
例如:
| 请求 | 结果 |
|---|---|
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 没有 domainName 或 fqdn 字段。因此下面这种写法不是合法的标准 NetworkPolicy:
# 不存在这样的标准字段
egress:
- to:
- domainName: api.example.com
域名白名单与 IP 白名单存在本质差异:
- DNS 解析结果可能变化;
- 一个域名可能返回多个 A 或 AAAA 记录;
- CDN 可能返回不同区域的地址;
- 应用可能直接连接 IP,绕过域名;
- DNS 响应可能包含短 TTL;
- IPv4 与 IPv6 可能走不同出口;
- 域名可能通过 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 配错会导致两类故障:
- 集群内 Service 被发送给不能解析集群域名的外部代理;
- 本来必须经过代理的外部域名被绕过,直接出站。
因此不能假设所有应用都遵循这些环境变量。必须查看实际运行时,例如 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 / 防火墙
└─> 外部服务
它通常解决两个问题:
- 固定源地址:外部系统只需白名单网关的地址;
- 集中治理:在一个位置执行路由、TLS、策略、限速和审计。
但是 Egress Gateway 不是 Kubernetes 核心 API 中统一定义的对象。不同实现可能来自:
- Service Mesh;
- CNI 插件;
- 云厂商网络产品;
- 专用代理或防火墙。
不能因为某个产品存在 EgressGateway CRD,就认为所有 Kubernetes 集群都有同样的 API 和语义。
5.2 典型数据流
以 Service Mesh 的出站网关为例,可能存在如下路径:
应用容器
-> 本地 Sidecar
-> 节点或网格重定向规则
-> Egress Gateway Sidecar
-> 外部地址
需要确认的状态包括:
- 应用是否真的把流量交给 Sidecar;
- Sidecar 是否识别目标协议;
- 出站规则是否把目标路由到 Egress Gateway;
- Gateway 是否允许该目标;
- Gateway 是否拥有正确的 DNS、路由和证书;
- Gateway 出口是否经过预期 NAT;
- 返回包是否能沿连接跟踪状态返回。
任意一层失败都可能表现为应用超时。
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,通常先执行:
然后选择一个 IP 建立连接:
因此,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
这三个命令分别验证:
- DNS;
- TCP 建连;
- TCP、TLS、HTTP 的完整链路。
6.2 DNS 不是安全授权
DNS 只回答“这个名称当前解析到哪些地址”,不能证明:
- 返回结果一定属于可信服务;
- 访问者具有业务权限;
- 目标 IP 不会变化;
- 目标服务不会通过重定向引导到其他域名;
- 应用没有直接访问其他 IP。
DNS 也可能受到缓存、污染、配置错误或域名劫持影响。因此,域名白名单不能替代:
- TLS 证书校验;
- HTTP Host/SNI 校验;
- 目标服务认证;
- IP 或网络层白名单;
- 代理侧的策略。
七、白名单设计:从“域名列表”转化为可验证条件
7.1 白名单至少包含五个维度
一个可执行的出站白名单应尽量写成:
其中:
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 出站
常见做法是:
- 用 Admission 控制器或工作负载模板把 ServiceAccount 映射成 Pod 标签;
- 用 NetworkPolicy 按该标签选择 Pod;
- 在更高层用 Service Mesh 或代理身份做 L7 授权;
- 用 RBAC 控制谁可以修改 Deployment、ServiceAccount 和 NetworkPolicy。
网络策略与 Kubernetes 授权不是同一层:
- NetworkPolicy 控制数据包;
- RBAC 控制谁可以操作 Kubernetes API;
- ServiceAccount 是工作负载调用 API 或被其他系统识别的身份;
- 它不会自动变成 NetworkPolicy 的网络源身份。
八、一个可验证的最小实验
下面的实验展示“默认拒绝 + DNS 放行 + 固定目标放行”。它假设:
- Namespace
payments已存在; - CNI 支持 NetworkPolicy;
payment-apiDeployment 已存在;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,应检查:
- Pod 到 Node 的路由;
- Node 是否执行 SNAT;
- 云 NAT 是否存在并绑定正确子网;
- 多可用区是否存在不同出口;
- Egress Gateway 是否有独立地址;
- 返回路径是否被网络 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 的关系:谁能改变出站边界
出站治理不仅是运行时流量问题,也是配置变更权限问题。
例如,攻击者如果能够:
- 修改 Deployment,清除
HTTPS_PROXY; - 创建一个宽松的 NetworkPolicy;
- 修改工作负载标签,使其脱离原有选择器;
- 修改 ServiceAccount 或挂载的凭据;
- 部署一个
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 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes NetworkPolicy:Selector、Ingress、Egress、默认拒绝和验证
- 下一篇:Kubernetes Service Mesh:Sidecar、mTLS、流量治理、遥测和边界
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论