Skip to main content

Cilium 1.20 Puts Tenant Auth in the CNI: What ExternalAuth Means for Fleets Still on ingress-nginx

9 min readDora NodaDora Noda
Share
On this page

Your ingress controller has had no security patches for six months. The Kubernetes project announced the retirement of ingress-nginx in November 2025, maintenance ended in March 2026, and since then every CVE in that codebase has been yours to absorb.

If you run a self-hosted fleet, you already know the migration target is Gateway API. But if your tenant routes authenticate through auth_request annotations and a sidecar auth proxy, you have likely stalled on the same gap every auth-heavy fleet hits: until this week, the CNI most fleets already run had no first-class answer for that pattern.

On September 14, Cilium 1.20 removed that rock. The headline feature is the ExternalAuth filter (GEP-1494): per-route external authorization declared on the HTTPRoute itself, enforced by the gateway your CNI already operates, before a single byte reaches the application. This post gives you the concrete before/after — what moves, what disappears, and what the migration actually costs — so you can decide whether 1.20 is the release that unblocks your own move off the retired controller.

What 1.20 actually ships

Cilium 1.20 is the second major Cilium release of 2026 after 1.19, and its traffic-management theme is simple: more of your north-south story moves into Gateway API. The jump from Gateway API v1.4 to v1.6 brings:

FeatureWhat it does for a fleet
ExternalAuth filter (GEP-1494)Call an external authorizer from any HTTPRoute rule before forwarding
CORS filtersCross-origin policy as route config, not app middleware
ListenerSetsShare listeners across Gateways without duplicating port/TLS config
TCPRoute / UDPRouteDatabases, DNS, queues, and custom protocols through the same declarative API
cilium-cni 76 MB → 16 MBFaster node bootstrap for autoscaled and frequently rotated nodes

TCPRoute and UDPRoute matter more than they look: previously, exposing anything that wasn't HTTP meant dropping down to LoadBalancer or NodePort resources with provider-specific annotations. Now a Postgres port or an internal DNS endpoint is a route like any other. But the feature that changes fleet architecture — and the one this post is about — is ExternalAuth.

The semantics fit in one sentence: you attach a filter to an HTTPRoute rule, and for every matching request the gateway asks an external authorization service what to do before forwarding anything. Unauthenticated browser? A 302 to your login page. A script with no credentials? 401 or 403. A valid caller? The authorizer returns 200 OK, optionally injects identity headers, and the request proceeds to your backend.

Before/after: where tenant auth lives

Take one protected tenant route and hold it still while everything around it changes. That is the fastest way to see what 1.20 actually buys you.

Before: auth scattered across annotations and sidecars. The standard ingress-nginx pattern for a protected tenant service is the auth_request annotation pair pointing at an auth proxy, plus the proxy itself running somewhere in the cluster:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tenant-dashboard
  annotations:
    nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.auth.svc.cluster.local/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "https://dashboard.example.com/oauth2/start"
spec:
  ingressClassName: nginx
  rules:
    - host: dashboard.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: dashboard
                port:
                  number: 80

Count the moving parts the platform team owns: the annotations (nginx-specific, unportable, validated only at reload), the oauth2-proxy deployment (sized, upgraded, and monitored separately), and the implicit contract between them (a renamed auth service breaks every Ingress that references it, with no status condition to tell you). Multiply by one protected route per tenant and you get the real cost: auth policy smeared across N Ingress objects in a dialect only one retired controller speaks. On Cilium fleets that had already moved to Gateway API, the situation was barely better — the announcement is explicit that authenticating north-south callers previously required customizing Cilium's built-in Envoy proxy by hand.

After: auth as route config. The same protection under Cilium 1.20 is a filter block on the HTTPRoute, using the example shape from the release announcement:

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: hr-dashboard
spec:
  parentRefs:
    - name: acme-gateway
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      filters:
        - type: ExternalAuth
          externalAuth:
            protocol: HTTP
            backendRef:
              name: authelia
              port: 80
            http:
              path: /api/authz/ext-authz
              allowedHeaders:
                - cookie
              allowedResponseHeaders:
                - Remote-User
                - Remote-Email
                - Remote-Name
                - Remote-Groups
      backendRefs:
        - name: dashboard
          port: 80

Three things changed structurally. First, the authorizer is a backendRef — a typed reference to a Service, resolved and status-reported by the API, not a URL string inside an annotation. Second, identity flows back as headers (Remote-User, Remote-Groups), so the application makes its final authorization call without ever touching a password or token. Third, the filter speaks Envoy's ext_authz protocol with either HTTP or gRPC, which means existing authorizers — Authelia, oauth2-proxy against Keycloak, Duo, or Okta — slot in without modification.

One precision note, because the framing invites it: the authorization check itself runs in Cilium's Envoy-based gateway, not in the kernel eBPF datapath. What moved "into the CNI" is the ownership — the same Cilium control plane that already manages your networking now manages the auth enforcement point, configured through the same Gateway API you use for routing, instead of a bespoke proxy you bolt on beside it.

One filter, three caller types. The announcement deliberately names browsers, CI jobs, and AI agents as callers of the same gateway, and that grouping is the tell for where platform teams actually hurt in 2026. A human gets the 302 to SSO and returns with a session cookie. A CI job presents a JWT that oauth2-proxy validates. An AI agent talking MCP authenticates with a service-account client and no interactive login at all. Previously each of those tended to grow its own enforcement path; now all three are filter attachments on routes.

This isn't hypothetical consolidation — it's already happening in real GitOps repos. The devantler-tech/platform project tracked replacing its Traefik auth-proxy with the native ExternalAuth filter as blocked-on-1.20 work, then bumped to the 1.20 pre-release to delete the proxy. That is the exact shape of the win: a whole component removed from the fleet because the CNI absorbed its job.

What this removes from your fleet

Put in subtraction terms, for a fleet running one protected route per tenant:

  • One auth proxy hop per tenant service. The standalone proxy deployment (or per-namespace sidecars) collapses into filter blocks on routes you already manage. There is less to size, upgrade, and page on.
  • Auth policy as annotations. NGINX annotation dialects are validated late, documented per-controller, and now frozen forever on a retired project. Route filters are typed API fields with status conditions.
  • Custom Envoy patching inside Cilium. If you had already moved to Cilium Gateway API and hand-customized its Envoy for auth, that fork-in-spirit goes away; the supported path is now the filter.

And the honest remainder: you still run the authorizer. ExternalAuth wires an authorization decision into the route — it does not make the decision. Authelia, oauth2-proxy, your IdP: all still yours to operate. What 1.20 deletes is the glue between the gateway and the authorizer, not the authorizer itself. Anyone telling you the upgrade "adds authentication" without that caveat is selling something.

The migration bill

None of the above is free. If your tenant routes still live on ingress-nginx annotations, here is the itemized cost of collecting the win:

  1. Every annotation must map to Gateway API config. The mechanical rewrites (hosts, paths, TLS) are handled by the community ingress2gateway conversion tool, and Datadog, AWS, and VMware have all published migration guides this year. But there is no 1:1 mapping for exotic rewrites, custom auth snippets, and regex-path tricks accumulated over years of annotation sprawl. Budget the audit: enumerate every non-trivial annotation per tenant route before you promise a date.
  2. ExternalAuth is Experimental channel. GEP-1494 merged as implementable and Gateway API 1.4 defined the filter as Experimental — Cilium 1.20 (via PR #45739, merged in May against cilium-envoy v1.37) is an early implementation. Experimental means the CRD channel to install, the upgrade-risk disclaimer to accept, and the conformance-test gap to watch. Don't put it in front of your most sensitive tenant on day one; roll it out per route.
  3. Roll out per route, not per fleet. The filter attaches to individual HTTPRoute rules, so the safe sequence is: stand up the Cilium Gateway alongside ingress-nginx, move one low-risk tenant route with its ExternalAuth filter, verify the authorizer's 302/401/200 behavior end to end, then proceed tenant by tenant. The retired controller stays until the last route moves — which is exactly why starting now beats starting under CVE pressure.
  4. Sweep the non-HTTP stragglers with TCPRoute. While you're touching every route, the tenants exposing databases or DNS over LoadBalancer/NodePort escapes can come into the same API. It's the same migration window, so spend it once.

The clock is the fifth cost. VMware's migration guide put the forcing function bluntly — "The ingress-nginx project is archived, no more security patches… Do it now or do it under pressure when the first post-retirement CVE drops." Six months past end-of-maintenance, that reads less like advice and more like a status report. Isovalent's own guidance names this release the moment for holdouts to act: "If you are still running Ingress NGINX, now is the time to let the CNI you already run take on that traffic management too."

The verdict for self-hosted fleets

For a Cluster-API-managed fleet on owned machines, the math of this release is unusually clean. If Cilium is already your CNI, adopting Gateway API is no longer a new-component decision — no separate ingress controller to install, configure, and upgrade. It is a configuration migration: convert routes, attach filters, retire annotations. The auth story that used to justify keeping a second traffic stack (annotations for auth, a proxy for the check) is now a filter block on a route the CNI serves.

That makes Cilium 1.20 the right kind of forcing function paired with the wrong kind: ingress-nginx rotting unpatched is the stick, and a genuinely simpler auth architecture is the carrot. Start with one tenant route this week. Your future self, debugging at 2 AM during a post-retirement CVE, will thank you.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex