Cilium Gateway API: The 1.20 Update

A couple of months ago I wrote about my adventures with the Kubernetes Gateway API and Cilium: https://enaplo.hu/2026/07/03/deploying-cilium-gateway-api/

If you haven’t read it, the short version is: it should have been easy, it wasn’t, and I ended up running an older Gateway API version together with an experimental TLSRoute CRD because Cilium 1.19.5 still expected the old v1alpha2 TLSRoute API. I finished that post with this:

I expect both workarounds to disappear after upgrading to Cilium 1.20 together with Gateway API 1.6.

Well… They did.

Time to try again

I deliberately stopped the Gateway API migration at that point. There was no point spending more time trying to make the Cilium 1.19 implementation happy with the newer Gateway API CRDs, especially since Cilium 1.20 was already on the horizon.

The existing setup was perfectly usable, with just one small test domain running through the new Gateway. So I waited. Now that Cilium 1.20.1 is available, it was time to try again. The cluster was still running:

Kubernetes 1.33.13
Cilium 1.19.5

with the same Helm configuration from the original article.

Before upgrading, I checked the cluster for the Cilium 1.20 compatibility issues that were relevant to my setup. Nothing suspicious showed up, and a Helm dry-run with the existing values.yaml completed successfully. So, let’s do this.

Gateway API 1.6.1

First I upgraded the Gateway API CRDs to 1.6.1. This time there was no need to install the old experimental TLSRoute CRD separately. After the upgrade, the relevant CRDs looked like this:

TLSRoute:
  v1        served=true   storage=true
  v1alpha2  served=false  storage=false
  v1alpha3  served=false  storage=false

So the old alpha APIs are still visible as historical CRD versions, but they are no longer served. There are no TLSRoute objects in the cluster, so there was nothing to migrate there either.

Cilium 1.20.1

The actual upgrade was pleasantly boring:

helm upgrade cilium cilium/cilium \
  -n kube-system \
  --version 1.20.1 \
  -f cilium-values.yaml \
  --atomic

And:

Release "cilium" has been upgraded. Happy Helming!

That’s always a nice message to see when you’re upgrading the networking layer of a Kubernetes cluster. The post-upgrade status was clean:

Cilium:             OK
Operator:           OK
Envoy DaemonSet:    OK

cilium              15/15 Ready
cilium-envoy        15/15 Ready
cilium-operator      2/2 Ready

All 15 Cilium agents, all 15 Envoy instances and both Cilium operators came back healthy. So far, so good.

And then the interesting part

The original problem was that the Gateway remained:

PROGRAMMED=False
ADDRESS=

even though the Envoy listener itself was working. After upgrading to Cilium 1.20.1 and Gateway API 1.6.1:

NAME       CLASS    ADDRESS          PROGRAMMED
internet   cilium   89.117.146.204   True

There it is.

PROGRAMMED=True.

And, for the first time, the Gateway also had an address. The GatewayClass was healthy as well:

NAME     CONTROLLER                     ACCEPTED
cilium   io.cilium/gateway-controller   True

So this was no longer just a case of “traffic happens to work even though the status is wrong”. The Gateway controller was actually happy with the Gateway.

Let’s test some real traffic

The existing test route was:

spansh-proxy.iotguru.dev

A direct request reached Envoy:

HTTP/1.1 404 Not Found
server: envoy
x-envoy-upstream-service-time: 24

The 404 wasn’t particularly interesting, / simply isn’t a valid endpoint in that application. The interesting part was that Envoy had reached the upstream at all. And the actual web application using this Gateway was tested afterwards. Its health check went green. So the complete path was working:

Internet
   ↓
Cilium Gateway
   ↓
Envoy
   ↓
HTTPRoute
   ↓
Kubernetes Service
   ↓
Application

No special TLSRoute workaround. No broken Gateway status. No CrashLooping Cilium operator. Just… working.

What about the old workaround?

This was actually the bit I was most curious about. The original setup needed:

Gateway API 1.4.1 Standard
+
TLSRoute v1alpha2 Experimental

because Cilium 1.19.5 expected the old API.

With Gateway API 1.6.1 and Cilium 1.20.1, TLSRoute is now simply:

gateway.networking.k8s.io/v1

The old alpha versions are no longer served. There is still some historical storedVersions metadata on the TLSRoute CRD:

["v1alpha3","v1"]

but that is just Kubernetes CRD history. There are no actual TLSRoute resources using the old API. So, functionally, the workaround is gone.

The other workaround stays

One thing from the original post is still required. Because I’m running the Gateway on host-networked nodes, Envoy needs the capability to bind to privileged ports:

envoy:
  securityContext:
    capabilities:
      keepCapNetBindService: true
      envoy:
        - NET_ADMIN
        - SYS_ADMIN
        - NET_BIND_SERVICE

And:

gatewayAPI:
  enabled: true

  hostNetwork:
    enabled: true
    nodes:
      matchLabels:
        ingress: gateway

That wasn’t a Cilium 1.19 workaround. It’s simply part of the way this particular deployment is configured.

Final state

So the cluster is now running:

Kubernetes       1.33.13
Cilium           1.20.1
Gateway API      1.6.1
TLSRoute         v1

And the important bits are:

Cilium agents             15/15 Ready
Cilium Envoy              15/15 Ready
Cilium operators           2/2 Ready

GatewayClass              Accepted=True
Gateway                   Programmed=True
Gateway address           89.117.146.204

HTTPRoute                  working
Application health check   green

So, was it worth waiting?

Yes.

The original post ended with the hope that Cilium 1.20 would make the Gateway API situation considerably less awkward. It did. The migration from Cilium 1.19.5 to 1.20.1 was surprisingly uneventful. The existing Helm configuration could be reused as-is, the upgrade was done with --atomic, and after the upgrade the Gateway API 1.6.1 CRDs could be used without the old experimental TLSRoute workaround. Most importantly, the thing that had been sitting at:

PROGRAMMED=False

for the last couple of months is now:

PROGRAMMED=True

And the application is actually working. So I think I can finally stop writing about getting the Gateway API to work and start writing about actually using it. Which is probably how this should have started in the first place.

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top