How to Set Up Kong Gateway on Kubernetes With Gateway API (2026)
This guide is for platform engineers who want Kong Gateway in a Kubernetes cluster, configured the modern way: Gateway API objects for routing and KongPlugin resources for policy. At the end you will have Kong Operator managing a Kong data plane, an HTTPRoute to a test service, API key authentication with a KongConsumer, per-route rate limiting, and an HTTPS listener. I pinned and checked every version against release pages on 10 November 2026: Kong Operator 2.3.4 (Helm chart kong/kong-operator 1.4.3), Kong Gateway 3.16.0.1, the Gateway API v1.6.2 CRDs the chart bundles, and cert-manager v1.21.3. You need a Kubernetes 1.31 to 1.37 cluster (the range Kong lists for Operator 2.3 in its compatibility matrix), Helm, kubectl, and either a Kong Enterprise license file or a Konnect account. A local kind cluster is fine for following along.
One thing to know before you start, because it trips people up: Kong's 3.10.0.0 changelog removed free mode, and the 3.15 breaking changes make the Admin API read-only without a valid license. Kong's own on-prem quickstart includes a KongLicense step for that reason. If you are still choosing a gateway, my API gateway comparison covers what that licensing means next to APISIX, Envoy Gateway and Traefik.
Kong Operator vs Kong Ingress Controller: which one should you install?
Install Kong Operator. Kong Operator 2.0 (October 2025) merged the Kong Ingress Controller (KIC) and Kong Gateway Operator into one project: the ingress controller now runs embedded in the operator, which also provisions the Kong data plane for each Gateway you create. Standalone KIC 3.5 still receives patches, but the old kong/ingress Helm chart still pins app version 3.9, and Gateway API is the path Kong documents for new installs. If you have existing TCPIngress, UDPIngress or KongIngress objects, those are removed or replaced in Operator 2.x, so migrate them first.
Step 1: Install Kong Operator 2.3.4 with Helm
The chart installs the operator, Kong's CRDs, and the Gateway API standard CRDs. I rendered chart 1.4.3 offline with helm template --include-crds to confirm it ships the v1.6.2 standard bundle (including HTTPRoute, TLSRoute, TCPRoute and ListenerSet), so you don't need a separate CRD install on a fresh cluster.
helm repo add kong https://charts.konghq.com
helm repo update
# Pin both the chart and the operator image. Chart 1.4.3 ships operator 2.3.4.
helm upgrade --install kong-operator kong/kong-operator \
--version 1.4.3 \
--namespace kong-system --create-namespace \
--set image.tag=2.3.4
kubectl -n kong-system wait --for=condition=Available=true --timeout=300s \
deployment/kong-operator-kong-operator-controller-manager
Expected output from the wait:
deployment.apps/kong-operator-kong-operator-controller-manager condition met
If the cluster already has Gateway API CRDs from another controller, check their bundle version before installing, because two charts fighting over the same CRDs is a classic upgrade outage:
kubectl get crd gateways.gateway.networking.k8s.io \
-o jsonpath='{.metadata.annotations.gateway\.networking\.k8s\.io/bundle-version}{"\n"}'
# Kong's docs use this for manual upgrades of existing installs:
# kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.2/standard-install.yaml
Operator 2.3 supports Gateway API 1.0 through 1.6. Gateway API v1.7.0 came out on 29 October 2026, but don't jump to it until Kong lists it as supported.
Step 2: Apply your Kong license
The license FAQ says KongLicense is cluster-scoped and one object is shared by every Gateway the operator manages.
# license.json is the file Kong sent you. Keep it out of Git; use SOPS or ESO in real clusters.
echo "
apiVersion: configuration.konghq.com/v1alpha1
kind: KongLicense
metadata:
name: kong-license
rawLicenseString: '$(cat ./license.json)'
" | kubectl apply -f -
kubectl describe konglicense kong-license | grep -B2 'Type:'
# Look for: Reason: PickedAsLatest, Status: True, Type: Programmed
If you use Konnect instead, Kong's quickstart replaces this step with a KonnectAPIAuthConfiguration holding a personal access token and adds spec.konnect.authRef to the GatewayConfiguration. For storing the license file safely, see encrypting Kubernetes secrets with SOPS and age.
Step 3: Create the GatewayConfiguration, GatewayClass and Gateway
GatewayConfiguration is Kong's parameters object: it says which Kong image the data plane runs and lets you customise the pod template. The GatewayClass points at it with controllerName: konghq.com/gateway-operator.
# kong-gateway.yaml
apiVersion: v1
kind: Namespace
metadata:
name: kong
---
apiVersion: gateway-operator.konghq.com/v2beta1
kind: GatewayConfiguration
metadata:
name: kong-configuration
namespace: kong
spec:
dataPlaneOptions:
deployment:
podTemplateSpec:
spec:
containers:
- name: proxy
image: kong/kong-gateway:3.16.0.1 # pin the full patch tag
---
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: kong
spec:
controllerName: konghq.com/gateway-operator
parametersRef:
group: gateway-operator.konghq.com
kind: GatewayConfiguration
name: kong-configuration
namespace: kong
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: kong
namespace: kong
spec:
gatewayClassName: kong
listeners:
- name: http
protocol: HTTP
port: 80
kubectl apply -f kong-gateway.yaml
kubectl wait --for=condition=Programmed gateway/kong -n kong --timeout=5m
kubectl get gateway kong -n kong
Output similar to this (the address comes from your LoadBalancer, so yours will differ):
NAME CLASS ADDRESS PROGRAMMED AGE
kong kong 203.0.113.10 True 94s
The operator creates the ControlPlane and DataPlane objects for you. If PROGRAMMED stays False with reason DependenciesNotReady, give it a couple of minutes, then use the troubleshooting table below.
Step 4: Route traffic with an HTTPRoute
Deploy Kong's sample echo service and send /echo to it:
kubectl apply -n kong -f https://developer.konghq.com/manifests/kic/echo-service.yaml
# echo-route.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: echo
namespace: kong
spec:
parentRefs:
- name: kong
rules:
- matches:
- path:
type: PathPrefix
value: /echo
backendRefs:
- name: echo
port: 1027
kubectl apply -f echo-route.yaml
export PROXY_IP=$(kubectl get gateway kong -n kong -o jsonpath='{.status.addresses[0].value}')
curl -i "http://$PROXY_IP/echo"
You should get HTTP/1.1 200 OK and a body starting with Welcome, you are connected to node. On kind or any cluster without a LoadBalancer, the address stays empty; port-forward to the data plane's proxy Service instead, which the operator labels for you:
SVC=$(kubectl get svc -n kong \
-l gateway-operator.konghq.com/dataplane-service-type=ingress \
-o jsonpath='{.items[0].metadata.name}')
kubectl port-forward -n kong "svc/$SVC" 8080:80 &
export PROXY_IP=127.0.0.1:8080
Step 5: Add key-auth with a KongPlugin and a KongConsumer
Gateway API has no standard auth filter, so Kong attaches plugins through an ExtensionRef filter on the route rule (Kong's preferred method) or the older konghq.com/plugins annotation. Credentials live in a Secret labelled with the credential type, and a KongConsumer links to it.
# key-auth.yaml
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: key-auth
namespace: kong
plugin: key-auth
config:
key_names:
- apikey # header (or query param) Kong reads the key from
---
apiVersion: v1
kind: Secret
metadata:
name: test-user-apikey
namespace: kong
labels:
konghq.com/credential: key-auth
konghq.com/secret: "true"
stringData:
key: change-me-to-a-long-random-value # generate with: openssl rand -hex 24
---
apiVersion: configuration.konghq.com/v1
kind: KongConsumer
metadata:
name: test-user
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
username: test-user
credentials:
- test-user-apikey
Three details are worth knowing here. First, the Secret, the KongConsumer and the HTTPRoute live in the same namespace in this example; keeping consumers next to the routes they use is the simplest model to reason about and to audit. Second, nothing happens yet: the key-auth plugin only takes effect once a route references it, which you do in the next step, so you can create consumers ahead of time without locking anyone out. Third, the key in the Secret is the credential itself, so treat the manifest like any other secret and never commit the plaintext version. In a real setup I generate keys per client, store them through External Secrets Operator or SOPS, and rotate by adding a second Secret to the consumer's credentials list before removing the old one.
Step 6: Add rate limiting and attach both plugins to the route
# rate-limit.yaml
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: rate-limit-5pm
namespace: kong
plugin: rate-limiting
config:
minute: 5
policy: local # counters per data plane pod; use redis for a shared limit
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: echo
namespace: kong
spec:
parentRefs:
- name: kong
rules:
- matches:
- path:
type: PathPrefix
value: /echo
filters:
- type: ExtensionRef
extensionRef:
group: configuration.konghq.com
kind: KongPlugin
name: key-auth
- type: ExtensionRef
extensionRef:
group: configuration.konghq.com
kind: KongPlugin
name: rate-limit-5pm
backendRefs:
- name: echo
port: 1027
kubectl apply -f key-auth.yaml -f rate-limit.yaml
curl -s -o /dev/null -w '%{http_code}\n' "http://$PROXY_IP/echo"
# 401 (no key)
for i in 1 2 3 4 5 6; do
curl -s -o /dev/null -w '%{http_code}\n' -H 'apikey: change-me-to-a-long-random-value' "http://$PROXY_IP/echo"
done
# 200 200 200 200 200 429
The 429 body reads API rate limit exceeded. Gateway API allows repeated ExtensionRef filters (its CRD validation forbids repeating only the header modifier, redirect, URL rewrite and CORS filters), so stacking plugins this way is valid. With policy: local and two data plane replicas, a client can get up to twice the limit; switch to the redis policy when the number matters. For the algorithms behind these limits, see API rate limiting explained.
Step 7: Terminate TLS on the Gateway
Add an HTTPS listener that references a kubernetes.io/tls Secret. For a quick test, a self-signed certificate works:
openssl req -x509 -nodes -days 30 -newkey rsa:2048 \
-keyout tls.key -out tls.crt -subj "/CN=demo.example.com"
kubectl create secret tls demo-example-com -n kong --cert=tls.crt --key=tls.key
# gateway-https.yaml: replaces the Gateway from Step 3
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: kong
namespace: kong
annotations:
cert-manager.io/cluster-issuer: letsencrypt # only if you use cert-manager (below)
spec:
gatewayClassName: kong
listeners:
- name: http
protocol: HTTP
port: 80
- name: https
protocol: HTTPS
port: 443
hostname: demo.example.com
tls:
mode: Terminate
certificateRefs:
- group: ""
kind: Secret
name: demo-example-com
kubectl apply -f gateway-https.yaml
curl -sk --resolve demo.example.com:443:$PROXY_IP \
-H 'apikey: change-me-to-a-long-random-value' https://demo.example.com/echo | head -1
For real certificates, install cert-manager with Gateway API support turned on. The cert-manager Gateway docs use the config.gatewayAPI.enabled value and warn that the Gateway API CRDs must exist before cert-manager starts (or you restart it):
helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \
--version v1.21.3 --namespace cert-manager --create-namespace \
--set crds.enabled=true --set config.gatewayAPI.enabled=true
Then point a ClusterIssuer HTTP-01 solver at the Gateway with solvers[].http01.gatewayHTTPRoute.parentRefs naming kong in namespace kong, as in the HTTP-01 docs. My cert-manager troubleshooting guide covers stuck challenges.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Gateway Programmed=False, reason DependenciesNotReady |
Data plane pods still starting or image pull slow | Wait a few minutes; kubectl get dataplane,controlplane -n kong and kubectl describe pod on the data plane |
| Gateway has no address | No LoadBalancer implementation (kind, bare metal) | Port-forward the Service labelled dataplane-service-type=ingress, or install MetalLB or cloud-provider-kind |
| Routes accepted but changes never reach Kong; operator logs show Admin API errors | No valid license on the Enterprise image (free mode removed in 3.10) | Apply a KongLicense and check it is Programmed=True |
GatewayClass stays Accepted=Unknown |
Wrong controllerName or operator not running |
Use konghq.com/gateway-operator; check the controller-manager Deployment |
HTTPRoute ResolvedRefs=False |
backendRefs name or port wrong, or cross-namespace Service without a ReferenceGrant |
Fix the port (1027 for the echo sample) or add a ReferenceGrant |
401 Unauthorized with a key you think is valid |
Secret missing the konghq.com/credential: key-auth label, or consumer not picked up |
Add the label; confirm the KongConsumer has the kubernetes.io/ingress.class: kong annotation |
401 No API key found in request |
Header name differs from key_names |
Send apikey: or change config.key_names |
| Rate limit higher than configured | policy: local counts per data plane pod |
Use policy: redis with a shared Redis |
CRD conflicts during helm upgrade |
Gateway API CRDs owned by another chart or a different bundle version | Pick one owner for the CRDs; upgrade them with kubectl apply --server-side |
kubectl apply of KongIngress or TCPIngress fails after migration |
Removed or replaced in Operator 2.x | Move to KongUpstreamPolicy and TCPRoute |
FAQ
Does Kong Gateway on Kubernetes need a license?
The current kong/kong-gateway image does for full function: Kong's changelog says free mode was removed in 3.10, and since 3.15 an unlicensed or expired gateway has a read-only Admin API. Kong's on-prem quickstart applies a KongLicense; the alternative is Konnect hybrid mode. The open-source kong/kong image is frozen at the 3.9 line.
Should I use the konghq.com/plugins annotation or ExtensionRef?
Use ExtensionRef filters for new routes. Kong's docs call the annotation the legacy method. ExtensionRef also lets you attach a plugin to one rule instead of the whole route.
Can I run Kong Operator alongside another Gateway API controller?
Yes. Each GatewayClass names its controller, so Kong only reconciles Gateways whose class uses konghq.com/gateway-operator. The shared risk is the Gateway API CRDs themselves: make one tool own their installation and version.
How do I upgrade Kong Gateway versions?
Change the image tag in the GatewayConfiguration and the operator rolls the data plane. Read Kong's breaking changes for every minor version you cross, and check the operator's compatibility table before changing the operator chart.
What I check first in production
When a Kong install misbehaves, I look at three things before anything else: whether the Gateway and HTTPRoute conditions say Programmed and ResolvedRefs, whether the license object is programmed, and whether the data plane pods actually got the new config (the operator logs say so). Most problems are one of those. In production I would also pin every image by digest, keep the KongPlugin objects in their own directory so policy changes get their own review, and run at least two data plane replicas with a Redis-backed rate limit. Official docs: Kong Operator Gateway API quickstart, plugins on HTTPRoute, consumers and credentials, TLS termination and the Gateway API docs.
Related: How to Migrate From ingress-nginx to Gateway API: Step-by-Step (2026), How to Set Up cert-manager with Let's Encrypt and Fix Errors (2026) and Kubernetes Secrets in GitOps (2026): SOPS vs Sealed Secrets vs ESO.
Spot an error or something out of date? Email me and I will fix it and log it on the corrections page.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
How to Set Up Actions Runner Controller (ARC) on Kubernetes (2026)
Step-by-step ARC 0.15.1 setup for GitHub Actions self-hosted runners on Kubernetes: GitHub App auth, pinned Helm charts, runner scale sets, runner groups, Docker-in-Docker vs kubernetes container mode, scale to zero, metrics, and fixes for jobs stuck in queued.
13 minNov 10, 2026How to Build an Azure Pipelines YAML Pipeline: Step-by-Step (2026)
Build an azure-pipelines.yml from scratch in 2026: stages, jobs and steps, reusable templates, variable groups linked to Key Vault, a workload identity federation service connection, environments with approvals, and deployment to AKS or App Service, plus fixes for the common errors.
13 minOct 31, 2026How to Migrate Jenkins to GitHub Actions: Step-by-Step Guide (2026)
Migrate Jenkins pipelines to GitHub Actions in 2026: inventory jobs, plugins and credentials, run GitHub Actions Importer audit, forecast and dry-run, map Jenkinsfile stages to jobs, move keys to secrets and OIDC, rebuild shared libraries as reusable workflows, and run ARC 0.15.1 runners.
16 min