A complete Kubernetes deployment for Kong Event Gateway using Kong Operator 2.2+ and Strimzi Kafka. Demonstrates multi-tenant Kafka governance for the Northwind Financial use case via SNI-based virtual cluster routing.
Northwind Financial is a mid-size financial services firm with two business units — Retail Banking NY and Wealth Management LA — sharing a single Kafka cluster. This deployment shows how Kong Event Gateway provides isolated, governed access to that shared cluster without touching broker configuration.
All components — the Event Gateway data plane, virtual clusters, listeners, and policies — are managed declaratively through Kong Operator CRDs. No static deployment YAML is required.
| Business Unit | Virtual Cluster | Kafka Prefix | Topics (logical names) |
|---|---|---|---|
| Retail Banking NY | retail-banking-ny |
RETAIL_NY. |
payments.card-dispatch.v1 · markets.exchange-ticker.v1 · branches.commuter-foot-traffic.v1 · core.accounts.status.v2 |
| Wealth Management LA | wealth-management-la |
WEALTH_LA. |
advisor.daily-client-activity.v1 · portfolios.esg-allocation-adjustments.v1 · clients.sentiment-signals.v1 · infosec.security.fraud.risk-scores.v3 |
Topic prefixes are transparent to clients — Retail Banking connects to payments.card-dispatch.v1; the gateway silently maps it to RETAIL_NY.payments.card-dispatch.v1 on the broker.
| Business Unit | Bootstrap Address |
|---|---|
| Retail Banking NY | bootstrap.retail-banking-ny.127-0-0-1.sslip.io:9092 |
| Wealth Management LA | bootstrap.wealth-management-la.127-0-0-1.sslip.io:9092 |
For non-minikube clusters replace
127-0-0-1with your LoadBalancer IP in dotted-dash notation (e.g.192-168-49-2for192.168.49.2).
graph TB
subgraph host["Host Machine (minikube tunnel → 127.0.0.1)"]
clientA["Kafka Client\nRetail Banking NY\nbootstrap.retail-banking-ny\n.127-0-0-1.sslip.io:9092"]
clientB["Kafka Client\nWealth Management LA\nbootstrap.wealth-management-la\n.127-0-0-1.sslip.io:9092"]
end
subgraph konnect["Konnect (Control Plane)"]
cp["KonnectEventGateway\ncp-event-gateway"]
end
subgraph k8s["Kubernetes Cluster"]
subgraph certmgr["cert-manager"]
cert["Certificate\nkeg-certificate\n→ keg-tls-secret"]
end
subgraph kongns["Namespace: kong"]
gw["Kong Gateway\nGateway API · port 9092\nTLS Passthrough\n*.127-0-0-1.sslip.io"]
tlsA["TLSRoute\nkeg-tls-retail-banking-ny\n*.retail-banking-ny.*"]
tlsB["TLSRoute\nkeg-tls-wealth-management-la\n*.wealth-management-la.*"]
keg["KegDataPlane\nkeg-dp / keg-dp-kafka:9092\nTLS termination · SNI routing"]
subgraph kegconfig["KEG Configuration (Konnect-synced)"]
bc["EventGatewayBackendCluster\nnorthwind-kafka-bootstrap:9092"]
vcA["VirtualCluster\nretail-banking-ny\nprefix: RETAIL_NY."]
vcB["VirtualCluster\nwealth-management-la\nprefix: WEALTH_LA."]
listener["EventGatewayListener\nport 9092"]
polTLS["ListenerPolicy\nsni-tls-server\nTLS termination"]
polSNI["ListenerPolicy\nsni-forward\nSNI suffix routing"]
end
end
subgraph kafkans["Namespace: kafka"]
strimzi["Strimzi Operator\nv1.0.1+"]
kafka["Kafka Cluster\nnorthwind · 3 brokers · KRaft\nKafka 4.2.0"]
topics["KafkaTopics\nRETAIL_NY.* · WEALTH_LA.*"]
end
end
clientA -->|"TLS :9092\nSNI: *.retail-banking-ny.*"| gw
clientB -->|"TLS :9092\nSNI: *.wealth-management-la.*"| gw
gw --> tlsA & tlsB
tlsA & tlsB -->|"TLS passthrough"| keg
keg -->|"SNI → retail-banking-ny"| vcA
keg -->|"SNI → wealth-management-la"| vcB
vcA & vcB --> bc
bc -->|"PLAINTEXT\ninternal"| kafka
cert -.->|"keg-tls-secret"| keg
cp -.->|"operator sync"| keg
cp -.->|"operator sync"| kegconfig
strimzi -.->|"manages"| kafka
strimzi -.->|"manages"| topics
- Client connects to
bootstrap.retail-banking-ny.127-0-0-1.sslip.io:9092with TLS — DNS resolves to127.0.0.1via sslip.io,minikube tunnelroutes to the Kong Gateway LoadBalancer - Kong
Gatewaymatches the*.retail-banking-ny.127-0-0-1.sslip.ioTLSRouteand forwards (TLS passthrough) tokeg-dp-kafka:9092 - KEG terminates TLS, reads the SNI suffix (
.127-0-0-1.sslip.io), and routes to theretail-banking-nyvirtual cluster - Client produces to
payments.card-dispatch.v1→ KEG transparently writes toRETAIL_NY.payments.card-dispatch.v1on the broker; the prefix is never visible to the client
- Kubernetes cluster (1.24+) — tested with minikube
kubectlconfiguredhelm3.x- A Konnect account and Personal Access Token (PAT)
- Gateway API standard CRDs v1.5.1 (installed in step 2)
⚠️ Minikube clock drift: If you leave minikube running while your computer sleeps, the system clock may drift and cause certificate validation failures. Runminikube stop && minikube startto reset.
If you are using minikube for local development, configure it before starting.
This deployment requires approximately 3.5 CPU cores and 6 GB of memory. Start minikube with adequate resources:
minikube start --cpus=6 --memory=8gTo adjust an existing cluster:
minikube stop
minikube start --cpus=6 --memory=8gminikube addons enable metrics-serverRun minikube tunnel in a separate terminal (keep it running for the entire session). This exposes LoadBalancer services on 127.0.0.1, making the sslip.io DNS names resolve correctly:
# Run in a separate terminal — requires sudo on some systems
sudo minikube tunnel -p <your-profile-name>| Component | CPU Request | CPU Limit | Memory Request | Memory Limit | Replicas |
|---|---|---|---|---|---|
| Kafka Brokers (Strimzi) | 750m | 1.2 cores | 2.3 GB | 3 GB | 3 |
| Kong Event Gateway (KEG) | 500m | 750m | 512 MB | 1 GB | 1 |
| Kong Gateway (Operator) | 500m | 1 core | 512 MB | 1.5 GB | 1 |
| Kong Operator | 100m | 400m | 256 MB | 512 MB | 1 |
| Strimzi Operator | 100m | 400m | 256 MB | 512 MB | 1 |
| cert-manager | 100m | 250m | 128 MB | 256 MB | 1 |
| Total | ~3.3 cores | ~5.7 cores | ~6 GB | ~9 GB |
kubectl create namespace kong
kubectl create namespace kafkaThe TLSRoute resource is GA and has been part of the Standard Channel since v1.5.0. :
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.1/standard-install.yamlDo not use
--server-sidehere. Kong Operator's Helm chart also installs Gateway API objects via server-side apply (field manager:helm). Usingkubectl apply --server-sidefirst registerskubectlas field manager on the same fields, causing a conflict when Helm runs. If you have already applied with--server-side, run this to transfer ownership before retrying the Helm install:kubectl apply --server-side --force-conflicts --field-manager=helm \ -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.1/standard-install.yaml
Wait for the CRDs to be established:
kubectl wait --for=condition=Established crd/gateways.gateway.networking.k8s.io --timeout=60s
kubectl wait --for=condition=Established crd/tlsroutes.gateway.networking.k8s.io --timeout=60scert-manager automates TLS certificate issuance and renewal for KEG.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.18.2/cert-manager.yamlWait for cert-manager to be ready before proceeding:
kubectl wait --for=condition=Available deployment/cert-manager -n cert-manager --timeout=5m
kubectl wait --for=condition=Available deployment/cert-manager-webhook -n cert-manager --timeout=5mStrimzi manages the Kafka cluster declaratively. The operator version must match the Kafka version in kafka/kafka-cluster.yaml (Strimzi 1.0.1 + Kafka 4.2.0).
-
Install the Strimzi cluster operator into the
kafkanamespace:kubectl create -f 'https://strimzi.io/install/latest?namespace=kafka' -n kafka -
Wait for the Strimzi operator to be ready:
kubectl wait --for=condition=Available deployment/strimzi-cluster-operator -n kafka --timeout=5m -
Deploy the Northwind Kafka cluster (3-node KRaft, combined controller+broker):
kubectl apply -f kafka/kafka-cluster.yaml
-
Wait for all brokers to be ready (this takes 2–4 minutes on first run):
kubectl wait kafka/northwind -n kafka \ --for=condition=Ready \ --timeout=10m -
Deploy the Northwind topic definitions (Strimzi Topic Operator creates them automatically):
kubectl apply -f kafka/topics.yaml
Verify topics were created:
kubectl get kafkatopic -n kafka -l strimzi.io/cluster=northwind
Kong Operator manages both the Kong Gateway data plane and the KEG data plane.
helm repo add kong https://charts.konghq.com
helm repo update
helm upgrade --install kong-operator kong/kong-operator \
-n kong-system \
--create-namespace \
--set image.tag=2.2 \
--set env.ENABLE_CONTROLLER_KONNECT=true \
--set env.ENABLE_CONTROLLER_KEGDATAPLANE=true \
--set global.webhooks.options.certManager.enabled=trueWait for the operator to be ready:
kubectl wait --for=condition=Available deployment/kong-operator-kong-operator-controller-manager \
-n kong-system --timeout=5mcert-manager integration (recommended): To have cert-manager manage the operator's webhook certificates, we added
--set global.webhooks.options.certManager.enabled=trueto the Helm command above. Remove it if you are handling it manually.
Create your Konnect Personal Access Token in the Konnect PAT page, then store it as a Kubernetes secret:
export KONNECT_TOKEN='<YOUR_KONNECT_PAT>'
echo 'apiVersion: v1
kind: Secret
metadata:
name: konnect-api-auth-secret
namespace: kong
labels:
konghq.com/credential: konnect
konghq.com/secret: "true"
stringData:
token: "'$KONNECT_TOKEN'"' | kubectl apply -f -Apply the KonnectAPIAuthConfiguration that references this secret:
kubectl apply -f keg/konnect-auth.yamlKEG terminates TLS for Kafka clients. A wildcard certificate covering both virtual cluster hostnames is required.
Option A: cert-manager (Recommended)
-
Apply the self-signed
ClusterIssuer:kubectl apply -f certificates/cluster-issuer.yaml
-
Apply the wildcard
Certificate(createskeg-tls-secretin thekongnamespace):kubectl apply -f certificates/keg-certificate.yaml
-
Wait for the certificate to be issued:
kubectl wait certificate/keg-certificate -n kong \ --for=condition=Ready \ --timeout=5m
Option B: Manual with openssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=127-0-0-1.sslip.io" \
-addext "subjectAltName=\
DNS:*.127-0-0-1.sslip.io,\
DNS:*.retail-banking-ny.127-0-0-1.sslip.io,\
DNS:*.wealth-management-la.127-0-0-1.sslip.io"
kubectl create secret tls keg-tls-secret \
--cert=tls.crt --key=tls.key \
-n kongApply the KEG resources in order, waiting for each to be programmed in Konnect before proceeding.
Control plane:
kubectl apply -f keg/event-gateway-cp.yaml
kubectl wait konnecteventgateway/cp-event-gateway -n kong \
--for=condition=Programmed=True \
--timeout=10mBackend cluster (points KEG at the Strimzi Kafka bootstrap):
kubectl apply -f keg/backend-cluster.yaml
kubectl wait eventgatewaybackendcluster/default-backend-cluster -n kong \
--for=condition=Programmed=True \
--timeout=10mVirtual clusters (Retail Banking NY and Wealth Management LA with topic prefixing):
kubectl apply -f keg/virtual-clusters.yaml
kubectl wait eventgatewayvirtualcluster/retail-banking-ny -n kong \
--for=condition=Programmed=True \
--timeout=10m
kubectl wait eventgatewayvirtualcluster/wealth-management-la -n kong \
--for=condition=Programmed=True \
--timeout=10mListener (accepts Kafka client connections on port 9092):
kubectl apply -f keg/listener.yaml
kubectl wait eventgatewaylistener/sni-listener -n kong \
--for=condition=Programmed=True \
--timeout=10mListener policies (TLS termination + SNI-based routing to virtual clusters):
kubectl apply -f keg/listener-policies.yaml
kubectl wait eventgatewaylistenerpolicy/sni-tls-server -n kong \
--for=condition=Programmed=True \
--timeout=10m
kubectl wait eventgatewaylistenerpolicy/sni-forward -n kong \
--for=condition=Programmed=True \
--timeout=10mKEG data plane (Kong Operator creates and manages the pod + ClusterIP service):
kubectl apply -f keg/keg-dataplane.yamlWait for the KEG pod to be running:
kubectl wait pod -n kong \
--for=condition=Ready \
--selector app.kubernetes.io/name=keg \
--timeout=10mConsume and produce policies (optional — adds tenant headers to records):
kubectl apply -f keg/consume-produce-policies.yamlKong Operator provisions a managed Kong Gateway data plane. The GatewayClass and GatewayConfiguration tell the operator which image to use and how to connect to Konnect.
GatewayConfiguration and GatewayClass:
kubectl apply -f kong/gateway-config.yaml
kubectl wait gatewayclass/kong \
--for=condition=Accepted=True \
--timeout=5mGateway (TLS passthrough listener on port 9092, hostname *.127-0-0-1.sslip.io):
kubectl apply -f kong/gateway.yaml
kubectl wait gateway/kong-keg -n kong \
--for=condition=Accepted=True \
--timeout=10mTLSRoutes (one per virtual cluster, pointing at keg-dp-kafka ClusterIP service):
kubectl apply -f kong/tlsroutes.yamlNote:
kubectl wait --for=condition=Accepteddoes not work for Gateway API route resources. TLSRoute (and all*Routetypes) store their status conditions nested understatus.parents[].conditions[]— one entry per parent Gateway — not at the top-levelstatus.conditions[]thatkubectl waitinspects. Verify acceptance instead with:kubectl get tlsroute -n kong -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{range .status.parents[*]} parent: {.parentRef.name} accepted: {range .conditions[?(@.type=="Accepted")]}{.status}{"\n"}{end}{end}{end}'Both routes should show
accepted: Truebefore proceeding.
# All KEG operator resources should be Programmed=True
kubectl get konnecteventgateway,eventgatewaybackendcluster,eventgatewayvirtualcluster,eventgatewaylistener,eventgatewaylistenerpolicy,kegdataplane -n kong
# Gateway and TLSRoutes
kubectl get gateway,tlsroute -n kong
# Kafka cluster and topics
kubectl get kafka,kafkatopic -n kafka
# All pods running
kubectl get pods -n kong
kubectl get pods -n kafka
kubectl get pods -n kong-systemRun these commands on your host machine, not inside a Kubernetes pod. The
127-0-0-1.sslip.ioDNS trick resolves to127.0.0.1, whichminikube tunnelbinds to on the host. Inside a pod,127.0.0.1is the pod's own loopback and the connection will be refused.
Make sure minikube tunnel is running (see LoadBalancer Access).
Option A — kafkactl (recommended)
# Retail Banking NY
kafkactl --context retail-banking-ny get brokers
# Wealth Management LA
kafkactl --context wealth-management-la get brokers
# Or use the helper script
./scripts/kafkactl-helper.sh smoke-testOption B — kcat on the host
# Install kcat locally if needed: brew install kcat
# Retail Banking NY
kcat -b bootstrap.retail-banking-ny.127-0-0-1.sslip.io:9092 \
-X security.protocol=SSL \
-X enable.ssl.certificate.verification=false \
-L
# Wealth Management LA
kcat -b bootstrap.wealth-management-la.127-0-0-1.sslip.io:9092 \
-X security.protocol=SSL \
-X enable.ssl.certificate.verification=false \
-LExpected output — brokers advertised as broker-{0,1,2}.<cluster>.127-0-0-1.sslip.io:9092:
Metadata for all topics (from broker -1: bootstrap.retail-banking-ny.127-0-0-1.sslip.io:9092/bootstrap):
3 brokers:
broker 0 at broker-0.retail-banking-ny.127-0-0-1.sslip.io:9092
broker 1 at broker-1.retail-banking-ny.127-0-0-1.sslip.io:9092
broker 2 at broker-2.retail-banking-ny.127-0-0-1.sslip.io:9092 (controller)
./scripts/cleanup.shkong-event-gw-kubernetes/
├── kafka/
│ ├── kafka-cluster.yaml # Strimzi KafkaNodePool + Kafka (northwind cluster)
│ └── topics.yaml # KafkaTopic resources for all Northwind topics
├── keg/ # Kong Event Gateway operator CRDs (all in kong namespace)
│ ├── konnect-auth.yaml # KonnectAPIAuthConfiguration (secretRef)
│ ├── event-gateway-cp.yaml # KonnectEventGateway control plane
│ ├── backend-cluster.yaml # EventGatewayBackendCluster → northwind Kafka
│ ├── virtual-clusters.yaml # EventGatewayVirtualCluster (retail-banking-ny, wealth-management-la)
│ ├── listener.yaml # EventGatewayListener (port 9092)
│ ├── listener-policies.yaml # TLS server termination + SNI forwarding policies
│ ├── keg-dataplane.yaml # KegDataPlane (ClusterIP; operator manages pod lifecycle)
│ └── consume-produce-policies.yaml # x-kong-tenant header policies per virtual cluster
├── kong/ # Kong Operator Gateway resources
│ ├── gateway-config.yaml # GatewayConfiguration + GatewayClass (gateway-operator)
│ ├── gateway.yaml # Gateway (TLS passthrough · *.127-0-0-1.sslip.io:9092)
│ └── tlsroutes.yaml # TLSRoute per virtual cluster → keg-dp-kafka
├── certificates/
│ ├── cluster-issuer.yaml # cert-manager ClusterIssuer (self-signed)
│ └── keg-certificate.yaml # Certificate (wildcard, creates keg-tls-secret in kong ns)
├── examples/
│ ├── kafka-client-configs/
│ │ ├── retail-banking-ny-client.properties
│ │ └── wealth-management-la-client.properties
│ └── test-commands.md # Detailed testing, isolation verification, diagnostics
└── scripts/
├── cleanup.sh # Full teardown
├── generate-wildcard-cert.sh # Manual cert generation (alternative to cert-manager)
└── kafkactl-helper.sh # Kafka admin helper
| Virtual Cluster | dnsLabel |
Kafka Prefix | Example mapping |
|---|---|---|---|
retail-banking-ny |
retail-banking-ny |
RETAIL_NY. |
client: payments.card-dispatch.v1 → broker: RETAIL_NY.payments.card-dispatch.v1 |
wealth-management-la |
wealth-management-la |
WEALTH_LA. |
client: clients.sentiment-signals.v1 → broker: WEALTH_LA.clients.sentiment-signals.v1 |
The sni-forward listener policy (in keg/listener-policies.yaml) routes by hostname suffix:
| Hostname pattern | Routes to |
|---|---|
*.retail-banking-ny.127-0-0-1.sslip.io |
retail-banking-ny virtual cluster |
*.wealth-management-la.127-0-0-1.sslip.io |
wealth-management-la virtual cluster |
For a non-minikube cluster, replace 127-0-0-1 with your LoadBalancer IP in dotted-dash form. Example for IP 10.0.0.50:
kong/gateway.yaml—hostname: "*.10-0-0-50.sslip.io"keg/listener-policies.yaml—sniSuffix: ".10-0-0-50.sslip.io"certificates/keg-certificate.yaml— updatednsNamesentriesexamples/kafka-client-configs/*.properties— updatebootstrap.servers
| Component | Version |
|---|---|
| Kong Operator | 2.2+ |
| Kong Gateway | 3.14 |
| Strimzi | 1.0.1 |
| Kafka | 4.2.0 |
| Gateway API CRDs | 1.5.0 (standard) |
| cert-manager | 1.18.2 |
- Kong Event Gateway
- Kong Operator — Event Gateway
- Install Kong Operator for Event Gateway
- Deploy with TLSRoute and SNI
- Strimzi Kafka Operator
Apache License 2.0 — see LICENSE.