跳到主要内容

通过 SNI 代理 TLS 上的 TCP 流量

本文介绍如何通过 Ingress Controller 配置网关,使其基于服务器名称指示(SNI)路由经过 TLS 加密的 TCP 流量。

前提条件

  1. 完成设置 Ingress Controller 和网关

启动示例上游服务

创建 Kubernetes manifest 文件,定义 TCP echo 上游服务:

echo-tcp.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: echo-tcp
spec:
replicas: 1
selector:
matchLabels:
app: echo-tcp
template:
metadata:
labels:
app: echo-tcp
spec:
containers:
- name: echo-tcp
image: alpine:3.20
command:
- sh
- -c
- |
apk add --no-cache socat &&
socat TCP-LISTEN:8443,reuseaddr,fork EXEC:"cat"
ports:
- containerPort: 8443
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: echo-tcp
spec:
selector:
app: echo-tcp
ports:
- name: tcp
port: 8443
targetPort: 8443

将配置应用到集群:

kubectl apply -f echo-tcp.yaml

启用网关 Stream 代理

升级网关,启用 stream 模式并设置 TCP 端口 9100

helm upgrade -n aic apisix apisix/apisix \
--set ... \ # add other parameters
--set "service.stream.enabled=true" \
--set "service.stream.tcp[0].addr=9100" \
--set "service.stream.tcp[0].tls=true"

生成证书和密钥

生成证书颁发机构(CA)的密钥和证书:

openssl genrsa -out ca.key 2048 && \
openssl req -new -sha256 -key ca.key -out ca.csr -subj "/CN=ROOTCA" && \
openssl x509 -req -days 36500 -sha256 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.crt

生成通用名称为 test.com 的密钥和证书,并使用 CA 证书进行签名:

openssl genrsa -out server.key 2048 && \
openssl req -new -sha256 -key server.key -out server.csr -subj "/CN=test.com" && \
openssl x509 -req -days 36500 -sha256 -extensions v3_req \
-CA ca.crt -CAkey ca.key -CAserial ca.srl -CAcreateserial \
-in server.csr -out server.crt

创建 Kubernetes TLS Secret:

kubectl create secret tls test-tls-secret \
--cert=server.crt \
--key=server.key \
--namespace=aic

配置网关 TLS

更新 Gateway manifest 文件,添加 TLS 监听器:

gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
namespace: aic
name: apisix
spec:
gatewayClassName: apisix
listeners:
- name: http
protocol: HTTP
port: 80
- name: tcptls
protocol: TLS
port: 9100
hostname: test.com
tls:
# TLS termination only. Passthrough is not supported.
certificateRefs:
- kind: Secret
group: ""
name: test-tls-secret
infrastructure:
parametersRef:
group: apisix.apache.org
kind: GatewayProxy
name: apisix-config

请注意,Gateway 监听器中的 port 是必填字段,但实际会被忽略。这是由于数据面的限制:它无法动态打开新端口。由于 Ingress Controller 不管理数据面部署,因此无法自动更新配置或重启数据面来应用端口变更。

将配置应用到集群:

kubectl apply -f gateway.yaml

创建路由

创建 Kubernetes manifest 文件,定义 TLSRoute:

tls-tcp-route.yaml
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
namespace: aic
name: tls-route
spec:
parentRefs:
- name: apisix
hostnames: ["test.com"]
rules:
- backendRefs:
- name: echo-tcp
port: 8443
关于 hostname

Gateway 监听器和 TLSRoute 中都会出现 hostname 配置,因为它们各自承担不同用途:

  • Gateway 监听器的 hostname:指定网关在该监听器上接受哪些 hostname。
  • TLSRoute 的 hostnames:定义路由适用的 hostname,用于过滤监听器接收到的流量。

TLSRoute 中的 hostnames 是必填字段,并且表示监听器 hostname 的子集。显式声明可以确保路由匹配更精确。

将配置应用到集群:

kubectl apply -f tls-tcp-route.yaml

验证

将网关的 TCP TLS Service 端口暴露到本地:

# 替换为你的网关 Service 名称
kubectl port-forward svc/<gateway-service-name> 9100:9100 &

运行以下命令,验证网关是否可以使用 SNI test.com 正确路由经过 TLS 加密的 TCP 流量:

openssl s_client -connect 127.0.0.1:9100 -servername test.com

TLS 握手完成后,可以输入任意文本并按 Enter。上游 echo server 会返回相同文本,说明网关已正确终止 TLS,并将明文 TCP 载荷转发到上游:

...
subject=CN=test.com
issuer=CN=ROOTCA
...
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Server public key is 2048 bit
...
---
read R BLOCK

hello world
hello world

如需验证 SNI 未匹配任何已配置路由时的行为,运行:

openssl s_client -connect 127.0.0.1:9100 -servername random.com

由于网关无法识别匹配的四层路由(stream route),该连接会被终止。