Proxy TCP Traffic over TLS by SNI
Route TLS-encrypted TCP traffic by Server Name Indication (SNI) using either a Gateway API TLSRoute or an APISIX CRD stream route.
Prerequisite
- Complete Set Up Ingress Controller and Gateway.
Start an Example Upstream Service
Create a Kubernetes manifest file for a TCP echo upstream service:
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
Apply the configuration to your cluster:
kubectl apply -f echo-tcp.yaml
Enable Gateway Stream Proxy
Upgrade your gateway to enable stream mode and set TCP port 9100:
- APISIX Gateway
- API7 Gateway
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"
Generate Certificate and Key
Generate the certificate authority (CA) key and certificate:
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
Generate the key and certificate with the common name test.com, and sign with the CA certificate:
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
Create a Kubernetes TLS secret:
kubectl create secret tls test-tls-secret \
--cert=server.crt \
--key=server.key \
--namespace=aic
Configure Gateway TLS
- Gateway API
- APISIX CRD
Update your Gateway manifest file to add a TLS listener:
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
The listener port does not open a data plane port. Configure the gateway stream proxy to listen on the same port before applying the Gateway. The Ingress Controller does not update or restart the data plane when this field changes.
Apply the configuration to your cluster:
kubectl apply -f gateway.yaml
Create a Kubernetes manifest file for TLS configuration:
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
Apply the configuration to your cluster:
kubectl apply -f tls.yaml
Select the Backend Transport
The TLS listener in this example terminates client TLS at the gateway, while echo-tcp accepts plain TCP. Use the tcp upstream scheme for this topology. If the backend itself accepts TLS, set the scheme to tls so the gateway establishes a new TLS connection upstream.
For a Gateway API route, attach the scheme to the backend Service port:
apiVersion: apisix.apache.org/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: echo-tcp-transport
namespace: aic
spec:
targetRefs:
- group: ""
kind: Service
name: echo-tcp
sectionName: tcp
scheme: tcp
For an APISIX CRD stream route, create an ApisixUpstream with the same name as the backend Service:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
name: echo-tcp
namespace: aic
spec:
ingressClassName: apisix
scheme: tcp
Apply the manifest that matches your route type. Do not use tcp, tls, or udp on an HTTP route; L4 schemes make an L7 upstream unreachable.
To attach stream plugins to the TLSRoute, see Apply Plugins to L4 Routes.
Create a Route
- Gateway API
- APISIX CRD
Create a Kubernetes manifest that defines a TLSRoute:
apiVersion: gateway.networking.k8s.io/v1
kind: TLSRoute
metadata:
namespace: aic
name: tls-route
spec:
parentRefs:
- name: apisix
hostnames: ["test.com"]
rules:
- backendRefs:
- name: echo-tcp
port: 8443
The hostname configuration appears in both the Gateway listener and the TLSRoute because each serves a distinct purpose:
- Gateway listener
hostname: specifies which host names the gateway will accept on this listener. - TLSRoute
hostnames: defines the host names to which the route applies, effectively filtering traffic received by the listener.
The hostnames field in the TLSRoute is required. At least one route hostname must intersect with the listener hostname for the route to attach. Route entries that do not match the listener are ignored.
Create a Kubernetes manifest that defines a 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
Apply the configuration to your cluster:
kubectl apply -f tls-tcp-route.yaml
Verify
Expose the gateway’s TCP TLS service port to your local machine:
# replace with your gateway’s service name
kubectl port-forward svc/<gateway-service-name> 9100:9100 &
To test that TLS-encrypted TCP traffic is routed correctly by the gateway using the SNI test.com, run:
openssl s_client -connect 127.0.0.1:9100 -servername test.com
After the TLS handshake completes, you can type any text and press Enter. The upstream echo server will echo it back, confirming that the gateway correctly terminated TLS and forwarded the plain TCP payload to the upstream:
...
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
To verify the behavior when the SNI does not correspond to any configured route, run:
openssl s_client -connect 127.0.0.1:9100 -servername random.com
The connection will be terminated as the gateway cannot identify a matching stream route.