Skip to main content

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

  1. Complete Set Up Ingress Controller and Gateway.

Start an Example Upstream Service

Create a Kubernetes manifest file for a TCP echo upstream service:

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

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:

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"

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

Update your Gateway manifest file to add a TLS listener:

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

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

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:

echo-upstream-policy.yaml
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:

echo-upstream.yaml
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

Create a Kubernetes manifest that defines a TLSRoute:

tls-tcp-route.yaml
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
about hostname

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.

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.