Cilium 1.20 ExternalAuth vs oauth2-proxy vs Istio AuthorizationPolicy
For years, the honest answer to “how do I authenticate users at the edge of my Kubernetes cluster without writing auth into every service” was a shrug and a sidecar. Cilium 1.20 ExternalAuth changes that: the gateway itself can now call an external authorizer before a request ever reaches your application, using a filter declared inline on an HTTPRoute. That is a genuine capability gain, and the vendor coverage is correct about what it does. It is also incomplete in two ways that matter more than the feature itself — the upstream API this filter implements is still an Experimental proposal, and Cilium’s own documentation does not yet carry a page for it. Both facts change the adoption calculus, and neither appears in the release notes.
What this covers: how the filter works at the request level, how it differs mechanically from a per-app oauth2-proxy and from Istio’s in-mesh policy pair, a decision matrix across six axes, what ListenerSets do to policy ownership, and a straight answer on whether to put this in production this quarter.
Context and Background
Cilium 1.20.0 was tagged on 29 July 2026, carrying more than 2,660 commits from a community of over 1,100 contributors. The headline networking work is elsewhere — datapath plugins, IPv6 for AWS ENI IPAM, a cilium-cni binary that shrank from roughly 77 MB to 16 MB. For anyone running a shared ingress, though, the Gateway API section is the release. Cilium moved from Gateway API v1.4 straight to v1.6.1, skipping a whole minor version of upstream work and absorbing everything that graduated in between.
That jump matters because of what landed upstream in v1.5 and v1.6. TLSRoute, the HTTPRoute CORS filter and ListenerSets all reached the Standard channel. The layer-4 route types graduated too, which the site covered separately in the Gateway API 1.6 L4 routing analysis. Gateway-level and HTTPRoute-level authentication, by contrast, landed as an experimental feature — and that is the one Cilium implemented.
The gap it fills is real. Cilium already had strong controls for east-west traffic: identity-based network policy, transparent encryption, and now ztunnel-based mutual TLS with either an internal certificate authority or SPIRE-issued workload identities. North-south was the hole. Authenticating a human in a browser, a CI job presenting a JWT, or an agent speaking MCP meant either building auth into each service or hand-writing CiliumEnvoyConfig resources to wire up Envoy’s ext_authz filter yourself. Neither scales across teams.
The incumbents each solved this differently. oauth2-proxy — now at v7.15.4, released 20 August 2026, with around 15,000 GitHub stars — puts a reverse proxy in front of each application and speaks OIDC to an identity provider. Istio splits the job across RequestAuthentication for JWT validation and AuthorizationPolicy for the decision, with a CUSTOM action that delegates to an external authorizer. Ingress NGINX had annotations. Every one of these is vendor-shaped. What GEP-1494 proposes, and what Cilium 1.20 ExternalAuth implements, is the first portable spelling of the same idea.
How the Cilium 1.20 ExternalAuth filter actually works
The ExternalAuth filter attaches to an HTTPRoute rule. For every request matching that rule, the gateway pauses the request, sends a check to an external authorization backend, and acts on the response code: 200 forwards the request to the backend, 302 redirects the caller (typically to an SSO login page), and 401 or 403 blocks the request at the gateway. The application never sees an unauthenticated request.

Figure 1: The three-way branch that defines the filter’s behaviour.
The diagram traces one request through the gateway. The client hits a protected path; the gateway forwards a check request containing a fixed header set plus whatever the route allows; the authorizer replies. On 200 the gateway copies permitted response headers onto the upstream request and forwards it. On 302 it returns the authorizer’s redirect to the client, which sends a browser off to the identity provider. On 401 or 403 the request terminates at the gateway. The backend pod is never contacted in the second and third cases, which is the entire point: unauthenticated traffic costs you one proxy hop, not a pod wake-up.
The filter is inline on the HTTPRoute, not a separate CRD
This is the single most important design decision in GEP-1494, and it is easy to miss. The configuration lives in the route object itself:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: hr-dashboard
spec:
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: hr-dashboard
port: 8080
There is no new CRD to install, no policy object to discover, and no cross-namespace attachment to reason about. The GEP is explicit about why: filters are ordered lists, ordering matters enormously for auth (header modification often has to run before an auth check), and the tightest possible scope for the config is the HTTPRoute rule. A Policy object can be targeted at a route rule with sectionName, but Policy brings discovery problems — you cannot tell from the route alone whether a policy applies to it.
The GEP defines a two-phase plan: Filter first, Policy second. Phase 2 is documented as “currently undefined until we reach agreement on the Filter + Policy approach.” Remember that when you read the section on adoption risk below.
What the authorizer sees, and what it can inject
The protocol field takes HTTP or GRPC. With GRPC, the backend must speak Envoy’s ext_authz protobuf API on the referenced port. With HTTP, the backend must return 200 for a successful authorization; anything else is an authorization failure.
The header semantics are specified more tightly than most implementations bother with, and they are worth knowing before you debug a check that silently loses a cookie. For the HTTP protocol, five headers are always forwarded to the authorizer regardless of configuration: Host, Method, Path, Content-Length and Authorization. Anything else — a session cookie, a custom tenant header — must be named in http.allowedHeaders. For the gRPC protocol the default set is different: when grpc.allowedHeaders is empty, Authorization, Location, Proxy-Authenticate, Set-Cookie and WWW-Authenticate are sent.
Content-Length has a subtlety that will bite anyone forwarding request bodies. The GEP requires the value sent to the authorizer to reflect the actual bytes forwarded. If forwardBody is unset or forwardBody.maxSize is 0, Content-Length must be 0 — not the client’s original value. Bodies larger than maxSize must be truncated to maxSize, and implementations that buffer past the limit are expected to reject with a 4xx, commonly 413 or 403.
Going the other way, http.allowedResponseHeaders controls what the authorizer can inject into the upstream request. If the list is empty, every header from the authorization response except Authority and Host is copied through. That default is generous, and on a shared gateway it is the wrong default — an authorizer compromise becomes header injection into every backend behind it. Name the headers explicitly. The Authelia example above does exactly that, copying four identity headers and nothing else.
Where the check executes in the Cilium datapath
Cilium’s Gateway API implementation is Envoy-backed. The ExternalAuth filter landed through cilium/cilium#45739 and compiles down to an Envoy ext_authz filter in the gateway’s listener chain, running in the Envoy instance Cilium manages rather than in eBPF. That is not a criticism — authorization is an L7 concern and eBPF is the wrong layer for parsing cookies — but it does mean the latency you add is one in-proxy filter plus one network round trip to the authorizer service, and the authorizer is reached over the normal service path.
Two related 1.20 changes are relevant here. BackendTLSPolicy support (cilium/cilium#43045) lets you terminate TLS to the authorizer with certificate validation, which you want for any authorizer handling credentials. And new ADS and Delta xDS modes for Envoy configuration delivery reduce CPU and policy-update latency, which matters when a filter change has to propagate to every gateway pod before it takes effect.
What the extra hop actually costs
The arithmetic is worth doing before you argue about it. On the allow path, a request that previously went client → gateway → backend now goes client → gateway → authorizer → gateway → backend. You have added one request-response exchange over the pod network, plus the authorizer’s own decision time.
The network part is cheap and predictable. Envoy holds a connection pool to the authorizer, so in steady state you are not paying TCP or TLS handshake cost per request — an intra-cluster round trip on a healthy node network sits in the low single-digit milliseconds. The decision time is the variable you actually control, and it is dominated by whether the authorizer can answer from local state. An authorizer validating a session cookie against an in-process or Redis-backed store answers in sub-millisecond to low-millisecond time. An authorizer that calls out to an identity provider’s introspection endpoint on every request does not, and will add tens to hundreds of milliseconds while coupling your data path to a third party’s availability.
That distinction, not the filter itself, is what determines whether this design is viable at your request rate. Two practical consequences follow. First, prefer authorizers that cache validation results and validate JWTs locally against a cached JWKS rather than calling the IdP per request. Second, size the authorizer for gateway-aggregate traffic, not per-app traffic — it is now in the path of every protected route in the cluster, and its CPU profile is request-rate-driven rather than payload-driven.
Also note what the filter does not add. Denied requests never reach the backend, so a credential-stuffing burst costs you gateway plus authorizer capacity but leaves application pods idle. Compared with a per-app proxy sitting in front of an application that still has to be scheduled and warm, that is a real capacity argument for the shared design under hostile traffic.
Three designs for the same problem
Ask the question narrowly — “where does the identity check execute, and how many hops does it cost?” — and the three designs separate cleanly.

Figure 2: The same request, three placements of the identity check.
Each row in the figure shows a client, the component that makes the authorization decision, and the application. What changes between rows is where the decision executes, who owns the object that configures it, and how many network hops the request costs when the answer is “allowed”.
Gateway-embedded ext_authz
In the Cilium 1.20 ExternalAuth design, the check runs in the gateway’s Envoy instance. One gateway serves every route, so one authorizer deployment can serve every protected application in the cluster. The allowed path costs one extra round trip from gateway to authorizer; the denied path costs the same round trip and never touches the backend.
The configuration object is the HTTPRoute, which in most clusters is owned by the application team. That is a meaningful shift: the team that owns the route owns the auth policy for it, with no platform ticket in the loop.
Per-app oauth2-proxy
oauth2-proxy runs in one of two shapes. In reverse-proxy mode it sits in the request path, terminates the OIDC flow itself, maintains a session (cookie-backed or Redis-backed), and forwards to an upstream. In auth-request mode it exposes a /oauth2/auth endpoint that an upstream proxy calls as a subrequest — the pattern Ingress NGINX users know through auth_request.
Either way it is an application-scoped deployment listening on port 4180 by default. With --set-xauthrequest it emits X-Auth-Request-User, X-Auth-Request-Email, X-Auth-Request-Groups and X-Auth-Request-Preferred-Username; add --pass-access-token and you also get X-Auth-Request-Access-Token.
The strength is isolation. Each application gets its own proxy, its own cookie secret, its own session store, its own blast radius. The cost is multiplicity — N applications means N deployments, N sets of flags, N client registrations at the identity provider, and N things to patch when a CVE lands. It also means the auth hop is inside the app’s own path, so the application team can break it independently of anyone else, for better and for worse.
Istio in-mesh AuthorizationPolicy
Istio splits authentication from authorization. RequestAuthentication validates JWTs against a JWKS endpoint. AuthorizationPolicy decides what an authenticated principal may do. For delegation to an external service, an AuthorizationPolicy with action: CUSTOM names a provider that must be pre-registered in the mesh configuration:
data:
mesh: |-
extensionProviders:
- name: "oauth2-proxy"
envoyExtAuthzHttp:
service: "oauth2-proxy.foo.svc.cluster.local"
port: "4180"
includeRequestHeadersInCheck: ["authorization", "cookie"]
headersToUpstreamOnAllow: ["authorization", "path", "x-auth-request-user",
"x-auth-request-email", "x-auth-request-access-token"]
headersToDownstreamOnAllow: ["set-cookie"]
headersToDownstreamOnDeny: ["content-type", "set-cookie"]
Two things follow from that snippet. First, the header controls are richer than GEP-1494’s: Istio separates headers sent upstream on allow, headers sent downstream on allow, and headers sent downstream on deny, whereas the GEP Phase 1 filter only has an upstream-on-allow list. Second, and more consequential, the provider registry lives in the istio ConfigMap in istio-system. Registering an authorizer is a cluster-admin operation. The namespaced AuthorizationPolicy can only reference a provider that a platform admin has already blessed.
There is a well-known trap here that catches teams every release cycle: RequestAuthentication alone does not reject a request that carries no token at all. A tokenless request is accepted — it simply has no authenticated identity. Enforcing authentication requires a paired AuthorizationPolicy, typically an ALLOW rule requiring requestPrincipals: ["*"] or a DENY rule on notRequestPrincipals: ["*"]. Ship RequestAuthentication on its own and you have configured a validator, not a gate.
The upside of the mesh design is reach. The same policy language covers east-west calls, not just traffic entering at the edge, and it composes with mTLS identity. If you need service-to-service authorization as well as user authentication, this is the only one of the three that gives you both in one model. The Cilium service mesh versus Istio ambient mesh decision record works through the broader data-plane trade-off.
Session state: the axis nobody puts in the matrix
Comparisons of these three designs almost always stop at “where does the check run”. The more consequential question for an operator is where the session lives, because that determines what a restart, a scale-out or a node drain does to logged-in users.
In the gateway-filter design the gateway is deliberately stateless about identity. It forwards a cookie to the authorizer and acts on a status code. All session state belongs to the authorizer, which means the gateway can be restarted, rescheduled or scaled without logging anybody out — and it also means the authorizer’s session store is now a stateful component you must plan for. If it is an in-memory store on a single replica, scaling the authorizer to two replicas will randomly invalidate half your sessions unless the store is shared or the cookie is self-contained.
In the oauth2-proxy design the session is the proxy’s own, and its durability depends on configuration. A cookie-backed session survives proxy restarts because the state is in the client’s cookie; a Redis-backed session survives restarts because the state is external; an unshared in-memory arrangement survives neither. The proxy also needs a stable cookie secret across replicas, which is a surprisingly common cause of intermittent logouts during rolling updates.
In the Istio design with RequestAuthentication, there is no session at all — the caller presents a JWT on every request and the proxy validates it against a cached JWKS. That is the most operationally robust of the three, and also the least usable by a human in a browser, which is precisely why so many Istio deployments end up running oauth2-proxy as a CUSTOM provider anyway. The mesh handles machine callers; something else handles the login flow.
If you are migrating from Ingress NGINX auth-url and auth-signin annotations, the mapping is direct and the session behaviour is the one thing that does not change: auth-url becomes backendRef plus http.path, auth-response-headers becomes allowedResponseHeaders, and the redirect that auth-signin produced is now the authorizer’s own 302. The session still lives wherever it lived before.
The decision matrix
| Axis | Cilium 1.20 ExternalAuth | oauth2-proxy per app | Istio AuthorizationPolicy |
|---|---|---|---|
| Where the check runs | Gateway Envoy, shared across all routes | In the app’s own path, one deployment per app | Sidecar or waypoint, or ingress gateway |
| Extra hops on allow | 1 round trip gateway to authorizer | 0 extra if in-path; 1 subrequest in auth-request mode | 1 round trip proxy to authorizer under CUSTOM |
| Header injection | allowedResponseHeaders; empty list copies everything except Authority and Host |
X-Auth-Request-* family, flag-controlled |
Separate upstream-on-allow, downstream-on-allow and downstream-on-deny lists |
| Who owns the config | Application team, inline on HTTPRoute | Application team, owns the whole deployment | Split: platform owns the provider registry, app owns the policy |
| Authorizer down | Not specified by the API; no failure-mode field exists in Phase 1 | Blast radius of one application | Status returned on error is configurable in the extension provider |
| API stability | Experimental GEP; Extended support level | Stable v7 line; alpha config format explicitly unstable | Stable CRDs; provider config is mesh-config, not a CRD |
Three cells deserve expanding.
The failure-mode row is the most uncomfortable. The GEP-1494 Phase 1 filter struct has fields for protocol, backend reference, per-protocol header configuration and body forwarding. It has no field for what to do when the authorizer is unreachable. Envoy’s ext_authz filter denies by default when the check cannot complete, so fail-closed is the behaviour you should plan for — but that is an inference from the data plane, not a guarantee from the API, and a future implementation could reasonably differ. If your risk register needs a documented answer, you do not have one yet.
The ownership row is the real differentiator, more than latency. In the Cilium design a team can add authentication to their own route in a pull request against their own manifests. In the Istio design they cannot introduce a new authorizer without a platform change to a mesh-wide ConfigMap. Whether that is a feature or a bug depends entirely on whether you trust application teams to choose authorizers.
The header-injection row hides a security decision. GEP-1494’s default — copy everything except Authority and Host — is the permissive one. An authorizer that has been compromised, or simply one that echoes attacker-controlled headers, can then set arbitrary headers on the upstream request. If your application trusts any header for identity, list allowedResponseHeaders explicitly and strip the same names from client requests at the gateway.
ListenerSets and who owns the auth policy
The ExternalAuth filter gives an application team a place to declare auth. ListenerSets give them a place to declare a listener. Together they make per-tenant authentication on a shared gateway tractable, which is why both landed in the same release.

Figure 3: Delegation chain from platform Gateway to tenant-owned route and filter.
The chain works top-down. A platform-owned Gateway opts into delegation with spec.allowedListeners, selecting namespaces by label. A tenant creates a ListenerSet in their own namespace with a parentRef back to the shared Gateway, defining listeners with their own hostnames, ports and TLS certificates. Routes then attach to the ListenerSet rather than the Gateway, and those routes carry the ExternalAuth filter.
Several behaviours in Cilium’s ListenerSet documentation are worth committing to memory before you design around it:
- By default a Gateway accepts no ListenerSets.
allowedListenersis opt-in, not opt-out. - A Route’s
parentRefmust explicitly setkind: ListenerSet. Omit the kind and it defaults toGateway, so the route silently attaches to nothing in the ListenerSet and you get a route that appears healthy but is never reached. - Listeners defined directly on the Gateway take precedence over conflicting ListenerSet listeners. Among ListenerSets, the older one wins, and the conflict is reported on the lower-precedence listener — so the tenant who arrives second sees the error, not the tenant who caused it.
- Certificate references are evaluated from the ListenerSet’s namespace. A cross-namespace Secret needs a
ReferenceGrantfor the ListenerSet; grants made to the parent Gateway are not inherited. - The parent Gateway must keep at least one valid listener of its own. A perfectly valid ListenerSet will not rescue an otherwise invalid Gateway.
Operationally, ListenerSet support is not behind a Cilium feature flag, but the CRD must be installed before the Cilium operator starts so support is detected at startup. Add the CRD to a running cluster and you must restart the operator. Status surfaces in two places: status.attachedListenerSets on the Gateway, and status.listeners on the ListenerSet, where you check the Accepted, Programmed, ResolvedRefs and Conflicted conditions per listener. A ListenerSet with a mix of valid and invalid listeners can still be Accepted at the top level, so never trust the top-level condition alone.
This is the combination that makes the feature strategically interesting. A single platform Gateway, per-tenant listeners with per-tenant certificates, and per-route authentication that each tenant configures against their own identity provider — without the platform team mediating every change. Neither half delivers that on its own.
Should you adopt Cilium 1.20 ExternalAuth now?
Here are the two facts the release coverage does not lead with.
GEP-1494 is Experimental, not Standard. The GEP page carries Status: Experimental against issue #1494. In Gateway API’s lifecycle, Experimental means the API shape can still change — fields can be renamed, semantics can be adjusted, and the resource lives in the experimental channel rather than the standard one. Concretely: the filter’s declared support level is Extended, meaning implementations are not required to support it at all, and the conformance features (HTTPRouteExtAuth, HTTPRouteExtAuthGRPC, HTTPRouteExtAuthHTTP, HTTPRouteExtAuthForwardBody) are separate flags an implementation may or may not claim. Body forwarding in particular is flagged separately in the GEP precisely because Envoy and Traefik support it while HAProxy and Ingress NGINX do not.
Worse for planning: Phase 2, the Policy object that would let a cluster admin set a default auth posture and have routes inherit it, is explicitly undefined. The GEP lists open design questions it has not answered — where a defaulted auth filter sits in a filter list, whether Policy should support overrides as well as defaults, whether Policy should be able to scope itself to specific matches. If your requirement is “all routes in this cluster are authenticated unless explicitly exempted”, the API cannot express that today. You would enforce it with admission control instead.
There is no dedicated Cilium documentation page for ExternalAuth. The v1.20 service-mesh documentation index lists Ingress, Gateway API Support, GAMMA, Ingress-to-Gateway migration, Istio integration, Mutual Authentication and L7-aware traffic management. ListenerSets got a full page with YAML, verification commands and an operational-considerations list. ExternalAuth did not. The available material is the release body, the CNCF write-up, and a vendor lab.
That is not a hypothetical problem. Missing documentation is where the unspecified behaviours live: what happens when the authorizer times out and what the timeout is, how the filter interacts with retries, whether the check re-runs on an internal redirect, what appears in Hubble when a request is denied at the filter, which metrics count denials. You will discover these by testing, and your findings will not be portable to the next Cilium minor version, because nothing published commits to them.
So: adopt it now for internal dashboards, staging environments, and anything where a bad day means an engineer cannot reach a Grafana instance. Treat it as a strong candidate for customer-facing paths only once you have run it for a quarter and written down the behaviours yourself.
Hedge the API risk cheaply. Keep the filter block in a templated overlay rather than hand-written into every route, so a field rename is one patch. Pin your Gateway API CRD channel deliberately and track the experimental channel separately from the standard one. And keep an authorizer that speaks plain Envoy ext_authz — Authelia, oauth2-proxy, Keycloak-fronted proxies all do — so that if the Gateway API spelling changes you are re-writing the route, not replacing the identity layer.
Trade-offs, gotchas, and what goes wrong

Figure 4: What breaks, and how widely, when the authorizer stops answering.
The shared gateway is a shared failure domain. With a per-app oauth2-proxy, an authorizer outage takes down one application, and users with live sessions may keep working until their cookie expires. With the gateway-embedded filter, every route that carries it fails at the same instant, because they all depend on the same authorizer deployment and the same Envoy filter chain. Run the authorizer with a PodDisruptionBudget and anti-affinity, and treat it as tier-0 infrastructure, because it now is.
Health checks and probes need a filter-free route. If the filter matches PathPrefix: /, it also matches /healthz. External uptime checks, synthetic monitors and anything else that expects an unauthenticated 200 will start seeing 401s or redirects. Because Phase 1 has no exemption mechanism, the fix is a separate HTTPRoute rule matching the probe paths with no ExternalAuth filter — and a more specific match, since rule ordering decides which one applies.
Empty allowedResponseHeaders is an injection surface. Covered above, but it is the mistake most likely to reach production, because the permissive default works perfectly in testing and fails silently as a security property.
Body forwarding costs buffering. forwardBody.maxSize makes the gateway buffer up to that many bytes before the check can complete, which converts a streaming upload into a buffered one and adds latency proportional to body size. Leave it unset unless the authorizer genuinely needs the body, and if it does, set maxSize as small as the policy allows.
Do not confuse this with east-west authentication. ExternalAuth is north-south only; GEP-1494 says so explicitly. Service-to-service identity inside the cluster is a different mechanism — ztunnel and SPIRE in Cilium’s case, covered in the SPIFFE and SPIRE workload identity architecture. Legacy Mutual Authentication is among the upgrade items flagged for 1.20, alongside Envoy Go extensions, Kafka-aware policies, the cilium.io/v2alpha1 CiliumNodeConfig API, the libnetwork integration and custom CNI configuration. Read the upgrade notes before you touch a cluster that uses any of them.
Two gateway-adjacent changes will surprise you. HTTPRoutes gained native CORS support in 1.20, and if you were previously running CORS handling inside oauth2-proxy or the application, you now have two places that can set the same headers. And CiliumGatewayClassConfig can now overwrite, append or preserve the HTTP Server response header, which is useful hardening but will change responses you may have fingerprinted in tests.
Practical recommendations
Start by deciding what you are optimising for. If the answer is “stop every team writing their own login flow”, the gateway filter is the right tool and the Experimental status is an acceptable risk on internal surfaces. If the answer is “isolate each application’s auth failure domain”, oauth2-proxy per app is still the safer engineering choice, and it will be for at least another release cycle. If you already run a mesh and need one policy language across north-south and east-west, Istio’s pair remains the only option that covers both.
If you go with Cilium 1.20 ExternalAuth, work through this order:
- Upgrade to 1.20 first and verify the Gateway API v1.6.1 CRDs land cleanly, before adding any auth. Install the ListenerSet CRD ahead of the operator if you plan to delegate.
- Stand up the authorizer with
BackendTLSPolicyso the check traffic is encrypted and the certificate is validated. - Protect one low-stakes route end to end. Confirm all three branches — allow, redirect, deny — with real requests, not just the allow path.
- Name every header explicitly in
allowedHeadersandallowedResponseHeaders. Never ship the empty-list default. - Add a filter-free rule for probe and health paths, more specific than the protected rule, and verify it from outside the cluster.
- Measure added latency at p50 and p99 before and after, on the allow path. The extra hop is small but it is not free, and you want the number before an incident.
- Write down the undocumented behaviours you observe — timeout, retry interaction, Hubble output on deny — and re-test them on every Cilium minor upgrade until a docs page exists.
- Keep the filter configuration templated so an upstream field rename is a single change.
Frequently Asked Questions
Is Cilium 1.20 ExternalAuth production-ready?
The Cilium implementation is in a stable 1.20 release, but the API it implements is not. GEP-1494 carries Experimental status, its declared support level is Extended rather than Core, and the Policy half of the design is undefined. Cilium also has no dedicated documentation page for the feature. For internal applications that is an acceptable risk. For customer-facing authentication, run it for a quarter in a lower environment and document the behaviours yourself first.
What happens if the external authorizer goes down?
The GEP-1494 Phase 1 filter has no field controlling failure behaviour, so the API does not answer this. Envoy’s ext_authz filter denies requests by default when a check cannot complete, so plan for fail-closed: every route carrying the filter stops serving. Because one authorizer typically backs every protected route on a shared gateway, that failure domain is cluster-wide. Run the authorizer as tier-0 infrastructure with a PodDisruptionBudget and anti-affinity.
Can I replace oauth2-proxy with the ExternalAuth filter?
Not exactly — you usually keep oauth2-proxy and change where it sits. It is one of the named integration targets, and it works well as the authorizer behind the filter, terminating the OIDC flow and returning identity headers. What changes is that one shared deployment can serve every protected route instead of one per application. You lose per-app isolation and gain a single place to configure and patch.
How does this differ from Istio’s AuthorizationPolicy?
Three ways. Placement: the gateway filter is north-south only, while Istio’s policies also cover service-to-service traffic. Ownership: the ExternalAuth filter is inline on an HTTPRoute the app team owns, whereas Istio’s external authorizers must be registered in a mesh-wide ConfigMap by a platform admin. Portability: GEP-1494 is a vendor-neutral proposal that multiple gateways will implement, while AuthorizationPolicy is Istio-specific.
Do I need ListenerSets to use ExternalAuth?
No. The filter works on any HTTPRoute attached to a normal Gateway. ListenerSets matter when several teams share one gateway and each needs its own hostname, its own TLS certificate and its own authentication policy. Together they let a platform team own the Gateway while tenants own their listeners, routes and auth configuration. Note that a Gateway accepts no ListenerSets until you set spec.allowedListeners.
Does the ExternalAuth filter work with gRPC backends?
The filter’s protocol field accepts GRPC, in which case the authorizer must implement Envoy’s ext_authz protobuf API on the referenced port. The default header set differs from the HTTP case: with an empty grpc.allowedHeaders, the gateway sends Authorization, Location, Proxy-Authenticate, Set-Cookie and WWW-Authenticate. Conformance treats HTTP and gRPC as separate feature flags, so confirm which one an implementation actually claims.
Further Reading
- Gateway API 1.6 layer-4 routing versus LoadBalancer Services — the other half of Cilium’s Gateway API v1.6 jump.
- Cilium service mesh versus Istio ambient mesh: an architecture decision record — the broader data-plane comparison behind this one.
- SPIFFE and SPIRE workload identity architecture for zero trust — the east-west identity layer this feature deliberately does not touch.
- Kubernetes Gateway API versus Ingress in 2026 — start here if you are still on Ingress annotations.
- GEP-1494: HTTP Auth in Gateway API — the primary source, including the Go structs quoted above.
- Cilium 1.20.0 release notes and the Cilium ListenerSet documentation.
By Riju — about
