Docs
Ingress ControllerManage traffic and APIsTraffic ManagementProxy TCP Traffic over TLS by SNI

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 kubectl configured 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:

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 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=120s

Step 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"

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.crt

Expected: 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=aic

Step 4: Configure gateway TLS

Add a TLS listener to your existing Gateway manifest:

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

Step 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:

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

Apply the policy:

kubectl apply -f echo-upstream-policy.yaml

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.

Step 6: Create the route

Create a TLSRoute manifest:

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

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

Validate

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/null

Expected: 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/null

Expected: 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.crt

Stop the local port-forward process if it is still running:

kill %1 2>/dev/null || true

Troubleshoot

SymptomCauseFix
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}.