Moving from nginx-gateway-fabric to envoy-gateway

I used to run the (kubernetes-run) ingress-nginx. When they announced they’d stop updating it, I knew I wanted to move to the Gateway API/HTTPRoute method, because that seems like the future. I originally switched to nginx-gateway-fabric. That is controlled by NGINX/F5 though, and they want money, so they’re putting features I want behind a very expensive paywall. You know who doesn’t paywall features and also supports Gateway API v1.5.1 (or newer)? envoy-gateway.

History / Requirements

When ingress-nginx announced they weren’t going to be updating it after March of 2026, I started looking into GatewayAPI, knowing I wanted to jump to HTTPRoute. I tried moving over to nginx-gateway-fabric at one point, but I quickly found out a Gateway only supports 16 Listeners. This was way too little, and being a homelab, I only have one public IP to point to for all my services. Thankfully, I saw that ListenerSets were being added (or already had been) in v1.5.1 of the Gatewy API, but when looking, most implementations hadn’t implemented that version yet. So I ended up waiting.

Eventually nginx-gateway-fabric released a version with support for ListenerSets, so I made the move. While I implemented it successfully, I learned a couple things:

apiVersion: gateway.nginx.org/v1alpha1
kind: ClientSettingsPolicy
metadata:
  name: client-settings
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: myhttproute
  body:
    maxSize: "10g"
apiVersion: gateway.nginx.org/v1alpha1
kind: AuthenticationFilter
metadata:
  name: basic-auth
spec:
  type: Basic
  basic:
    secretRef:
      name: basic-auth
    realm: "Basic auth"

Then you attach that AuthenticationFilter to an HTTPRoute

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: mysite
spec:
  hostnames:
  - mysite.joyrex.net
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: ListenerSet
    name: my-ls
    sectionName: mysite
  rules:
  - backendRefs:
    - kind: Service
      name: my-service
      port: 80
    matches:
    - path:
        type: PathPrefix
        value: /
    filters:
    - type: ExtensionRef
      extensionRef:
        group: gateway.nginx.org
        kind: AuthenticationFilter
        name: basic-auth

Learned Requirements

Envoy Gateway

Looking around, I found envoy-gateway was another popular gateway server, supported my requirements (I thought, more later) and had good documentation.

Thankfully, gateway and HTTPProxy are core stuff not unique to nginx’s implementation, so there wasn’t much I needed to change for my gateway or applications. I just had to update the gateway name I was using and make some of the other changes. Those are talked about below. Before I did that, I disabled all the HTTPRoutes that use BasicAuth so I didn’t accidentally put stuff on the public web until I got OIDC working.

Dual-Stack

I had dual-stack (IPv4/IPv6) going with nginx and I wanted the same for this gateway. To do that, I have to use an envoy-specific manifest to make sure the Service is correctly set up:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: config
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyDeployment:
        replicas: 2
      envoyService:
        annotations:
          metallb.io/loadBalancerIPs: 192.168.1.209,2400:a842:40ae::209
  ipFamily: DualStack

The ipFamily: DualStack is the important part here, and then I use the annotations to set the IPs I want via MetalLB.

For this to be used, you attach it to the gateway’s spec:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: klopjob-gateway
spec:
  ...
  infrastructure:
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: config
  ...

OIDC Support

Using the Envoy Gateway documentation about OIDC and the Authelia Envoy Gateway documentation, I was able to get OIDC working fine. For reference, here’s an example entry for something I want to have behind OIDC.

Authelia OIDC Client Config
- client_id: 'clientid'
  client_name: 'mysite.joyrex.net'
  client_secret: 
    path: "/secrets/authelia-secrets/oidc.client.mysite-joyrex-net.value"
  consent_mode: "implicit"
  public: false
  authorization_policy: 'two_factor'
  require_pkce: false
  pkce_challenge_method: ''
  redirect_uris:
    - 'https://mysite.joyrex.net/authelia/openid_connect/callback'
  scopes:
    - 'openid'
  grant_types:
    - 'authorization_code'
  response_types:
    - 'code'
  access_token_signed_response_alg: 'none'
  userinfo_signed_response_alg: 'none'
  token_endpoint_auth_method: 'client_secret_basic'
Security Policy
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: mysite-joyrex-net-oidc
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: mysite
  oidc:
    provider:
      issuer: 'https://auth.joyrex.net'
      authorizationEndpoint: 'https://auth.joyrex.net/api/oidc/authorization'
      tokenEndpoint: 'https://auth.joyrex.net/api/oidc/token'
    clientID: "clientid"
    clientSecret:
      name: "mysite-joyrex-net-oidc-secret"
    cookieDomain: 'mysite.joyrex.net'
    cookieNames:
      idToken: ''
      accessToken: ''
    scopes:
      - 'openid'
    redirectURL: "https://mysite.joyrex.net/authelia/openid_connect/callback"
    forwardAccessToken: false
    refreshToken: false
    passThroughAuthHeader: false
HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: mysite
spec:
  hostnames:
  - mysite.joyrex.net
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: ListenerSet
    name: my-ls
    sectionName: mysite
  rules:
  - backendRefs:
    - kind: Service
      name: mysite
      port: 8080
    matches:
    - path:
        type: PathPrefix
        value: /
    filters:
    - type: RequestHeaderModifier
      requestHeaderModifier:
        add:
        - name: x-real-ip
          value: '%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%'
  - matches:
    - path:
        type: PathPrefix
        value: /authelia/openid_connect/callback
    filters:
    - type: RequestRedirect
      requestRedirect:
        path:
          type: ReplaceFullPath
          replaceFullPath: /
        statusCode: 302

The x-real-ip setting is something that I’ll be describing right now.

X-Real-Ip

The X-Forwarded-For header is already set, but X-Real-Ip doesn’t get set by default. Anubis looks for this value to verify stuff is configured properly, so I wanted this attached to all my requests.

I’m supposed to be able to attach this to a gateway as a whole, but that didn’t work for me. I was tired of debugging and learning, so for right now I just attached the filter to all the httproutes I wanted it on.

    filters:
    - type: RequestHeaderModifier
      requestHeaderModifier:
        add:
        - name: x-real-ip
          value: '%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%'

This is shown in place in the full httproute above.

Body Size

This is where I hit issues. I just assumed setting body size would be common enough, but it’s not. According to this ticket and this question page, the way you increase the body size is to enable requestBuffering. This has the following downsides:

That said, to set requestBuffering, you need to create a BackendPolicy that attaches to one or more HTTPRoutes.

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: request-buffer
spec:
  targetRefs:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: mysite
  requestBuffer:
    limit: 4294967295

Conclusion

Besides the requestBuffer issue, and the minor X-Real-IP header setting not applying to the Gateway as a whole, I am extremely happy with Envoy. Leaving nginx-gateway-fabric behind is great, and I am stoked I have OIDC back.