通过 SNI 代理 TLS 上的 TCP 流量
本文介绍如何通过 Ingress Controller 配置网关,使其基于服务器名称指示(SNI)路由经过 TLS 加密的 TCP 流量。
前 提条件
启动示例上游服务
创建 Kubernetes manifest 文件,定义 TCP echo 上游服务:
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:
- APISIX 网关
- API7 网关
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"
helm upgrade -n aic api7-ee-3-gateway api7/gateway \
--set ... \ # add other parameters
--set "gateway.stream.enabled=true" \
--set "gateway.stream.only=false" \
--set "gateway.stream.tcp[0].addr=9100" \
--set "gateway.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 API
- APISIX CRD
更新 Gateway manifest 文件,添加 TLS 监听器:
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 文件,定义 TLS 配置:
apiVersion: apisix.apache.org/v2
kind: ApisixTls
metadata:
namespace: aic
name: test-tls
spec:
ingressClassName: apisix
hosts:
- test.com
secret:
name: test-tls-secret
namespace: aic
将配置应用到集群:
kubectl apply -f tls.yaml
创建路由
- Gateway API
- APISIX CRD
创建 Kubernetes manifest 文件,定义 TLSRoute:
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
Gateway 监听器和 TLSRoute 中都会出现 hostname 配置,因为它们各自承担不同用途:
- Gateway 监听器的
hostname:指定网关在该监听器上接受哪些 hostname。 - TLSRoute 的
hostnames:定义路由适用的 hostname,用于过滤监听器接收到的流量。
TLSRoute 中的 hostnames 是必填字段,并且表示监听器 hostname 的子集。显式声明可以确保路由匹配更精确。
创建 Kubernetes manifest 文件,定义 TCP 四层路由(stream route):
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: tls-stream-route
namespace: aic
spec:
ingressClassName: apisix
stream:
- name: tls-stream-route
protocol: TCP
match:
ingressPort: 9100
backend:
serviceName: echo-tcp
servicePort: 8443
将配置应用到集群:
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),该连接会被终止。