Proxy TCP Traffic over TLS by SNI
Configure APISIX or API7 Ingress Controller to proxy TCP traffic over TLS by Server Name Indication (SNI).
Applies to:API7 Ingress Controller (drives API7 Gateway)APISIX Ingress Controller (drives Apache APISIX)
Goal
Route TLS-encrypted TCP traffic by Server Name Indication (SNI) to a Kubernetes echo Service, using either a Gateway API TLSRoute or an APISIX CRD stream route.
Before you begin
- Complete Set Up Ingress Controller and Gateway.
- You have
kubectlconfigured against the cluster used in the setup guide. - You have OpenSSL installed to create the test CA and server certificate.
- Choose a free local port for the gateway TLS stream port. The examples use
9100; change this if it conflicts.
Set the variables this guide uses:
export TLS_SNI="test.com"
export GATEWAY_SERVICE_NAME="apisix-gateway" # your gateway's Service name in namespace aic
export GATEWAY_TLS_PORT="9100"
export TEST_MESSAGE="hello world"Steps
Step 1: Deploy the example TCP echo upstream
Create a manifest for a TCP echo server that listens on port 8443 in the aic namespace:
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: 8443Apply the manifest and wait for the Pod to become ready:
kubectl apply -f echo-tcp.yaml
kubectl wait --for=condition=ready pod -l app=echo-tcp -n aic --timeout=120sStep 2: Enable the gateway stream proxy for TLS
Upgrade the gateway Helm release to enable stream mode and expose TCP port 9100 with TLS:
helm upgrade -n aic apisix apisix/apisix \
--reuse-values \
--set "service.stream.enabled=true" \
--set "service.stream.tcp[0].addr=${GATEWAY_TLS_PORT}" \
--set "service.stream.tcp[0].tls=true"helm upgrade -n aic api7-ee-3-gateway api7/gateway \
--reuse-values \
--set "gateway.stream.enabled=true" \
--set "gateway.stream.only=false" \
--set "gateway.stream.tcp[0].addr=${GATEWAY_TLS_PORT}" \
--set "gateway.stream.tcp[0].tls=true"Step 3: Generate the test CA and server certificate
Create a local CA and a server certificate whose common name matches TLS_SNI:
openssl genrsa -out tls-ca.key 2048
openssl req -x509 -new -sha256 -days 1 -key tls-ca.key -out tls-ca.crt -subj "/CN=TLS Guide CA"
openssl genrsa -out tls-server.key 2048
openssl req -new -sha256 -key tls-server.key -out tls-server.csr -subj "/CN=${TLS_SNI}"
openssl x509 -req -sha256 -days 1 -in tls-server.csr -CA tls-ca.crt -CAkey tls-ca.key -CAcreateserial \
-extfile <(printf "subjectAltName=DNS:%s\nextendedKeyUsage=serverAuth\n" "${TLS_SNI}") \
-out tls-server.crt
openssl verify -CAfile tls-ca.crt tls-server.crtExpected: tls-server.crt: OK.
Create a Kubernetes TLS secret from the generated certificate and key:
kubectl create secret tls test-tls-secret \
--cert=tls-server.crt \
--key=tls-server.key \
--namespace=aicStep 4: Configure gateway TLS
Add a TLS listener to your existing Gateway manifest:
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-configThe listener port does not open a data plane port by itself. 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 Gateway:
kubectl apply -f gateway.yamlCreate an ApisixTls manifest:
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: aicApply it:
kubectl apply -f tls.yamlStep 5: Set the upstream transport scheme
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.
Attach a BackendTrafficPolicy to the backend Service:
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: tcpApply the policy:
kubectl apply -f echo-upstream-policy.yamlCreate 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: tcpApply the upstream configuration:
kubectl apply -f echo-upstream.yamlDo 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.
Step 6: Create the route
Create a TLSRoute manifest:
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: 8443The 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 accepts 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.
Apply the TLSRoute:
kubectl apply -f tls-tcp-route.yamlCreate a TCP stream route manifest:
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: 8443Apply the stream route:
kubectl apply -f tls-tcp-route.yamlValidate
Forward the gateway TLS stream port to your local machine:
kubectl port-forward -n aic "svc/${GATEWAY_SERVICE_NAME}" "${GATEWAY_TLS_PORT}:${GATEWAY_TLS_PORT}" &Test that TLS-encrypted TCP traffic is routed correctly by the gateway using the SNI test.com:
printf '%s\n' "${TEST_MESSAGE}" | openssl s_client -connect 127.0.0.1:"${GATEWAY_TLS_PORT}" -servername "${TLS_SNI}" -CAfile tls-ca.crt -quiet 2>/dev/nullExpected: the message hello world is echoed back after the TLS handshake.
To verify that an unknown SNI is rejected, run:
printf '%s\n' "${TEST_MESSAGE}" | openssl s_client -connect 127.0.0.1:"${GATEWAY_TLS_PORT}" -servername random.com -CAfile tls-ca.crt -quiet 2>/dev/nullExpected: the connection terminates because the gateway cannot identify a matching stream route.
Roll back / Clean up
Delete the manifests and test secrets created in this guide:
kubectl delete -f tls-tcp-route.yaml
kubectl delete -f gateway.yaml
kubectl delete -f tls.yaml
kubectl delete -f echo-tcp.yaml
kubectl delete -f echo-upstream-policy.yaml
kubectl delete -f echo-upstream.yaml
kubectl delete secret test-tls-secret -n aic
rm -f tls-ca.key tls-ca.crt tls-ca.srl tls-server.key tls-server.csr tls-server.crtStop the local port-forward process if it is still running:
kill %1 2>/dev/null || trueTroubleshoot
| Symptom | Cause | Fix |
|---|---|---|
openssl s_client fails to connect or reports a certificate error. | The gateway stream listener is not enabled for TLS, the TLSRoute parentRefs do not resolve, or the SNI does not match. | Confirm the Helm release has service.stream.enabled=true, service.stream.tcp[0].addr=9100, and service.stream.tcp[0].tls=true (or the API7 Gateway equivalents), the Gateway listener protocol is TLS and hostname matches TLS_SNI, and the TLSRoute hostnames intersect with the listener. |
| The echo server does not respond. | The BackendTrafficPolicy or ApisixUpstream scheme is wrong, or the upstream Pod is not ready. | Verify scheme: tcp targets the echo-tcp Service and the Pod is ready. |
| The connection succeeds but the SNI test fails. | The Gateway listener hostname or TLSRoute hostnames do not include the requested SNI. | Check that the listener hostname and route hostnames both include ${TLS_SNI}. |