Skip to content

How-to Guides

Scope

Task recipes for Envoy Gateway v1.9: install, upgrade, routing, TLS, rate limiting, authentication, Ingress-NGINX migration, standalone mode, debugging, and troubleshooting. Commands assume EG v1.9.1 and Gateway API v1.6. Version facts are in Reference. Background is in Explanation.

Install with Helm

Install the chart from Docker Hub's OCI registry. It bundles the Gateway API CRDs (experimental channel) and the EG CRDs.

helm install eg oci://docker.io/envoyproxy/gateway-helm \
  --version v1.9.1 -n envoy-gateway-system --create-namespace

kubectl wait --timeout=5m -n envoy-gateway-system \
  deployment/envoy-gateway --for=condition=Available

kubectl get pods -n envoy-gateway-system
kubectl get gatewayclass

Deploy the upstream quickstart (GatewayClass eg, Gateway, HTTPRoute, and a sample backend) to check the data path:

kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/v1.9.1/quickstart.yaml -n default

Install CRDs Separately

Use this when a platform tool or the cloud provider manages Gateway API CRDs, or when you want the standard channel only.

helm template eg oci://docker.io/envoyproxy/gateway-crds-helm \
  --version v1.9.1 \
  --set crds.gatewayAPI.enabled=true \
  --set crds.gatewayAPI.channel=standard \
  --set crds.envoyGateway.enabled=true \
  | kubectl apply --server-side -f -

helm install eg oci://docker.io/envoyproxy/gateway-helm \
  --version v1.9.1 -n envoy-gateway-system --create-namespace \
  --set crds.enabled=false

helm template | kubectl apply --server-side avoids a Helm limitation with very large CRDs. If the provider already manages compatible Gateway API CRDs (for example GKE), set crds.gatewayAPI.enabled=false and install only the EG CRDs.

Gateway API version skew

Gateway API CRDs are cluster-scoped. EG v1.9 bundles v1.6.1, while Linkerd 2.20 documents 1.2.1 to 1.5.1. When a mesh shares the cluster, manage the CRDs outside the chart and pick a version every controller supports. See the Linkerd topic.

Enable Optional APIs

Backend, EnvoyPatchPolicy, and Lua are off by default:

helm upgrade eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 \
  -n envoy-gateway-system --reuse-values \
  --set config.envoyGateway.extensionApis.enableBackend=true \
  --set config.envoyGateway.extensionApis.enableEnvoyPatchPolicy=true

Warning

Enable EnvoyPatchPolicy only when you trust everyone who can create it. Patch authors can inject arbitrary Envoy configuration.

Upgrade Envoy Gateway

Helm does not upgrade CRDs, so upgrade them first, then the controller.

# 1. Gateway API and EG CRDs
helm template eg-crds oci://docker.io/envoyproxy/gateway-crds-helm \
  --version v1.9.1 \
  --set crds.gatewayAPI.enabled=true \
  --set crds.envoyGateway.enabled=true \
  | kubectl apply --force-conflicts --server-side -f -

# 2. Controller
helm upgrade eg oci://docker.io/envoyproxy/gateway-helm \
  --version v1.9.1 -n envoy-gateway-system

Upgrading to v1.9

  • Skip v1.9.0. Users on v1.8.x should go straight to v1.9.1. v1.9.0 changed SDS/RDS initial fetch timeouts, and the v1.9.1 revert can hit an Envoy bug on proxies still running during the upgrade.
  • Gateway API v1.6 CRDs are required. EG v1.9 reconciles TCPRoute and UDPRoute as gateway.networking.k8s.io/v1. Standard-channel users must change those manifests to v1 before upgrading the CRDs, or traffic for those routes is dropped.
  • Lua is now opt-in. Set extensionApis.enableLua: true if you use Lua EnvoyExtensionPolicies.
  • OIDC sessions reset. v1.9.1 and v1.8.4 switch session cookies to AES-256-GCM, so logged-in users re-authenticate once.

After the CRD upgrade, migrate stored TCPRoute and UDPRoute objects to the new storage version:

kubectl get tcproutes.gateway.networking.k8s.io -A -o json | kubectl replace -f -
kubectl get udproutes.gateway.networking.k8s.io -A -o json | kubectl replace -f -

Expose an HTTP Service

A Gateway with an HTTP listener and an HTTPRoute that sends /api on app.example.com to a Service:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: my-gateway
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: myapp-route
spec:
  parentRefs:
    - name: my-gateway
  hostnames: ["app.example.com"]
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: myapp-svc
          port: 8080

Check that the route was accepted and find the external address:

kubectl get gateway my-gateway -o jsonpath='{.status.addresses[0].value}'
kubectl get httproute myapp-route -o jsonpath='{.status.parents[0].conditions}'

Configure TLS Termination

Store the certificate as a kubernetes.io/tls Secret in the Gateway's namespace and reference it from an HTTPS listener:

kubectl create secret tls example-cert --cert=tls.crt --key=tls.key
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: eg
spec:
  gatewayClassName: eg
  listeners:
  - name: https
    protocol: HTTPS
    port: 443
    hostname: "app.example.com"
    tls:
      mode: Terminate
      certificateRefs:
      - kind: Secret
        name: example-cert

For TLS passthrough, use a listener with protocol: TLS, tls.mode: Passthrough, and a TLSRoute (gateway.networking.k8s.io/v1 since Gateway API v1.5).

Split Traffic for a Canary

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: canary
spec:
  parentRefs:
    - name: my-gateway
  rules:
    - backendRefs:
        - name: myapp-v1
          port: 8080
          weight: 90
        - name: myapp-v2
          port: 8080
          weight: 10

Shift the weights step by step, and watch error rates on myapp-v2 before each step.

Add Rate Limiting

Local (per Envoy replica)

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: local-rate-limit
spec:
  targetRefs:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: myapp-route
  rateLimit:
    local:
      rules:
      - clientSelectors:
        - headers:
          - name: x-user-id
            value: one
        limit:
          requests: 3
          unit: Hour

Global (shared across replicas)

First point EG at a Redis instance, which deploys envoy-ratelimit:

helm upgrade eg oci://docker.io/envoyproxy/gateway-helm \
  --set config.envoyGateway.rateLimit.backend.type=Redis \
  --set config.envoyGateway.rateLimit.backend.redis.url="redis.redis-system.svc.cluster.local:6379" \
  --reuse-values -n envoy-gateway-system

Then attach a global rule:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: global-rate-limit
spec:
  targetRefs:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: myapp-route
  rateLimit:
    global:
      rules:
      - limit:
          requests: 100
          unit: Minute

rateLimit.type: Global still works but is deprecated. Set global and/or local directly. Since v1.9, redis.urlRef can read the URL from a Secret in envoy-gateway-system.

Require a JWT

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: jwt-auth
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: myapp-route
  jwt:
    providers:
      - name: auth0
        issuer: https://my-tenant.auth0.com/
        remoteJWKS:
          uri: https://my-tenant.auth0.com/.well-known/jwks.json
        claimToHeaders:
          - claim: sub
            header: x-subject

Target the Gateway instead of the HTTPRoute to protect every route on it. Add recomputeRoute: true to a provider when later route matches depend on headers derived from claims.

Delegate to an External Authorization Service

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: ext-auth
spec:
  targetRefs:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      name: myapp-route
  extAuth:
    http:
      backendRefs:
        - name: http-ext-auth
          port: 9002
      headersToBackend: ["x-current-user"]

Use extAuth.grpc.backendRefs for a gRPC ext_authz server. To encrypt the hop to the auth service, attach a BackendTLSPolicy to its Service (next section). failOpen and statusOnError control behavior when the service is down.

Enable CORS

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: cors-example
spec:
  targetRefs:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: myapp-route
  cors:
    allowOrigins:
    - "https://*.example.com"
    allowMethods: [GET, POST]
    allowHeaders: ["x-header-1"]
    allowCredentials: true

Encrypt Traffic to Backends

TLS with BackendTLSPolicy

apiVersion: gateway.networking.k8s.io/v1
kind: BackendTLSPolicy
metadata:
  name: enable-backend-tls
spec:
  targetRefs:
  - group: ""
    kind: Service
    name: tls-backend
    sectionName: https
  validation:
    hostname: www.example.com
    caCertificateRefs:
    - name: example-ca
      group: ""
      kind: ConfigMap
    subjectAltNames:
    - type: Hostname
      hostname: san.example.com

On Gateway API releases before v1.4 the API version was v1alpha3.

Mutual TLS with the Backend CRD

Requires extensionApis.enableBackend=true. The gateway presents gateway-client-cert and validates the backend against backend-ca.

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: Backend
metadata:
  name: mtls-backend
spec:
  endpoints:
  - fqdn:
      hostname: secure-backend.default.svc.cluster.local
      port: 443
  tls:
    clientCertificateRef:
      kind: Secret
      name: gateway-client-cert
    caCertificateRefs:
    - group: ""
      kind: ConfigMap
      name: backend-ca

Reference it from a route with backendRefs: [{group: gateway.envoyproxy.io, kind: Backend, name: mtls-backend}].

Migrate from Ingress-NGINX

Ingress-NGINX is retired (maintenance ended March 2026), so plan a parallel cut-over instead of an in-place swap.

  1. Install EG next to the existing controller (see Install with Helm).
  2. Install ingress2gateway 1.0:

    go install github.com/kubernetes-sigs/ingress2gateway@v1.0.0
    # or: brew install ingress2gateway
    
  3. Convert Ingress objects and annotations, emitting EG policies where Gateway API has no equivalent:

    ingress2gateway print --providers=ingress-nginx --emitter=envoy-gateway -A > gateway-resources.yaml
    
  4. Review the warnings. Snippet annotations (configuration-snippet, server-snippet) and custom Lua have no automatic translation. Rebuild them with SecurityPolicy, BackendTrafficPolicy, HTTPRouteFilter, or, as a last resort, EnvoyPatchPolicy.

  5. Set gatewayClassName: eg on the generated Gateways, apply them, and test each hostname against the new Gateway address with curl --resolve.
  6. Move DNS, or the load balancer, host by host. Remove the Ingress objects and the old controller after a quiet period.

Run in Standalone Mode

Experimental; upstream says not to use it in production. This runs EG on a host with the File provider and Envoy as a local process.

mkdir -p /tmp/envoy-gateway-test
envoy-gateway certgen --local
cat > standalone.yaml <<'EOF'
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyGateway
gateway:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
provider:
  type: Custom
  custom:
    resource:
      type: File
      file:
        paths: ["/tmp/envoy-gateway-test"]
    infrastructure:
      type: Host
      host: {}
logging:
  level:
    default: info
extensionApis:
  enableBackend: true
EOF
envoy-gateway server --config-path standalone.yaml

Drop Gateway API manifests into /tmp/envoy-gateway-test/ (the repository ships examples/standalone/quickstart.yaml). Each change triggers an update. Routes point at local endpoints through Backend resources.

Debug with egctl

# Offline: what xDS would this manifest produce?
egctl x translate --from gateway-api --to xds -t route -f my-routes.yaml

# Live: dump config from a proxy pod
egctl config envoy-proxy route -n envoy-gateway-system <envoy-pod>
egctl config envoy-proxy cluster -n envoy-gateway-system <envoy-pod>

# Status conditions across all resources
egctl x status all -A

# Controller view of translated resources (v1.8+)
egctl config envoy-gateway all -n envoy-gateway-system

Watch xdsNACKTotal (v1.9) and the Envoy envoy_sds_init_fetch_timeout counter to catch configs that Envoy rejected or never received.

Troubleshoot Common Issues

Symptom Diagnosis Fix
Route not working kubectl get httproute -o yaml, check status.parents[].conditions Fix parentRefs, sectionName, hostnames, or listener allowedRoutes
Route accepted but shadowed RouteRulesOverlap warning condition (v1.9) Remove the duplicate match or make it more specific
TLS handshake errors Gateway listener condition ResolvedRefs, Secret contents Put the cert and key in the Gateway's namespace, or add a ReferenceGrant
503 responses kubectl get endpointslice -l kubernetes.io/service-name=<svc> Fix backend readiness and Service selectors and ports
Policy has no effect kubectl get backendtrafficpolicy <name> -o yaml, check ancestor conditions Check targetRefs; a more specific policy may override it
TCPRoute or UDPRoute ignored after upgrade Gateway API CRD version Install the v1.6 CRDs and use apiVersion: gateway.networking.k8s.io/v1
Lua policy rejected after v1.9 upgrade Policy status Set extensionApis.enableLua: true
Config change not applied on proxies xdsNACKTotal, egctl config envoy-proxy all Fix the policy that produced the rejected config; pinned Envoy image mismatches cause this too

Sources