How to Set Up Kong Gateway on Kubernetes With Gateway API (2026)

Nov 10, 2026 · 11 min · Ajay Kumar

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.

What you create vs what Kong Operator creates 1. Helm install kong/kong-operator 1.4.3 + Gateway API v1.6.2 CRDs 2. You apply GatewayConfiguration GatewayClass, Gateway 3. Operator creates ControlPlane + DataPlane LoadBalancer Service 4. You apply HTTPRoute + KongPlugin KongConsumer + Secret 5. Request path client to Gateway address key-auth, rate limit, backend Enterprise image: apply a cluster-scoped KongLicense before creating the Gateway. Konnect hybrid mode replaces the license step with a KonnectAPIAuthConfiguration in the same position.
The objects in this guide and who owns them. Versions are those pinned in the steps below; the flow follows Kong's Gateway API quickstart for Kong Operator.

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